/* * lvc.c -- Low Voltage Cutoff (LVC) protection (Issue #613) * * State machine: * NORMAL -> WARNING when vbat < LVC_WARNING_MV * WARNING -> CRITICAL when vbat < LVC_CRITICAL_MV * CRITICAL -> CUTOFF when vbat < LVC_CUTOFF_MV * * Recovery: step down severity only when voltage exceeds the threshold * by LVC_HYSTERESIS_MV. CUTOFF is latched for the remainder of the session. * * Buzzer alerts: * WARNING -- MELODY_LOW_BATTERY once, then every 30 s * CRITICAL -- MELODY_LOW_BATTERY x2, then every 10 s * CUTOFF -- MELODY_ERROR (one-shot; motor disable handled by main.c) */ #include "lvc.h" #include "buzzer.h" #include "config.h" /* Periodic buzzer reminder intervals */ #define LVC_WARN_INTERVAL_MS 30000u /* 30 s */ #define LVC_CRIT_INTERVAL_MS 10000u /* 10 s */ static LvcState s_state = LVC_NORMAL; static bool s_cutoff_latched = false; static uint32_t s_buzzer_tick = 0u; /* ---- lvc_init() ---- */ void lvc_init(void) { s_state = LVC_NORMAL; s_cutoff_latched = false; s_buzzer_tick = 0u; } /* ---- lvc_tick() ---- */ void lvc_tick(uint32_t now_ms, uint32_t vbat_mv) { if (vbat_mv == 0u) { return; /* ADC not ready; hold current state */ } /* Determine new state from raw voltage */ LvcState new_state; if (vbat_mv < LVC_CUTOFF_MV) { new_state = LVC_CUTOFF; } else if (vbat_mv < LVC_CRITICAL_MV) { new_state = LVC_CRITICAL; } else if (vbat_mv < LVC_WARNING_MV) { new_state = LVC_WARNING; } else { new_state = LVC_NORMAL; } /* Hysteresis on recovery: only decrease severity when voltage exceeds * the threshold by LVC_HYSTERESIS_MV to prevent rapid toggling. */ if (new_state < s_state) { LvcState recovered; if (vbat_mv >= LVC_CUTOFF_MV + LVC_HYSTERESIS_MV) { recovered = LVC_CRITICAL; } else if (vbat_mv >= LVC_CRITICAL_MV + LVC_HYSTERESIS_MV) { recovered = LVC_WARNING; } else if (vbat_mv >= LVC_WARNING_MV + LVC_HYSTERESIS_MV) { recovered = LVC_NORMAL; } else { recovered = s_state; /* insufficient margin; stay at current level */ } new_state = recovered; } /* CUTOFF latch: once triggered, only a reboot clears it */ if (s_cutoff_latched) { new_state = LVC_CUTOFF; } if (new_state == LVC_CUTOFF) { s_cutoff_latched = true; } /* Buzzer alerts */ bool state_changed = (new_state != s_state); s_state = new_state; switch (s_state) { case LVC_WARNING: if (state_changed || (now_ms - s_buzzer_tick) >= LVC_WARN_INTERVAL_MS) { s_buzzer_tick = now_ms; buzzer_play_melody(MELODY_LOW_BATTERY); } break; case LVC_CRITICAL: if (state_changed || (now_ms - s_buzzer_tick) >= LVC_CRIT_INTERVAL_MS) { s_buzzer_tick = now_ms; /* Double alert to distinguish critical from warning */ buzzer_play_melody(MELODY_LOW_BATTERY); buzzer_play_melody(MELODY_LOW_BATTERY); } break; case LVC_CUTOFF: if (state_changed) { /* One-shot alarm; motors disabled by main.c */ buzzer_play_melody(MELODY_ERROR); } break; default: break; } } /* ---- lvc_get_state() ---- */ LvcState lvc_get_state(void) { return s_state; } /* ---- lvc_get_power_scale() ---- */ /* * Returns the motor power scale factor (0-100): * NORMAL / WARNING : 100% -- no reduction * CRITICAL : 50% -- halve motor commands * CUTOFF : 0% -- all commands zeroed */ uint8_t lvc_get_power_scale(void) { switch (s_state) { case LVC_NORMAL: /* fall-through */ case LVC_WARNING: return 100u; case LVC_CRITICAL: return 50u; case LVC_CUTOFF: return 0u; default: return 100u; } } /* ---- lvc_is_cutoff() ---- */ bool lvc_is_cutoff(void) { return s_cutoff_latched; }