diff --git a/crates/core/shturman-power/src/fsm.rs b/crates/core/shturman-power/src/fsm.rs index 0f76a05..5ff5c28 100644 --- a/crates/core/shturman-power/src/fsm.rs +++ b/crates/core/shturman-power/src/fsm.rs @@ -30,6 +30,8 @@ pub enum Event { UnderVoltage, ThermalTrip, GraceExpired, + ThermalCleared, // тепло вернулось в норму до PONR → abort thermal-shutdown (гейт reason==Thermal) + FailsafeCut, // MCU-авторитетный cut (зависший SoC / истёк hold-up) → off, необратимо } #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -39,6 +41,7 @@ pub enum Action { AccChanged(bool), StartGrace, Commit, + Cut, // MCU снял питание (fail-safe) — сервис логирует + переходит в off } pub struct PowerFsm { @@ -149,6 +152,23 @@ impl PowerFsm { }; vec![Action::Commit] } + // тепло вернулось до PONR → abort (только thermal-shutdown; ACC-off/under-voltage — no-op) + ( + ShuttingDown { + phase: Abortable, + reason: ShutdownReason::Thermal, + }, + E::ThermalCleared, + ) => { + self.state = Running; + vec![Action::ShutdownAborted] + } + // MCU fail-safe cut → off из любого не-off (необратимо, MCU-авторитет) + (Off, E::FailsafeCut) => vec![], + (_, E::FailsafeCut) => { + self.state = Off; + vec![Action::Cut] + } // committed/off/sleep/battery_cutoff + всё прочее — no-op (committed не abort-ится) _ => vec![], } @@ -264,4 +284,33 @@ mod tests { IgnitionState::Off ); } + + #[test] + fn thermal_cleared_aborts_only_thermal_abortable() { + let mut f = PowerFsm::new(); // Running + f.step(Event::ThermalTrip); // → ShuttingDown{Abortable, Thermal} + assert_eq!(f.step(Event::ThermalCleared), vec![Action::ShutdownAborted]); + assert_eq!(f.state(), State::Running); + + // из ACC-off-shutdown ThermalCleared — no-op + let mut g = PowerFsm::new(); + g.step(Event::AccOff); + assert_eq!(g.step(Event::ThermalCleared), vec![]); + assert_eq!(g.power_state(), PowerState::ShuttingDown); + } + + #[test] + fn failsafe_cut_forces_off_from_any_nonoff() { + let mut f = PowerFsm::new(); // Running + assert_eq!(f.step(Event::FailsafeCut), vec![Action::Cut]); + assert_eq!(f.state(), State::Off); + // из off — no-op + assert_eq!(f.step(Event::FailsafeCut), vec![]); + // даже из committed (необратимый shutdown) cut уводит в off + let mut g = PowerFsm::new(); + g.step(Event::AccOff); + g.step(Event::GraceExpired); // committed + assert_eq!(g.step(Event::FailsafeCut), vec![Action::Cut]); + assert_eq!(g.state(), State::Off); + } } diff --git a/crates/core/shturman-power/src/service.rs b/crates/core/shturman-power/src/service.rs index 4fe5a4f..d43754b 100644 --- a/crates/core/shturman-power/src/service.rs +++ b/crates/core/shturman-power/src/service.rs @@ -81,6 +81,9 @@ async fn apply_event( }); } Action::Commit => durable_barrier(), + Action::Cut => { + tracing::warn!("power: MCU fail-safe cut (SoC hang / hold-up budget) — forced off"); + } } } Ok(())