docs(v0.3): спека power-safe ядра (FSM + graceful shutdown)

Веха v0.3: стаб Power → реальный lifecycle-FSM. Состояния off/accessory/running/
shutting_down{abortable,committed} (sleep/battery_cutoff — каркас); graceful
shutdown (ShutdownImminent→grace→durable-barrier sync→commit=PONR=unmount); abort
до PONR (re-power→ShutdownAborted). dev-mock кормит входы FSM. Watchdog/save-time/
монотоника. Подход A: FSM+сигналы, teardown через systemd/харнесс. Гибрид-E2E:
N=3 in-VM цикла + 1 reboot + abort + power-cut-сим. HW (hold-up/MCU/B08-B09) — v0.4.
Красные линии: Power не трогает CAN, без actuator (#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-24 20:51:58 +03:00
parent fd5c5c2dd5
commit 4fe5103e88
+241
View File
@@ -0,0 +1,241 @@
# Спека реализации: v0.3 — Power-safe ядро (FSM + graceful shutdown)
> Веха `v0.3` роадмапа: «переживает срыв питания». Capabilities **B01** (детект ACC), **B02** (graceful
> shutdown sequencing), **B03** (FSM + abort/committed), **B04** (`ru.shturman.Power`), **B05** (watchdog),
> **B06** (load-shedding), **B07** (save last-known-time), **A14** (HW-watchdog + recovery). Поверх **v0.2**.
> Источники: `docs/domains/b-power-lifecycle.md`, `docs/contracts/safety.md`, `docs/contracts/hardware.md`,
> `docs/contracts/ipc.md` §3, `docs/roadmap.md` §v0.3. Приёмка роадмапа: **«N циклов зажигания без потери `/data`;
> abort до PONR»**.
---
## 1. Цель и первый артефакт
Оживить **стаб** Power (v0.1: плоский `State`, mock флипает ignition/power) в **реальный lifecycle-FSM**:
ACC → graceful shutdown с **durable-write до PONR** → переживание срыва питания.
**Первый артефакт:** fake-ACC-off → FSM `running``shutting_down``ShutdownImminent` → grace → commit
(durable-write barrier `sync` → unmount `/data` = PONR); **N=3 цикла зажигания**`/data` цел; **abort до PONR**
(re-power → `ShutdownAborted``running`); **power-cut-сим** (SIGKILL до fsync → `/data` консистентен).
**Не цель v0.3:** реальный hold-up cap / MCU-протокол / fail-safe-таймер и выбор **B08/B09** (MCU vs supercap) —
**v0.4** (вероятно нужна аппаратная проверка); реальный `/dev/watchdog` арминг — HW; полные sleep/wake/long-park —
v1/v2; перф-вердикт — RK3588.
---
## 2. Скоуп и границы
### 2.1 В скоупе (делаем сейчас)
- **FSM питания (B03):** чистый модуль `PowerFsm` — состояния, события, переходы, действия. Юнит-тестируемый
без D-Bus. Внутренние субсостояния `ShuttingDown{Abortable, Committed}` маппятся в D-Bus `shutting_down`.
- **Детект ACC-логика (B01):** debounce/гистерезис + crank-приоритет — **формализованы в FSM** (вход `AccOff`
принимается только как стабильный; в VM источник — fake-ACC dev-mock). Реальный GPIO/MCU-детект — HW.
- **Graceful shutdown sequencing core (B02):** `ShutdownImminent(sec, reason)` → grace-окно → **durable-write
barrier** (`sync(2)`; Settings уже синхронен) → commit → unmount (PONR). Ordered teardown апов/CAN — позже (§2.2).
- **Abort до PONR (B03):** re-power в abortable → `ShutdownAborted` → назад в `running`/`accessory`.
- **Load-shedding (B06):** хук на commit/power-loss — в v0 **лог** (реальных нагрузок нет; рейлы amp/подсветка/
модем — HW).
- **Watchdog (B05/A14):** systemd `RuntimeWatchdogSec` (runtime) + `RebootWatchdogSec` (shutdown-фаза) — **конфиг
+ дисциплина**. Реальный `/dev/watchdog`/MCU-арминг — HW.
- **Save last-known-time (B07):** `shturman-savetime.timer`+`.service` — periodic fake-hwclock save (`/data`) +
on-shutdown save (в graceful sequence до PONR). `fake-hwclock``/data` — уже из v0.6.
- **Монотоника (§8):** `Uptime` + grace-timer + все lifecycle-таймеры на `CLOCK_MONOTONIC`
(`shturman_common::monotonic_secs` уже есть). НЕ wall-clock.
- **Харнесс:** FSM-юниты (каждый переход) + integration (сигналы по session-шине) + E2E **гибрид** (§9).
### 2.2 Явно НЕ в скоупе (отложено, с указателем «куда»)
- **Hold-up cap / supercap, MCU-копилот, MCU fail-safe-таймер, SoC↔MCU heartbeat/`safe-to-cut`, реальный
power-cut energy budget + дератинг по T** — **HW** (hardware §3); **выбор B08/B09 (MCU vs supercap-only) — v0.4**.
- **Реальный `/dev/watchdog` арминг + bootcount-handshake recovery** — **HW/v4** (в VM watchdog-device нет).
- **Полные sleep/wake + long-park battery-cutoff** (низкопотребление, wake-on-ACC/таймер/реверс) — **v1/v2** (B §7).
В v0.3 состояния `sleep`/`battery_cutoff`**зарезервированы** (переходы заглушены).
- **Consumer-ack save-протокол** (`ShutdownImminent`→consumers save→ack/timeout, сумма ≤ hold-up-бюджет) —
**App-Host v3** (в v0 Settings durable-write синхронен → ack не нужен).
- **E гасит OBD-TX / закрывает ISO-TP при shutdown** — **домен E / v1** (Power **не трогает CAN**, §3).
- **Перф-вердикт** (time-to-shutdown, hold-time) — **RK3588** (performance §2). В VM — функционально.
### 2.3 Частично в скоупе (каркас сейчас, тело — позже)
- **`sleep`/`battery_cutoff`** — состояния в enum/FSM есть, переходы **no-op/заглушка** (тело — v1/v2).
- **Load-shedding** — лог-хук (реальные рейлы — HW).
- **Watchdog** — конфиг systemd (реальный арминг — HW).
- **ACC-детект** — debounce-логика в FSM (источник в VM — fake-ACC; реальный GPIO/MCU — HW).
### 2.4 Трассируемость ID → статус
| ID | Веха | Статус в v0.3 |
|----|------|----------------|
| B01 | Детект ACC (debounce + crank) | логика в FSM; источник VM = fake-ACC (реальный GPIO/MCU — HW) |
| B02 | Graceful shutdown sequencing | core: ShutdownImminent→grace→durable-barrier→commit (teardown апов/CAN — позже) |
| B03 | FSM + abort/committed | полностью (sleep/battery_cutoff — каркас) |
| B04 | `ru.shturman.Power` | оживлён из FSM (сигналы/состояние — реальные переходы) |
| B05 | Watchdog | конфиг RuntimeWatchdogSec/RebootWatchdogSec + дисциплина (реальный WDT — HW) |
| B06 | Load-shedding | лог-хук (реальные нагрузки — HW) |
| B07 | Save last-known-time | periodic timer + on-shutdown save в `/data` |
| A14 | HW-watchdog + recovery | конфиг + дисциплина (реальный арминг/recovery — HW/v4) |
---
## 3. Красные линии, безопасность
- **#2 (нерушимо):** Power **не трогает CAN** и **не имеет actuator-путей** — только software-оркестрация
lifecycle + **read-only** состояние. CAN-TX-гашение при shutdown — **домен E (v1)**, не Power. Граница — safety.md.
- **#5 (power-safe):** durable-write до PONR — главная гарантия; FSM коммитит только после grace + `sync`. Реальный
power-cut на HW; в VM — функциональная модель + атомарность файла (foundation §9.1) уже доказана.
- **Прод-гейт:** dev-mock `PowerMock1` (fake-ACC/voltage/thermal) — за фичей `dev-mocks`; прод `--no-default-features`
→ не регистрируется (foundation §5.2, §8.3).
---
## 4. Раскладка (новые/изменённые артефакты)
- **`crates/core/shturman-power/src/fsm.rs`** (новый) — `PowerFsm`: `State`, `Event`, `Action`, `fn step(&mut self,
Event) -> Vec<Action>`. Чистый, без D-Bus/async. Grace-таймер — снаружи (сервис), FSM лишь даёт `StartGrace(sec)`/
принимает `GraceExpired`.
- **`crates/core/shturman-power/src/service.rs`** — обернуть FSM: D-Bus state/properties/signals **из FSM**;
dev-mock методы **кормят FSM-события** (не флипают `State` напрямую).
- **`crates/core/shturman-power/src/main.rs`** — grace-таймер (монотоника, tokio), durable-write barrier (`sync`),
трансляция FSM-actions → D-Bus-сигналы.
- **systemd:** `shturman-power.service` drop-in `RuntimeWatchdogSec=` (дисциплина); `shturman-savetime.service`+
`.timer` (B07 periodic save); system `RebootWatchdogSec=` (shutdown-дедлайн). Раскладка — lima/E2E.
- **harness:** `tests/e2e/run.sh` — блок power-safe (гибрид §9); integration-тесты в `crates/core/shturman-power/tests/`.
---
## 5. FSM питания (B03) — контракт
**Состояния (внутренние):** `Off`, `Accessory`, `Running`, `ShuttingDown { phase: Abortable | Committed, reason }`,
`Sleep`*, `BatteryCutoff`* (`*` — зарезервированы). **D-Bus-маппинг** (`PowerState`): `ShuttingDown{*}` → `shutting_down`;
остальные 1:1.
**События (`Event`):** `AccOn`, `AccOff`, `EngineOn`, `EngineOff` (accessory↔running по напряжению; VM — mock),
`UnderVoltage`, `ThermalTrip`, `GraceExpired`. (Re-power = `AccOn` во время shutdown.)
**Переходы:**
| Из | Событие | В | Действия |
|----|---------|---|----------|
| `Off` | `AccOn` | `Accessory` | `EmitAccChanged(true)` |
| `Accessory` | `EngineOn` | `Running` | — |
| `Running` | `EngineOff` | `Accessory` | — |
| `Accessory`/`Running` | `AccOff` | `ShuttingDown{Abortable, acc_off}` | `EmitShutdownImminent(sec, acc_off)`, `StartGrace(sec)` |
| `Accessory`/`Running` | `UnderVoltage` | `ShuttingDown{Abortable, under_voltage}` | `EmitShutdownImminent(sec, under_voltage)`, `StartGrace(sec)` |
| `Accessory`/`Running` | `ThermalTrip` | `ShuttingDown{Abortable, thermal}` | `EmitShutdownImminent(sec, thermal)`, `StartGrace(sec)` |
| `ShuttingDown{Abortable}` | `AccOn` (re-power) | `Running` | `EmitShutdownAborted`, `EmitAccChanged(true)` |
| `ShuttingDown{Abortable}` | `GraceExpired` | `ShuttingDown{Committed}` | `Commit` (durable-barrier → PONR) |
| `ShuttingDown{Committed}` | — | `Off` | (cut: unmount + снятие питания — systemd/харнесс/HW) |
| `Sleep`/`BatteryCutoff` | * | (no-op) | зарезервировано (v1/v2) |
**Действия (`Action`):** `EmitShutdownImminent(reason)`, `EmitShutdownAborted`, `EmitAccChanged(bool)`,
`StartGrace(secs)`, `Commit`. `reason ∈ {acc_off, under_voltage, thermal, battery_cutoff}`.
**Инвариант:** после `Committed` abort невозможен (только → `Off`).
**Чистота:** `step` детерминирован, без I/O; сервис исполняет действия (сигналы/таймер/`sync`). Юнит-тест — каждый переход.
---
## 6. Graceful shutdown (B02/B06) — последовательность (подход A)
Power-сервис = **FSM + сигналы + grace/abort-окно**; реальный teardown (unmount/cut) — через systemd (реальный
poweroff) либо харнесс (in-VM-цикл). На железе — MCU/supercap-sequencing (v0.4).
1. `AccOff` (стабильный; VM — fake-ACC) → `ShuttingDown{Abortable}` → **`ShutdownImminent(sec, acc_off)`** + grace-таймер
(монотоника). Потребители (приборка/будущие апы) получают сигнал и сохраняются.
2. **Abort-окно (abortable):** `AccOn` до `GraceExpired` → `EmitShutdownAborted` → `Running`; откат load-shed (v0: лог).
3. **`GraceExpired` → Commit:** save last-known-time (B07) → **durable-write barrier** (`sync(2)`; Settings уже
синхронен по каждому Set) → `Committed` (**= PONR**).
4. **PONR = unmount `/data`** (RW→RO): на реальном poweroff — systemd; в in-VM-цикле — харнесс; на HW — MCU/supercap
дают энергию завершить unmount/sync до cut.
5. **Load-shedding (B06):** на commit/power-loss — лог `«load-shed: amp/backlight/modem (реальных нагрузок нет в v0)»`;
hold-up кормит SoC+хранилище — HW.
**Гарантия #5:** commit (и потому unmount/PONR) наступает **только после** grace + `sync` → усечённый shutdown
оставляет `/data` консистентным (атомарность файлов — foundation §9.1).
---
## 7. D-Bus `ru.shturman.Power` — v0.3 оживляет (расширение foundation §5.2)
- **Состояние/properties из FSM** (не из плоского `State`): `GetPowerState`, `IgnitionState`, `PowerSource`, `Uptime`.
- **Сигналы из FSM-actions** (не из mock-флипа): `AccChanged`, `ShutdownImminent(sec, reason)`, `ShutdownAborted`.
`Sleep`/`Wake` — **объявлены, не эмитятся** (sleep — v1/v2).
- **`PowerSource`:** `vehicle_12v` (норма) → на under-voltage/commit сигналим `holdup_cap`/`low_battery` (потребителям
«времени мало»). `sleep_rail` — v1/v2.
- **dev-mock `ru.shturman.dev.PowerMock1` (fake-ACC, фича `dev-mocks`) — кормит входы FSM:**
- `SetAcc(on)` → `AccOn`/`AccOff`;
- `SetIgnition(state)` → `EngineOn`/`EngineOff` (accessory↔running) либо `AccOn`/`AccOff`;
- `TriggerShutdown(sec, reason)` → `UnderVoltage`/`ThermalTrip` с заданным grace;
- `AbortShutdown()` → re-power (`AccOn`) в abortable.
Прод-сборка mock не регистрирует (#3-гейт §3). Policy `send_destination=ru.shturman.Power` покрывает (foundation §13).
---
## 8. Watchdog / монотоника / save-time
- **Watchdog (B05/A14):** drop-in `shturman-power.service.d/watchdog.conf` — `RuntimeWatchdogSec=` (дисциплина: один
userspace-владелец WDT). System `systemd/system.conf.d` `RebootWatchdogSec=` — дедлайн shutdown-фазы (зависание в
unmount/sync не оставит устройство под питанием). **В VM `/dev/watchdog` нет → конфиг присутствует, реальный арминг —
HW** (VM↔HW-граница, как zram/vcan в v0.6). MCU-backstop — v0.4.
- **Монотоника (§8):** `Uptime` + grace-таймер + sleep/wake-таймеры на `CLOCK_MONOTONIC` (`monotonic_secs`). Wall-clock
легитимно прыгает на NTP/GPS-синке — lifecycle на него не завязан.
- **Save last-known-time (B07):** `shturman-savetime.service` (`fake-hwclock save` с `FILE=/data/...`, как в v0.6) +
`.timer` (~15 мин, monotonic). On-shutdown save — шаг 3 §6. После срыва часы откатываются максимум на интервал.
---
## 9. Dev-харнесс и план тестирования
### 9.1 Unit (FSM — `fsm.rs`)
Каждый переход §5: `Off→Accessory→Running`; `Running→ShuttingDown{abortable,reason}` для каждого reason; abort
(`Abortable+AccOn→Running`+`ShutdownAborted`); `GraceExpired→Committed`+`Commit`; `Committed` — abort игнорируется;
`Sleep`/`BatteryCutoff` — no-op. Действия проверяются по возвращаемому `Vec<Action>`.
### 9.2 Integration (session-шина, `#[ignore]`, `just test-integration`)
`SetAcc(false)` → наблюдаем `ShutdownImminent`; `AbortShutdown` (в abortable) → `ShutdownAborted`; `SetIgnition`
→ `IgnitionState` property; `GetPowerState` отражает FSM.
### 9.3 E2E (Lima, гибрид — расширение `run.sh`)
- **N=3 in-VM цикла зажигания:** записать `/data`-маркер (Settings `ui.theme=night` + счётчик `/data/state/power-cycles`);
цикл: fake-ACC-off → наблюдать `ShutdownImminent` → харнесс: stop `shturman-stage1.target` + `sync` + `umount /data`
→ `mount /data` → restart → **маркер цел, счётчик++**. После 3 циклов: night + счётчик=3.
- **1 реальный reboot-цикл:** fake-ACC-off → commit → `systemctl poweroff` → `limactl start` → boot → `/data` цел.
- **Abort до PONR:** fake-ACC-off → `ShutdownImminent` → fake-ACC-on **до unmount** → `ShutdownAborted` наблюдаем →
`/data` RW (смонтирован) → `GetPowerState=running`.
- **Power-cut-сим:** во время shutdown (до fsync) `SIGKILL` power+settings → `/data`: remount ok, `fsck -n` clean,
последнее durable-значение присутствует (атомарность §9.1 на уровне файла).
- **Монотоника:** `Uptime` растёт; не прыгает при wall-clock-синке.
- **Watchdog/save-time:** drop-in `RuntimeWatchdogSec` у power.service присутствует; `shturman-savetime.timer` активен;
`/data/state/fake-hwclock.data` обновляется.
### 9.4 Критерии приёмки
- [ ] FSM: все переходы §5 покрыты unit-тестами; `sleep`/`battery_cutoff` — no-op/документированы.
- [ ] `ShutdownImminent` на ACC-off; **abort до PONR → `ShutdownAborted`**; commit только после grace + durable-barrier.
- [ ] **N=3 цикла зажигания — `/data` + счётчик целы** (нет потери).
- [ ] 1 реальный reboot-цикл — `/data` цел.
- [ ] power-cut-сим — `/data` консистентен (`fsck -n` clean, last value present).
- [ ] `Uptime` монотонен; lifecycle-таймеры на `CLOCK_MONOTONIC`.
- [ ] watchdog-конфиг (`RuntimeWatchdogSec`/`RebootWatchdogSec`) на месте; `savetime.timer` активен.
- [ ] **Регресс v0.1/v0.2** (foundation §9.4 + v0.2 §9.3) зелёный (фазы/кадр/персист не сломаны).
- [ ] `just ci` зелёный; красные линии целы (нет CAN/actuator); **prod-build-gate** (`--no-default-features` →
нет `PowerMock1`) зелёный.
---
## 10. Двунаправленные швы (синхронизировать при реализации)
- **`domain B`:** пометить реализованные срезы B01–B07 (v0.3, VM-модель); abort/PONR в VM = stop+umount+remount;
HW (MCU/hold-up/heartbeat/`safe-to-cut`/fail-safe-таймер) + выбор **B08/B09** → **v0.4**.
- **`ipc.md` §3:** Power-сигналы/состояние оживлены из FSM (не mock); `Sleep`/`Wake` зарезервированы.
- **`foundation §5.2`:** «Power-стаб» → **реальный FSM** (обновить формулировку «стартует в running, без логики»).
- **`hardware §3` / `B §5`:** B08/B09 (MCU vs supercap-only) остаётся открытым 🟡 → v0.4.
- **`CLAUDE.md`:** статус v0.3 готово → следующее v0.4/v0.5.
---
## 11. Дальше по ритму
`v0.3` (эта спека) → **writing-plans** → **TDD** (FSM-юниты → сервис-обёртка → durable-barrier/grace → systemd/save-time
→ E2E-блок) → реализация → **verify в Lima** → коммит. Далее: `v0.4` (MCU/thermal — замыкает B08/B09) после v0.3;
`v0.5` (полный shell) параллельно.