e54a34cd64
+ Action::Cut и его хендлер в apply_event (нужен для компиляции крейта — P8.6 шаг 2 сделан здесь). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
317 lines
10 KiB
Rust
317 lines
10 KiB
Rust
//! Чистый FSM питания (B03, спека v0.3 §5). Без D-Bus/async/I/O — сервис исполняет `Action`.
|
|
|
|
use shturman_ipc::types::{IgnitionState, PowerSource, PowerState, ShutdownReason};
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Phase {
|
|
Abortable,
|
|
Committed,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum State {
|
|
Off,
|
|
Accessory,
|
|
Running,
|
|
ShuttingDown {
|
|
phase: Phase,
|
|
reason: ShutdownReason,
|
|
},
|
|
Sleep, // зарезервировано (полные sleep/wake — v1/v2)
|
|
BatteryCutoff, // зарезервировано (long-park — v1/v2)
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Event {
|
|
AccOn,
|
|
AccOff,
|
|
EngineOn,
|
|
EngineOff,
|
|
UnderVoltage,
|
|
ThermalTrip,
|
|
GraceExpired,
|
|
ThermalCleared, // тепло вернулось в норму до PONR → abort thermal-shutdown (гейт reason==Thermal)
|
|
FailsafeCut, // MCU-авторитетный cut (зависший SoC / истёк hold-up) → off, необратимо
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum Action {
|
|
ShutdownImminent(ShutdownReason),
|
|
ShutdownAborted,
|
|
AccChanged(bool),
|
|
StartGrace,
|
|
Commit,
|
|
Cut, // MCU снял питание (fail-safe) — сервис логирует + переходит в off
|
|
}
|
|
|
|
pub struct PowerFsm {
|
|
state: State,
|
|
}
|
|
|
|
impl Default for PowerFsm {
|
|
fn default() -> Self {
|
|
Self {
|
|
state: State::Running,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl PowerFsm {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
pub fn state(&self) -> State {
|
|
self.state
|
|
}
|
|
|
|
/// D-Bus-проекция состояния (`PowerState`).
|
|
pub fn power_state(&self) -> PowerState {
|
|
match self.state {
|
|
State::Off => PowerState::Off,
|
|
State::Accessory => PowerState::Accessory,
|
|
State::Running => PowerState::Running,
|
|
State::ShuttingDown { .. } => PowerState::ShuttingDown,
|
|
State::Sleep => PowerState::Sleep,
|
|
State::BatteryCutoff => PowerState::BatteryCutoff,
|
|
}
|
|
}
|
|
/// Ось зажигания (канон, B §1).
|
|
pub fn ignition(&self) -> IgnitionState {
|
|
match self.state {
|
|
State::Running => IgnitionState::Running,
|
|
State::Accessory => IgnitionState::Accessory,
|
|
_ => IgnitionState::Off,
|
|
}
|
|
}
|
|
/// Источник питания — сигнал потребителям «времени мало» при shutdown.
|
|
pub fn source(&self) -> PowerSource {
|
|
match self.state {
|
|
State::ShuttingDown {
|
|
reason: ShutdownReason::UnderVoltage,
|
|
..
|
|
} => PowerSource::LowBattery,
|
|
State::ShuttingDown { .. } => PowerSource::HoldupCap,
|
|
_ => PowerSource::Vehicle12v,
|
|
}
|
|
}
|
|
|
|
/// Шаг FSM. Возвращает действия для исполнения сервисом (спека §5).
|
|
pub fn step(&mut self, ev: Event) -> Vec<Action> {
|
|
use Event as E;
|
|
use Phase::*;
|
|
use State::*;
|
|
match (self.state, ev) {
|
|
(Off, E::AccOn) => {
|
|
self.state = Accessory;
|
|
vec![Action::AccChanged(true)]
|
|
}
|
|
(Accessory, E::EngineOn) => {
|
|
self.state = Running;
|
|
vec![]
|
|
}
|
|
(Running, E::EngineOff) => {
|
|
self.state = Accessory;
|
|
vec![]
|
|
}
|
|
// ACC-off: линия ACC сменилась (AccChanged) + старт shutdown.
|
|
(Accessory | Running, E::AccOff) => {
|
|
self.state = ShuttingDown {
|
|
phase: Abortable,
|
|
reason: ShutdownReason::AccOff,
|
|
};
|
|
vec![
|
|
Action::AccChanged(false),
|
|
Action::ShutdownImminent(ShutdownReason::AccOff),
|
|
Action::StartGrace,
|
|
]
|
|
}
|
|
// under-voltage/thermal: ACC не менялся → без AccChanged.
|
|
(Accessory | Running, E::UnderVoltage) => {
|
|
self.begin_shutdown(ShutdownReason::UnderVoltage)
|
|
}
|
|
(Accessory | Running, E::ThermalTrip) => self.begin_shutdown(ShutdownReason::Thermal),
|
|
(
|
|
ShuttingDown {
|
|
phase: Abortable, ..
|
|
},
|
|
E::AccOn,
|
|
) => {
|
|
self.state = Running;
|
|
vec![Action::ShutdownAborted, Action::AccChanged(true)]
|
|
}
|
|
(
|
|
ShuttingDown {
|
|
phase: Abortable,
|
|
reason,
|
|
},
|
|
E::GraceExpired,
|
|
) => {
|
|
self.state = ShuttingDown {
|
|
phase: Committed,
|
|
reason,
|
|
};
|
|
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![],
|
|
}
|
|
}
|
|
|
|
fn begin_shutdown(&mut self, reason: ShutdownReason) -> Vec<Action> {
|
|
self.state = State::ShuttingDown {
|
|
phase: Phase::Abortable,
|
|
reason,
|
|
};
|
|
vec![Action::ShutdownImminent(reason), Action::StartGrace]
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn off_acc_on_to_accessory() {
|
|
let mut f = PowerFsm { state: State::Off };
|
|
assert_eq!(f.step(Event::AccOn), vec![Action::AccChanged(true)]);
|
|
assert_eq!(f.state(), State::Accessory);
|
|
}
|
|
|
|
#[test]
|
|
fn accessory_engine_on_to_running_and_back() {
|
|
let mut f = PowerFsm {
|
|
state: State::Accessory,
|
|
};
|
|
assert_eq!(f.step(Event::EngineOn), vec![]);
|
|
assert_eq!(f.state(), State::Running);
|
|
assert_eq!(f.step(Event::EngineOff), vec![]);
|
|
assert_eq!(f.state(), State::Accessory);
|
|
}
|
|
|
|
#[test]
|
|
fn acc_off_begins_abortable_shutdown() {
|
|
let mut f = PowerFsm::new(); // Running
|
|
assert_eq!(
|
|
f.step(Event::AccOff),
|
|
vec![
|
|
Action::AccChanged(false),
|
|
Action::ShutdownImminent(ShutdownReason::AccOff),
|
|
Action::StartGrace
|
|
]
|
|
);
|
|
assert_eq!(f.power_state(), PowerState::ShuttingDown);
|
|
assert_eq!(f.source(), PowerSource::HoldupCap);
|
|
}
|
|
|
|
#[test]
|
|
fn under_voltage_reason_and_source() {
|
|
let mut f = PowerFsm::new();
|
|
let a = f.step(Event::UnderVoltage);
|
|
assert_eq!(a[0], Action::ShutdownImminent(ShutdownReason::UnderVoltage));
|
|
assert_eq!(f.source(), PowerSource::LowBattery);
|
|
}
|
|
|
|
#[test]
|
|
fn abort_before_ponr() {
|
|
let mut f = PowerFsm::new();
|
|
f.step(Event::AccOff);
|
|
assert_eq!(
|
|
f.step(Event::AccOn),
|
|
vec![Action::ShutdownAborted, Action::AccChanged(true)]
|
|
);
|
|
assert_eq!(f.state(), State::Running);
|
|
}
|
|
|
|
#[test]
|
|
fn grace_expired_commits_and_is_irreversible() {
|
|
let mut f = PowerFsm::new();
|
|
f.step(Event::AccOff);
|
|
assert_eq!(f.step(Event::GraceExpired), vec![Action::Commit]);
|
|
assert_eq!(f.step(Event::AccOn), vec![]); // committed: abort игнорируется
|
|
assert!(matches!(
|
|
f.state(),
|
|
State::ShuttingDown {
|
|
phase: Phase::Committed,
|
|
..
|
|
}
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn reserved_states_noop() {
|
|
let mut f = PowerFsm {
|
|
state: State::Sleep,
|
|
};
|
|
assert_eq!(f.step(Event::AccOn), vec![]);
|
|
assert_eq!(f.state(), State::Sleep);
|
|
}
|
|
|
|
#[test]
|
|
fn ignition_projection() {
|
|
assert_eq!(
|
|
PowerFsm {
|
|
state: State::Running
|
|
}
|
|
.ignition(),
|
|
IgnitionState::Running
|
|
);
|
|
assert_eq!(
|
|
PowerFsm {
|
|
state: State::Accessory
|
|
}
|
|
.ignition(),
|
|
IgnitionState::Accessory
|
|
);
|
|
assert_eq!(
|
|
PowerFsm { state: State::Off }.ignition(),
|
|
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);
|
|
}
|
|
}
|