diff --git a/docs/specs/v0.4-mcu-thermal.md b/docs/specs/v0.4-mcu-thermal.md new file mode 100644 index 0000000..a5405ac --- /dev/null +++ b/docs/specs/v0.4-mcu-thermal.md @@ -0,0 +1,291 @@ +# Спека реализации: v0.4 — MCU/thermal fail-safe (тепловой триггер + MCU-протокол + fail-safe-таймер) + +> Веха `v0.4` роадмапы: «аппаратный фундамент питания/тепла». Capabilities **A12** (тепловой мониторинг + +> базовый throttling), **B08** (MCU-копилот shutdown-протокол), **B09** (MCU аппаратный fail-safe-таймер), +> **B10** (thermal shutdown: триггер + hysteresis + UX). Поверх **v0.3** (живой `PowerFsm` + graceful shutdown). +> Источники: `docs/domains/b-power-lifecycle.md` §4/§5/§6/§1a, `docs/contracts/hardware.md` §3/§1a, +> `docs/contracts/ipc.md` §3, `docs/contracts/safety.md`, `docs/roadmap.md` §v0.4. Приёмка роадмапы: +> **«thermal-trip → graceful; MCU-таймер режет питание, если SoC завис»**. +> +> **Решение скоупа (брейнсторм):** разрабатываем без платы (принцип #13). MCU-копилот принят как +> **reference-архитектура** (доки рек.); делаем **софт + симуляцию**, физический выбор **B08/B09** +> (MCU vs supercap-only) и реальное железо — отложены в **HW-bring-up-подфазу**. Симметрия с v0.3: +> чистое ядро (политика/кодек) → абстракция (trait) → dev-mock. + +--- + +## 1. Цель и первый артефакт + +Замкнуть **тепловой** и **MCU**-швы домена B поверх живого FSM v0.3: (а) тепловой монитор с гистерезисом +кормит существующий `Event::ThermalTrip` → graceful shutdown; (б) SoC↔MCU shutdown-протокол (heartbeat / +`safe-to-cut` / wait-for-completion) с защищённым кодеком; (в) модель **независимого fail-safe-таймера** — +MCU детерминированно «режет питание», если SoC завис. + +**Первый артефакт (в Lima, мок-MCU/sensor):** +1. **thermal-trip:** `SetTemp ≥ critical` → `ThermalTrip` → `ShutdownImminent(thermal)` → graceful (реюз v0.3-пути, + `/data` консистентен); восстановление по гистерезису. +2. **MCU fail-safe:** `HangSoc` (heartbeat пропал в `running`) → мок-MCU детерминированно снимает питание + (в VM = форс-`off` сервиса) — «MCU режет питание, если SoC завис»; `/data` консистентен. +3. **throttling:** `SetTemp` в warn/throttle-банде → throttle-действие записано (VM `Noop`), без shutdown; + гистерезис на спаде (нет осцилляции). + +**Не цель v0.4:** реальный UART/I2C-драйвер, реальный cpufreq-эффект, прошивка MCU, физический B09-чип/supercap, +полный supercap-only-путь (остаётся абстракцией-fallback), thermal-рендер в Shell (**v0.5**), sleep/wake/ +battery-cutoff (**v1/v2**), числовой тюнинг порогов/hold-up — **RK3588**. Перф/тепловой вердикт — на таргете. + +--- + +## 2. Скоуп и границы + +### 2.1 В скоупе (делаем сейчас) + +- **Тепловая политика (A12/B10):** чистый `ThermalPolicy` — `temp + предыдущий уровень → ThermalLevel + ∈ {Normal, Warn, Throttle(n), Critical}` с **гистерезисом** (раздельные пороги вверх/вниз — нет осцилляции). + Юнит-тестируемый без I/O. Пороги — placeholder-константы (`// тюнинг на RK3588`, hardware §1a). +- **Источник температуры (`TempSource` trait):** real = sysfs `/sys/class/thermal/thermal_zone*/temp` (max по + зонам); VM = `MockTempSource` (значение из dev-D-Bus `SetTemp`). В v0.4 активен mock. +- **Throttler (`Throttler` trait):** real = cpufreq-cap (best-effort, HW); VM = `NoopThrottler` (запись уровня + для E2E/лог). Эффект — абстракция; реальное снижение частоты — HW. +- **Thermal-монитор:** периодический poll на **монотонике** → политика → throttle + на `Critical` кормит + `Event::ThermalTrip` в FSM (тот же `apply_event` из v0.3). Восстановление до PONR → `Event::ThermalCleared` + (abort thermal-shutdown, симметрия с re-power; гейт по `reason == Thermal`). +- **SoC↔MCU протокол (B08):** типы сообщений `SocToMcu`/`McuToSoc`; **чистый кодек** (framing + seq + CRC16 + + **replay/desync-guard**) — закрывает требование «защита линка от replay/мусора/десинка» (B §5, hardware §4). + Байты текут через in-memory-канал (codec исполняется по-настоящему и в integration). +- **Coprocessor (`Coprocessor` trait):** real = `SerialCoprocessor` (UART — **стаб**, HW-подфаза); VM = + `MockCoprocessor` (in-process, кормится через dev-D-Bus). +- **SoC-side `CoprocessorClient`:** heartbeat в `running`; на `ShutdownImminent` → **wait-for-completion** + (расширенный таймаут ≥ shutdown-бюджет, B §6) — не короткий keepalive посреди unmount; `safe-to-cut` после + PONR → немедленный cut. MCU — **fail-safe-авторитет** (SoC не командует cut-на-ходу — B §5). +- **Fail-safe-таймер (B09) — модель:** `MockCoprocessor` моделирует независимый таймер: heartbeat пропал + (`running`) ИЛИ бюджет истёк без `safe-to-cut` → детерминированный cut (в VM = форс-`off` power-сервиса). +- **D-Bus-контракт (ipc §3):** property `ThermalState ∈ {normal, warn, throttle, critical}` + сигнал + `ThermalChanged(state, celsius)`; `ShutdownImminent(thermal)` уже есть. **Контракт сейчас — Shell рисует в v0.5.** +- **Харнесс:** юниты (политика/кодек/клиент/таймер) + integration (session-шина) + E2E-блок v0.4 (§9). + +### 2.2 Явно НЕ в скоупе (отложено, с указателем «куда») + +- **Физический выбор B08/B09** (MCU-копилот vs supercap-only), реальный MCU-чип, прошивка, реальный + hold-up cap/supercap, реальный UART/I2C-драйвер + GPIO ACC-детект — **HW-bring-up-подфаза** (hardware §3). +- **Реальный cpufreq/DDR/GPU-throttling-эффект + числовые тепловые пороги/дератинг** — **RK3588** (hardware §1a). +- **Thermal-UX рендер в Shell** («перегрев»-overlay) — **v0.5** (живой shell; v0.4 даёт только контракт). +- **Supercap-only полный путь** (SoC-таймер + разряд cap, ACC-детект в софте) — остаётся **абстракцией-fallback** + (`Coprocessor` trait), тело — HW-подфаза при выборе supercap. +- **Sleep/wake/scheduled-wake/battery-cutoff** — **v1/v2** (B §7); состояния зарезервированы (как в v0.3). +- **Реальный `/dev/watchdog` арминг + MCU-watchdog-бэкстоп железом** — **HW** (в v0.4 — дисциплина/модель, как v0.3). +- **Перф-вердикт** (время до cut, hold-time, тепловая инерция) — **RK3588** (performance §2). В VM — функционально. + +### 2.3 Частично в скоупе (каркас сейчас, тело — позже) + +- **`Throttler`** — trait + `Noop` (лог уровня); реальный cpufreq — HW. +- **`Coprocessor`** — trait + `MockCoprocessor`; `SerialCoprocessor` (UART) — стаб, HW-подфаза. +- **`TempSource`** — trait + `MockTempSource`; `SysfsTempSource` — каркас (читает зоны, в Lima зоны статичны). +- **MCU-watchdog/линк-fail-safe** — логика клиента + модель таймера; реальный независимый чип — HW. + +### 2.4 Трассируемость ID → статус + +| ID | Веха | Статус в v0.4 | +|----|------|----------------| +| A12 | Тепловой мониторинг + базовый throttling | `ThermalPolicy` + `TempSource`/`Throttler` (VM mock/noop); пороги placeholder, эффект cpufreq — HW | +| B08 | MCU-копилот shutdown-протокол | типы + кодек (CRC/replay/desync) + `CoprocessorClient` (heartbeat/wait/safe-to-cut); транспорт = in-memory (UART — HW) | +| B09 | MCU аппаратный fail-safe-таймер | **модель** в `MockCoprocessor` (hang/budget → детерминированный cut); реальный независимый чип — HW | +| B10 | Thermal shutdown (триггер + hysteresis + UX) | триггер `ThermalTrip` (реюз FSM v0.3) + гистерезис + abort `ThermalCleared`; **UX-рендер — v0.5** (контракт-property/сигнал сейчас) | + +--- + +## 3. Красные линии, безопасность + +- **MCU/Power — только питание устройства, не CAN/actuator (#1/#2):** копроцессор мониторит зажигание/напряжение + и коммутирует **рейл питания SoC** — никаких узлов авто, никаких write/actuator-путей. Протокол SoC↔MCU не + несёт автомобильных команд. (Engine-state/OBD — домен E, read-only; Power **не трогает CAN**.) +- **MCU — fail-safe-авторитет (B §5):** ни одно SoC-сообщение не может (а) снять питание на ходу, (б) держать + вечно и разрядить АКБ. В модели `MockCoprocessor` cut инициируется **только** таймером MCU (hang/budget) или + `safe-to-cut` после PONR — не произвольной SoC-командой. +- **Защита линка:** кодек отбрасывает replay (seq ≤ last)/мусор (битый CRC)/десинк (resync по SYNC) — + юнит-доказано (§9.1). Аналог защиты CAN-линка (hardware §4). +- **Durability-инвариант v0.3 сохраняется:** thermal-trip и MCU-cut идут через тот же graceful-путь до PONR + (durable-barrier `sync` до unmount); после усечённого shutdown `/data` консистентен (atomic-write A §3). +- **prod-build-gate:** `--no-default-features` (без `dev-mocks`) → `PowerMock1`/`SetTemp`/`HangSoc` не + регистрируются (как в v0.3). dev-D-Bus-policy — dev-only drop-in. + +--- + +## 4. Раскладка (новые/изменённые артефакты) + +- **Create** `crates/core/shturman-power/src/thermal.rs` — `ThermalLevel`, `ThermalPolicy` (чистая, гистерезис), + `TempSource` trait (`SysfsTempSource`/`MockTempSource`), `Throttler` trait (`Cpufreq`-стаб/`NoopThrottler`), + `ThermalMonitor` (poll → политика → throttle + `ThermalTrip`/`ThermalCleared`). +- **Create** `crates/core/shturman-power/src/protocol.rs` — `SocToMcu`/`McuToSoc` (типы сообщений). +- **Create** `crates/core/shturman-power/src/codec.rs` — кадр (SYNC/LEN/SEQ/TYPE/PAYLOAD/CRC16) + encode/decode + + replay/desync-guard. Юнит-тесты в файле. +- **Create** `crates/core/shturman-power/src/coprocessor.rs` — `Coprocessor` trait, `MockCoprocessor` (in-process + + B09-таймер-модель), `SerialCoprocessor` (UART-стаб), `CoprocessorClient` (SoC-side: heartbeat/wait/safe-to-cut). +- **Modify** `crates/core/shturman-power/src/fsm.rs` — `Event::ThermalCleared` (+ переход abort из + `ShuttingDown{Abortable, reason: Thermal}` → `Running`); `Event::FailsafeCut` (→ `off` из любого не-`off`, + необратимо — MCU-авторитет); подтвердить армы `ThermalTrip`. +- **Modify** `crates/core/shturman-power/src/service.rs` — владеть `ThermalMonitor` + `CoprocessorClient` (кормят + FSM); property `ThermalState` + сигнал `ThermalChanged`; dev-mock расширить: `SetTemp(d)`, `HangSoc()`, + `McuLinkLoss()`. +- **Modify** `crates/core/shturman-power/src/lib.rs` — `pub mod thermal; pub mod protocol; pub mod codec; pub mod coprocessor;`. +- **Modify** `crates/shturman-ipc/src/proxy.rs` — `Power1Proxy`: property `ThermalState` + сигнал `ThermalChanged`. +- **Modify** `crates/core/shturman-power/tests/integration.rs` — thermal-trip / abort / fail-safe-cut по session-шине. +- **Modify** `tests/e2e/run.sh` — блок v0.4 (thermal-trip → graceful; MCU fail-safe; throttling/гистерезис). +- **Modify (швы §10)** `docs/domains/b-power-lifecycle.md`, `docs/contracts/hardware.md`, `docs/contracts/ipc.md` §3, + `docs/capability-catalog.md` (A12/B08/B09/B10), `CLAUDE.md`. + +--- + +## 5. Тепловая подсистема (A12/B10) — контракт + +``` +ThermalLevel = Normal | Warn | Throttle(level: u8) | Critical +``` + +**Пороги (placeholder-константы, тюнинг на RK3588 — hardware §1a; Tjmax RK3588 ~100 °C):** + +| Переход | Порог вверх | Порог вниз (гистерезис) | +|---------|-------------|--------------------------| +| Normal → Warn | `WARN_C = 75` | `WARN_C − HYST` | +| Warn → Throttle | `THROTTLE_C = 85` | `THROTTLE_C − HYST` | +| Throttle → Critical | `CRITICAL_C = 95` | `CRITICAL_C − HYST` | +| `HYST = 5 °C` | | | + +- **`ThermalPolicy::next(prev: ThermalLevel, temp_c) -> ThermalLevel`** — чистая; гистерезис = переход вниз + только ниже `(порог − HYST)`, иначе уровень держится (нет осцилляции на границе). +- **`ThermalMonitor`** (tokio-интервал на монотонике, ~`POLL_SECS = 1`): `temp = TempSource::read()` → + `lvl = policy.next(prev, temp)`; при смене уровня: `Throttler::apply(lvl)` + `ThermalChanged(state, temp)`; + **на входе в `Critical`** → `apply_event(Event::ThermalTrip)`; **на выходе из `Critical`** (по гистерезису), + если FSM ещё в `ShuttingDown{Abortable, reason: Thermal}` → `apply_event(Event::ThermalCleared)`. +- **`ThermalState` (D-Bus property)** = проекция текущего `ThermalLevel` (`Throttle(_)` → `"throttle"`). + +--- + +## 6. SoC↔MCU протокол (B08) + fail-safe-таймер (B09) — контракт + +### 6.1 Сообщения + +``` +SocToMcu = Heartbeat { seq } // периодический keepalive в running/accessory + | ShutdownImminent { budget } // вход в shutdown → MCU в wait-for-completion (таймаут ≥ budget) + | SafeToCut // после PONR → MCU снимает питание немедленно +McuToSoc = Ack { seq } + | Acc { on } // дебаунснутый ACC (источник зажигания; в VM кормит FSM AccOn/AccOff) + | Voltage { mv } // напряжение бортсети (под under-voltage backstop) + | CutWarning // бюджет почти истёк (диагностика) +``` + +### 6.2 Кодек (`codec.rs`) + +- **Кадр:** `[SYNC=0xA5][LEN u8][SEQ u8][TYPE u8][PAYLOAD…][CRC16-CCITT]`, CRC по `LEN..=PAYLOAD`. +- **Replay/dup guard:** приёмник держит `last_seq` на направление; кадр с `seq ≤ last_seq` (в окне) — **drop**. +- **Desync/мусор:** битый CRC или нет SYNC → **resync** (скан до следующего `SYNC`), кадр отброшен. +- Юнит-тесты: round-trip всех типов; corruption (флип бита → drop); replay (повтор seq → drop); desync + (мусор перед SYNC → восстановление на следующем валидном кадре). + +### 6.3 SoC-side `CoprocessorClient` + +- В `running`/`accessory`: `Heartbeat{seq++}` каждые `HEARTBEAT_SECS` (монотоника); ждёт `Acc`/`Voltage` от MCU + → кормит FSM (`AccOn`/`AccOff`/`UnderVoltage`). +- На `ShutdownImminent` (FSM вошёл в shutdown): шлёт `SocToMcu::ShutdownImminent{budget}` → переходит в + **wait-for-completion** (heartbeat останавливается, ждёт завершения секвенсинга; таймаут ≥ shutdown-бюджет, B §6). +- После PONR (commit): `SafeToCut` → MCU режет немедленно. +- **Не** шлёт power-команд с эффектом cut-на-ходу (red-line §3). + +### 6.4 Fail-safe-таймер (B09) — модель в `MockCoprocessor` + +- **Hang-детект:** нет `Heartbeat` дольше `FAILSAFE_MISS × HEARTBEAT_SECS` в `running` → SoC завис → cut. +- **Budget-таймер:** после `ShutdownImminent` без `SafeToCut` за `HOLDUP_BUDGET_SECS` → cut (детерминированно). +- **Cut (в VM):** мок-MCU зовёт `apply_event(Event::FailsafeCut)` → FSM → `off` (необратимо, MCU-авторитет) + + лог «MCU cut». В E2E дополнительно реюзаем v0.3 power-cut (SIGKILL до fsync) для durability-доказательства + (`fsck` clean, durable-value цел). Это **VM-модель**: реальный зависший SoC теряет питание извне, в модели + cut = событие не-реально-зависшего процесса (симметрично v0.3 «abort/PONR = stop+umount+remount»). +- Значения (`HEARTBEAT_SECS=1`, `FAILSAFE_MISS=3`, `HOLDUP_BUDGET_SECS` ~ grace+запас) — **placeholder** + (реальный hold-up sizing — hardware §3, RK3588). + +--- + +## 7. D-Bus `ru.shturman.Power` — v0.4 расширяет + +- **+ Property `ThermalState: s`** ∈ `{normal, warn, throttle, critical}` (+ `PropertiesChanged`). +- **+ Сигнал `ThermalChanged(s state, i celsius)`** — для Shell (рендер «перегрев» — **v0.5**). +- `ShutdownImminent(u seconds, s reason)` — `reason=thermal` уже объявлен (ipc §3); v0.4 его **реально эмитит** + на thermal-trip. +- **dev-mock `ru.shturman.dev.PowerMock1` (feature `dev-mocks`)** дополняется: + - `SetTemp(i celsius)` → кормит `MockTempSource` → монитор → политика. + - `HangSoc()` → останавливает heartbeat → провоцирует B09-таймер. + - `McuLinkLoss()` → тишина линка: **SoC-сторона деградирует** (лог/маркер degraded, **не** self-cut — red-line §3). + MCU-сторонняя политика cut-vs-hold при тишине — **B §12-open → HW**. + - Прод-гейт `--no-default-features` — не регистрируются (как v0.3 §3). + +--- + +## 8. Watchdog / монотоника (реюз v0.3) + +- Все новые таймеры (poll, heartbeat, wait-for-completion, fail-safe, hold-up) — на **`CLOCK_MONOTONIC`** + (`shturman_common::monotonic_secs`), НЕ wall-clock (B §8). +- MCU-watchdog/линк-fail-safe — **логика клиента + модель** (реальный независимый чип/арминг — HW). systemd + `RuntimeWatchdogSec`/`RebootWatchdogSec` — уже из v0.3 (новых юнитов не требуется; thermal/coprocessor живут + внутри `shturman-power.service`). + +--- + +## 9. Dev-харнесс и план тестирования + +### 9.1 Unit (чистые модули) + +- **`thermal.rs`:** банды Normal/Warn/Throttle/Critical; **гистерезис** (нет осцилляции на границе ±HYST); + `ThermalTrip` на входе в Critical, `ThermalCleared` на выходе. +- **`codec.rs`:** round-trip всех типов; corruption→drop; replay(seq)→drop; desync→resync. +- **`coprocessor.rs`:** клиент heartbeat→wait-for-completion→safe-to-cut; B09-таймер (hang→cut, budget→cut); + MCU игнорит небезопасные SoC-команды (red-line). +- **`fsm.rs`:** `ThermalCleared` abort только из `ShuttingDown{Abortable, reason: Thermal}` (из `AccOff`-shutdown — + no-op); committed — no-op. `FailsafeCut` → `off` из любого не-`off` (необратимо). + +### 9.2 Integration (session-шина, `#[ignore]`, `just test-integration`) + +- `SetTemp ≥ critical` → наблюдаем `ShutdownImminent(thermal)` + `ThermalChanged(critical)`; state `shutting_down`. +- `SetTemp` recovery до grace → `ShutdownAborted` (через `ThermalCleared`) + `running`. +- `HangSoc` → наблюдаем fail-safe-cut (state → off / forced-off). + +### 9.3 E2E (Lima, гибрид — расширение `run.sh`, после блока power-safe v0.3) + +- **thermal-trip:** `SetTemp ≥ critical` → `ShutdownImminent(thermal)` → graceful (реюз v0.3: stop→umount(PONR)→ + remount) → `/data` консистентен; затем `SetTemp` норма → `running`. +- **MCU fail-safe:** `HangSoc` (heartbeat пропал) → мок-MCU режет питание (наблюдаем forced-off / + SIGKILL-эквивалент) → `fsck` clean, durable-value цел (как v0.3 power-cut). +- **throttling/гистерезис:** `SetTemp` в warn/throttle → `ThermalState` меняется, throttle записан, **без** + shutdown; спад чуть выше нижнего порога — уровень держится (нет осцилляции). +- **Регресс v0.1/v0.2/v0.3 зелёный**; machine-id стабилен; `E2E OK ✅`. + +### 9.4 Критерии приёмки (роадмапа + спека) + +- [ ] thermal-trip → graceful (`ShutdownImminent(thermal)`→commit→`/data` консистентен); гистерезис — нет осцилляции. +- [ ] MCU fail-safe-таймер: SoC-hang/бюджет → **детерминированный cut** (модель); `/data` консистентен. +- [ ] Throttling-политика по бандам применена (запись в VM; числа — RK3588). +- [ ] Кодек: replay/desync/corruption отбиты (unit). +- [ ] `ThermalState`/`ThermalChanged` на шине; `Uptime`/таймеры монотонны. +- [ ] Регресс v0.1–v0.3 зелёный; `just ci` зелёный; prod-build-gate (нет `PowerMock1`/`SetTemp`); красные линии целы + (MCU/Power — только питание, нет CAN/actuator). + +--- + +## 10. Двунаправленные швы (синхронизировать при реализации) + +- **`domain B`:** §4 (thermal-trip-путь реализован), §5 (MCU shutdown-протокол + кодек + клиент — софт/модель; + физический MCU/fail-safe-чип/supercap → HW-подфаза), §6 (wait-for-completion реализован), §1a (тепловые пороги — + placeholder, тюнинг RK3588). Пометить **A12/B08/B09/B10** реализованными (VM-модель). +- **`hardware §3`/`§1a`:** **B08/B09** физический выбор (MCU vs supercap-only) остаётся **🟡 → HW-bring-up-подфаза**; + тепловой конверт/класс/охлаждение — 🟡 (числа на таргете). +- **`ipc.md §3`:** Power + `ThermalState`/`ThermalChanged`; `ShutdownImminent(thermal)` реально эмитится. +- **`capability-catalog`:** A12 ✅ (политика+абстракция), B10 ✅ (триггер+гистерезис; UX→v0.5), B08/B09 — софт/модель + реализованы, физический выбор 🟡 → HW. +- **`CLAUDE.md`:** статус v0.4 (после реализации) → следующее v0.5 shell. + +--- + +## 11. Дальше по ритму + +`v0.4` (после утверждения спеки) → **План 8** (`docs/specs/plans/08-v0.4-mcu-thermal.md`, writing-plans) → TDD → +verify в Lima → коммиты `feat/v0.4-mcu-thermal`. Затем **v0.5 — полный shell** (живой weston-shell; замкнёт +thermal-UX-рендер). Физический **HW-bring-up** (MCU/supercap выбор, реальный UART/cpufreq/B09-чип, тепловой +вердикт) — отдельной подфазой при появлении RK3588-платы.