/* * test_rgb_fsm.c — RGB Status LED State Machine tests (Issue #290) * * Verifies: * - State transitions and initial state * - Animation progression for each LED state * - Timing and animation cycles * - State-specific animations (pulse, breathe, spin, blink, fill) * - Edge cases and invalid inputs */ #include #include #include #include /* ── LED State Machine Simulator ──────────────────────────────────*/ typedef enum { LED_STATE_BOOT = 0, LED_STATE_IDLE, LED_STATE_ARMED, LED_STATE_NAV, LED_STATE_ERROR, LED_STATE_LOW_BATT, LED_STATE_CHARGING, LED_STATE_ESTOP, LED_STATE_COUNT } LedState; typedef struct { LedState current_state; LedState previous_state; uint32_t state_start_time_ms; uint32_t last_tick_ms; uint8_t animation_frame; } RgbFsm; static RgbFsm sim = {0}; void sim_init(void) { memset(&sim, 0, sizeof(sim)); sim.current_state = LED_STATE_BOOT; sim.previous_state = LED_STATE_BOOT; } bool sim_set_state(LedState state) { if (state >= LED_STATE_COUNT) return false; if (state == sim.current_state) return false; sim.previous_state = sim.current_state; sim.current_state = state; sim.state_start_time_ms = (uint32_t)-1; sim.animation_frame = 0; return true; } LedState sim_get_state(void) { return sim.current_state; } void sim_tick(uint32_t now_ms) { if (sim.state_start_time_ms == (uint32_t)-1) { sim.state_start_time_ms = now_ms; return; } uint32_t elapsed = now_ms - sim.state_start_time_ms; switch (sim.current_state) { case LED_STATE_BOOT: sim.animation_frame = (elapsed % 2000) * 255 / 2000; break; case LED_STATE_IDLE: sim.animation_frame = (elapsed % 2000) * 255 / 2000; break; case LED_STATE_ARMED: sim.animation_frame = 255; break; case LED_STATE_NAV: sim.animation_frame = ((elapsed % 1000) / 125) * 32; break; case LED_STATE_ERROR: sim.animation_frame = ((elapsed % 500) < 250) ? 255 : 0; break; case LED_STATE_LOW_BATT: sim.animation_frame = ((elapsed % 1000) < 500) ? 255 : 0; break; case LED_STATE_CHARGING: sim.animation_frame = ((elapsed % 1000) / 125) * 32; break; case LED_STATE_ESTOP: sim.animation_frame = 255; break; default: break; } sim.last_tick_ms = 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_initial_state(void) { TEST("Initial state is BOOT"); sim_init(); ASSERT(sim_get_state() == LED_STATE_BOOT, "State is BOOT"); } void test_state_transitions(void) { TEST("State transitions work correctly"); sim_init(); bool result = sim_set_state(LED_STATE_IDLE); ASSERT(result == true, "Transition succeeds"); ASSERT(sim_get_state() == LED_STATE_IDLE, "State changed"); result = sim_set_state(LED_STATE_IDLE); ASSERT(result == false, "Same state returns false"); } void test_all_states(void) { TEST("All 8 states are accessible"); sim_init(); for (int i = 1; i < LED_STATE_COUNT; i++) { bool result = sim_set_state((LedState)i); ASSERT(result == true, "State transition succeeds"); } } void test_boot_animation(void) { TEST("BOOT state animates"); sim_init(); sim_set_state(LED_STATE_BOOT); sim_tick(0); sim_tick(500); uint8_t frame = sim.animation_frame; ASSERT(frame > 0 && frame < 255, "Animation progresses"); } void test_idle_animation(void) { TEST("IDLE state animates"); sim_init(); sim_set_state(LED_STATE_IDLE); sim_tick(0); sim_tick(500); ASSERT(sim.animation_frame > 0, "Animation starts"); } void test_armed_static(void) { TEST("ARMED state is static"); sim_init(); sim_set_state(LED_STATE_ARMED); sim_tick(0); sim_tick(100); ASSERT(sim.animation_frame == 255, "No animation"); } void test_nav_animation(void) { TEST("NAV state spins"); sim_init(); sim_set_state(LED_STATE_NAV); sim_tick(0); sim_tick(150); ASSERT(sim.animation_frame > 0, "Animation starts"); } void test_error_animation(void) { TEST("ERROR state flashes"); sim_init(); sim_set_state(LED_STATE_ERROR); sim_tick(0); sim_tick(100); ASSERT(sim.animation_frame == 255, "Bright state"); sim_tick(300); ASSERT(sim.animation_frame == 0, "Dark state"); } void test_low_batt_animation(void) { TEST("LOW_BATT state blinks"); sim_init(); sim_set_state(LED_STATE_LOW_BATT); sim_tick(0); sim_tick(100); ASSERT(sim.animation_frame == 255, "Bright"); sim_tick(600); ASSERT(sim.animation_frame == 0, "Dark"); } void test_charging_animation(void) { TEST("CHARGING state fills"); sim_init(); sim_set_state(LED_STATE_CHARGING); sim_tick(0); sim_tick(200); ASSERT(sim.animation_frame > 0, "Animation progresses"); } void test_estop_static(void) { TEST("ESTOP state is static"); sim_init(); sim_set_state(LED_STATE_ESTOP); sim_tick(0); sim_tick(100); ASSERT(sim.animation_frame == 255, "Solid red"); } void test_state_reset(void) { TEST("State change resets timing"); sim_init(); sim_set_state(LED_STATE_BOOT); sim_tick(0); sim_tick(500); uint8_t boot_frame = sim.animation_frame; sim_set_state(LED_STATE_IDLE); sim_tick(0); sim_tick(100); uint8_t idle_frame = sim.animation_frame; ASSERT(idle_frame < boot_frame, "Fresh animation start"); } void test_invalid_state(void) { TEST("Invalid state rejected"); sim_init(); bool result = sim_set_state((LedState)255); ASSERT(result == false, "Invalid rejected"); } void test_rapid_changes(void) { TEST("Rapid transitions work"); sim_init(); sim_set_state(LED_STATE_IDLE); ASSERT(sim_get_state() == LED_STATE_IDLE, "In IDLE"); sim_set_state(LED_STATE_ERROR); ASSERT(sim_get_state() == LED_STATE_ERROR, "In ERROR"); } int main(void) { printf("\n══════════════════════════════════════════════════════════════\n"); printf(" RGB Status LED State Machine — Unit Tests (Issue #290)\n"); printf("══════════════════════════════════════════════════════════════\n"); test_initial_state(); test_state_transitions(); test_all_states(); test_boot_animation(); test_idle_animation(); test_armed_static(); test_nav_animation(); test_error_animation(); test_low_batt_animation(); test_charging_animation(); test_estop_static(); test_state_reset(); test_invalid_state(); test_rapid_changes(); 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; }