feat(v0.4): FSM ThermalCleared (abort thermal) + FailsafeCut (MCU cut)

+ Action::Cut и его хендлер в apply_event (нужен для компиляции крейта — P8.6 шаг 2 сделан здесь).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
2026-06-25 15:29:36 +03:00
parent b9ae2f23d5
commit e54a34cd64
2 changed files with 52 additions and 0 deletions
+49
View File
@@ -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);
}
}