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:
@@ -30,6 +30,8 @@ pub enum Event {
|
|||||||
UnderVoltage,
|
UnderVoltage,
|
||||||
ThermalTrip,
|
ThermalTrip,
|
||||||
GraceExpired,
|
GraceExpired,
|
||||||
|
ThermalCleared, // тепло вернулось в норму до PONR → abort thermal-shutdown (гейт reason==Thermal)
|
||||||
|
FailsafeCut, // MCU-авторитетный cut (зависший SoC / истёк hold-up) → off, необратимо
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
@@ -39,6 +41,7 @@ pub enum Action {
|
|||||||
AccChanged(bool),
|
AccChanged(bool),
|
||||||
StartGrace,
|
StartGrace,
|
||||||
Commit,
|
Commit,
|
||||||
|
Cut, // MCU снял питание (fail-safe) — сервис логирует + переходит в off
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PowerFsm {
|
pub struct PowerFsm {
|
||||||
@@ -149,6 +152,23 @@ impl PowerFsm {
|
|||||||
};
|
};
|
||||||
vec![Action::Commit]
|
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-ится)
|
// committed/off/sleep/battery_cutoff + всё прочее — no-op (committed не abort-ится)
|
||||||
_ => vec![],
|
_ => vec![],
|
||||||
}
|
}
|
||||||
@@ -264,4 +284,33 @@ mod tests {
|
|||||||
IgnitionState::Off
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -81,6 +81,9 @@ async fn apply_event(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Action::Commit => durable_barrier(),
|
Action::Commit => durable_barrier(),
|
||||||
|
Action::Cut => {
|
||||||
|
tracing::warn!("power: MCU fail-safe cut (SoC hang / hold-up budget) — forced off");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
Reference in New Issue
Block a user