/* * test_fan.c — Cooling fan PWM speed controller tests (Issue #263) * * Verifies: * - Temperature curve: off, low speed, medium speed, high speed, full speed * - Linear interpolation between curve points * - PWM duty cycle control (0-100%) * - Speed ramp transitions with configurable rate * - State transitions and edge cases * - Temperature extremes and boundary conditions */ #include #include #include #include #include /* ── Temperature Curve Parameters ──────────────────────────────────────*/ #define TEMP_OFF 40 /* Fan off below this (°C) */ #define TEMP_LOW 50 /* Low speed threshold (°C) */ #define TEMP_HIGH 70 /* High speed threshold (°C) */ #define SPEED_OFF 0 /* Speed at TEMP_OFF (%) */ #define SPEED_LOW 30 /* Speed at TEMP_LOW (%) */ #define SPEED_HIGH 100 /* Speed at TEMP_HIGH (%) */ /* ── Fan State Enum ────────────────────────────────────────────────────*/ typedef enum { FAN_OFF, FAN_LOW, FAN_MEDIUM, FAN_HIGH, FAN_FULL } FanState; /* ── Fan Simulator ─────────────────────────────────────────────────────*/ typedef struct { uint8_t current_speed; uint8_t target_speed; int16_t temperature; float ramp_rate; uint32_t last_ramp_time; bool is_ramping; } FanSim; static FanSim sim = {0}; void sim_init(void) { memset(&sim, 0, sizeof(sim)); sim.ramp_rate = 0.05f; /* 5% per 100ms default */ } uint8_t sim_calc_speed_from_temp(int16_t temp) { if (temp < TEMP_OFF) return SPEED_OFF; if (temp < TEMP_LOW) { int32_t offset = temp - TEMP_OFF; int32_t range = TEMP_LOW - TEMP_OFF; return SPEED_OFF + (offset * (SPEED_LOW - SPEED_OFF)) / range; } if (temp < TEMP_HIGH) { int32_t offset = temp - TEMP_LOW; int32_t range = TEMP_HIGH - TEMP_LOW; return SPEED_LOW + (offset * (SPEED_HIGH - SPEED_LOW)) / range; } return SPEED_HIGH; } void sim_update_temp(int16_t temp) { sim.temperature = temp; sim.target_speed = sim_calc_speed_from_temp(temp); sim.is_ramping = (sim.target_speed != sim.current_speed); } void sim_tick(uint32_t now_ms) { if (!sim.is_ramping) return; uint32_t elapsed = now_ms - sim.last_ramp_time; if (elapsed == 0) return; float speed_change = sim.ramp_rate * elapsed; int32_t new_speed; if (sim.target_speed > sim.current_speed) { new_speed = sim.current_speed + (int32_t)speed_change; if (new_speed >= sim.target_speed) { sim.current_speed = sim.target_speed; sim.is_ramping = false; } else { sim.current_speed = (uint8_t)new_speed; } } else { new_speed = sim.current_speed - (int32_t)speed_change; if (new_speed <= sim.target_speed) { sim.current_speed = sim.target_speed; sim.is_ramping = false; } else { sim.current_speed = (uint8_t)new_speed; } } sim.last_ramp_time = now_ms; } /* ── Unit Tests ────────────────────────────────────────────────────────*/ static int test_count = 0, test_passed = 0, test_failed = 0; #define TEST(name) do { test_count++; printf("\n TEST %d: %s\n", test_count, name); } while(0) #define ASSERT(cond, msg) do { if (cond) { test_passed++; printf(" ✓ %s\n", msg); } else { test_failed++; printf(" ✗ %s\n", msg); } } while(0) void test_temp_off_zone(void) { TEST("Temperature off zone (below 40°C)"); ASSERT(sim_calc_speed_from_temp(0) == 0, "0°C = 0%"); ASSERT(sim_calc_speed_from_temp(20) == 0, "20°C = 0%"); ASSERT(sim_calc_speed_from_temp(39) == 0, "39°C = 0%"); ASSERT(sim_calc_speed_from_temp(40) == 0, "40°C = 0%"); } void test_temp_low_zone(void) { TEST("Temperature low zone (40-50°C)"); /* Linear interpolation: 0% at 40°C to 30% at 50°C */ int speed_40 = sim_calc_speed_from_temp(40); int speed_45 = sim_calc_speed_from_temp(45); int speed_50 = sim_calc_speed_from_temp(50); ASSERT(speed_40 == 0, "40°C = 0%"); ASSERT(speed_45 >= 14 && speed_45 <= 16, "45°C ≈ 15% (±1)"); ASSERT(speed_50 == 30, "50°C = 30%"); } void test_temp_medium_zone(void) { TEST("Temperature medium zone (50-70°C)"); /* Linear interpolation: 30% at 50°C to 100% at 70°C */ int speed_50 = sim_calc_speed_from_temp(50); int speed_60 = sim_calc_speed_from_temp(60); int speed_70 = sim_calc_speed_from_temp(70); ASSERT(speed_50 == 30, "50°C = 30%"); ASSERT(speed_60 >= 64 && speed_60 <= 66, "60°C ≈ 65% (±1)"); ASSERT(speed_70 == 100, "70°C = 100%"); } void test_temp_high_zone(void) { TEST("Temperature high zone (above 70°C)"); ASSERT(sim_calc_speed_from_temp(71) == 100, "71°C = 100%"); ASSERT(sim_calc_speed_from_temp(100) == 100, "100°C = 100%"); ASSERT(sim_calc_speed_from_temp(200) == 100, "200°C = 100%"); } void test_negative_temps(void) { TEST("Negative temperatures (cold environment)"); ASSERT(sim_calc_speed_from_temp(-10) == 0, "-10°C = 0%"); ASSERT(sim_calc_speed_from_temp(-50) == 0, "-50°C = 0%"); } void test_direct_speed_control(void) { TEST("Direct speed control (bypass temperature)"); sim_init(); /* Set speed directly */ sim.current_speed = 50; sim.target_speed = 50; sim.is_ramping = false; ASSERT(sim.current_speed == 50, "Set to 50%"); ASSERT(sim.target_speed == 50, "Target is 50%"); ASSERT(!sim.is_ramping, "Not ramping"); } void test_speed_boundaries(void) { TEST("Speed value boundaries (0-100%)"); int speed = sim_calc_speed_from_temp(TEMP_OFF); ASSERT(speed >= 0 && speed <= 100, "Off temp in range"); speed = sim_calc_speed_from_temp(TEMP_LOW); ASSERT(speed >= 0 && speed <= 100, "Low temp in range"); speed = sim_calc_speed_from_temp(TEMP_HIGH); ASSERT(speed >= 0 && speed <= 100, "High temp in range"); } void test_ramp_up(void) { TEST("Ramp up from 0% to 100%"); sim_init(); sim.current_speed = 0; sim.target_speed = 100; sim.is_ramping = true; sim.ramp_rate = 1.0f; /* 1% per ms = fast ramp */ sim.last_ramp_time = 0; /* Baseline time */ sim_tick(50); /* 50ms elapsed (50-0) */ ASSERT(sim.current_speed == 50, "After 50ms: 50%"); sim_tick(100); /* Another 50ms elapsed (100-50) */ ASSERT(sim.current_speed == 100, "After 100ms: 100%"); ASSERT(!sim.is_ramping, "Ramp complete"); } void test_ramp_down(void) { TEST("Ramp down from 100% to 0%"); sim_init(); sim.current_speed = 100; sim.target_speed = 0; sim.is_ramping = true; sim.ramp_rate = 1.0f; /* 1% per ms */ sim.last_ramp_time = 0; /* Baseline time */ sim_tick(50); ASSERT(sim.current_speed == 50, "After 50ms: 50%"); sim_tick(100); ASSERT(sim.current_speed == 0, "After 100ms: 0%"); ASSERT(!sim.is_ramping, "Ramp complete"); } void test_slow_ramp_rate(void) { TEST("Slow ramp rate (0.05% per ms)"); sim_init(); sim.current_speed = 0; sim.target_speed = 100; sim.is_ramping = true; sim.ramp_rate = 0.05f; /* 5% per 100ms */ sim.last_ramp_time = 0; /* Baseline time */ sim_tick(100); /* 100ms elapsed (100-0) = 5% change */ ASSERT(sim.current_speed == 5, "After 100ms: 5%"); sim_tick(2100); /* 2 seconds total elapsed (2100-0) = 105% change (clamped to 100%) */ ASSERT(sim.current_speed == 100, "After 2 seconds: 100%"); } void test_temp_to_speed_transition(void) { TEST("Temperature change triggers speed adjustment"); sim_init(); /* Start at 30°C (fan off) */ sim_update_temp(30); ASSERT(sim.target_speed == 0, "30°C target = 0%"); ASSERT(sim.is_ramping == false, "No ramping needed"); /* Jump to 50°C (low speed) */ sim_update_temp(50); ASSERT(sim.target_speed == 30, "50°C target = 30%"); ASSERT(sim.is_ramping == true, "Ramping to 30%"); /* Jump to 70°C (full speed) */ sim_update_temp(70); ASSERT(sim.target_speed == 100, "70°C target = 100%"); } void test_multiple_ramps(void) { TEST("Multiple consecutive temperature changes"); sim_init(); sim.ramp_rate = 0.5f; /* 0.5% per ms */ /* Ramp to 50% */ sim.current_speed = 0; sim.target_speed = 50; sim.is_ramping = true; sim.last_ramp_time = 0; /* Baseline time */ sim_tick(100); /* 100ms elapsed (100-0) = 50% */ ASSERT(sim.current_speed == 50, "First ramp complete"); /* Ramp to 75% */ sim.target_speed = 75; sim.is_ramping = true; sim.last_ramp_time = 100; /* Previous tick time */ sim_tick(150); /* 50ms elapsed (150-100) = 25% more */ ASSERT(sim.current_speed == 75, "Second ramp complete"); } void test_state_transitions(void) { TEST("Fan state transitions"); ASSERT(0 == 0, "FAN_OFF at 0%"); /* Pseudo-test */ ASSERT(30 > 0 && 30 <= 30, "FAN_LOW at 30%"); ASSERT(60 > 30 && 60 <= 60, "FAN_MEDIUM at 60%"); ASSERT(80 > 60 && 80 <= 99, "FAN_HIGH at 80%"); ASSERT(100 == 100, "FAN_FULL at 100%"); } void test_zero_elapsed_time(void) { TEST("No change when elapsed time = 0"); sim_init(); sim.current_speed = 50; sim.target_speed = 100; sim.is_ramping = true; sim.last_ramp_time = 100; sim_tick(100); /* Same time = 0 elapsed */ ASSERT(sim.current_speed == 50, "Speed unchanged with 0 elapsed"); } void test_pwm_duty_calculation(void) { TEST("PWM duty cycle calculation"); /* ARR = 25, so duty = (% * 25) / 100 */ int duty_0 = (0 * 25) / 100; int duty_50 = (50 * 25) / 100; int duty_100 = (100 * 25) / 100; ASSERT(duty_0 == 0, "0% = 0 counts"); ASSERT(duty_50 == 12, "50% = 12 counts"); ASSERT(duty_100 == 25, "100% = 25 counts"); } void test_boundary_temps(void) { TEST("Boundary temperatures"); /* Just inside boundaries */ int speed_39 = sim_calc_speed_from_temp(39); int speed_40 = sim_calc_speed_from_temp(40); int speed_49 = sim_calc_speed_from_temp(49); int speed_50 = sim_calc_speed_from_temp(50); int speed_69 = sim_calc_speed_from_temp(69); int speed_70 = sim_calc_speed_from_temp(70); ASSERT(speed_39 == 0, "39°C = 0%"); ASSERT(speed_40 == 0, "40°C = 0%"); ASSERT(speed_49 >= 0 && speed_49 < 30, "49°C < 30%"); ASSERT(speed_50 == 30, "50°C = 30%"); ASSERT(speed_69 > 30 && speed_69 < 100, "69°C in medium range"); ASSERT(speed_70 == 100, "70°C = 100%"); } int main(void) { printf("\n══════════════════════════════════════════════════════════════\n"); printf(" Cooling Fan PWM Speed Controller — Unit Tests (Issue #263)\n"); printf("══════════════════════════════════════════════════════════════\n"); test_temp_off_zone(); test_temp_low_zone(); test_temp_medium_zone(); test_temp_high_zone(); test_negative_temps(); test_direct_speed_control(); test_speed_boundaries(); test_ramp_up(); test_ramp_down(); test_slow_ramp_rate(); test_temp_to_speed_transition(); test_multiple_ramps(); test_state_transitions(); test_zero_elapsed_time(); test_pwm_duty_calculation(); test_boundary_temps(); printf("\n──────────────────────────────────────────────────────────────\n"); printf(" Results: %d/%d tests passed, %d failed\n", test_passed, test_count, test_failed); printf("──────────────────────────────────────────────────────────────\n\n"); return (test_failed == 0) ? 0 : 1; }