From 860a591f16cb6cefef744753ecb0ba0c4e8f6cb1 Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 25 Jun 2026 15:31:55 +0300 Subject: [PATCH] =?UTF-8?q?feat(v0.4):=20Coprocessor=20trait=20+=20MockCop?= =?UTF-8?q?rocessor=20(B09-=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C)=20+=20?= =?UTF-8?q?=D0=BA=D0=BB=D0=B8=D0=B5=D0=BD=D1=82=20(B08)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 Signed-off-by: Alexander --- crates/core/shturman-power/src/coprocessor.rs | 208 ++++++++++++++++++ crates/core/shturman-power/src/lib.rs | 1 + 2 files changed, 209 insertions(+) create mode 100644 crates/core/shturman-power/src/coprocessor.rs diff --git a/crates/core/shturman-power/src/coprocessor.rs b/crates/core/shturman-power/src/coprocessor.rs new file mode 100644 index 0000000..43e6919 --- /dev/null +++ b/crates/core/shturman-power/src/coprocessor.rs @@ -0,0 +1,208 @@ +//! SoC↔MCU копроцессор (B08/B09, спека v0.4 §6.3–§6.4). Транспорт байт-уровневый (codec исполняется реально). +//! `MockCoprocessor` моделирует MCU + независимый fail-safe-таймер (B09). Прод `SerialCoprocessor` — стаб (UART → HW). + +use crate::codec::{encode_frame, FrameDecoder}; +use crate::protocol::{McuToSoc, SocToMcu}; +use std::sync::{Arc, Mutex}; + +// Тайминги — placeholder (сек). Тюнинг hold-up — hardware §3 (RK3588). +pub const HEARTBEAT_SECS: u64 = 1; +pub const FAILSAFE_MISS: u64 = 3; // пропущено heartbeat-окон в running → SoC завис +pub const HOLDUP_BUDGET_SECS: u64 = 5; // shutdown без safe-to-cut дольше → cut +pub const BROWNOUT_MV: u16 = 11_000; // under-voltage backstop (placeholder, hardware §3) + +/// Байт-уровневый линк к MCU. `failsafe_due`/`set_now` — модель B09 (прод: реальный MCU в железе → default). +pub trait Coprocessor: Send + Sync { + fn tx(&self, bytes: &[u8]); // SoC → MCU + fn rx(&self) -> Vec; // MCU → SoC (drain) + fn failsafe_due(&self) -> bool { + false + } + fn set_now(&self, _secs: u64) {} +} + +/// SoC-сторона: heartbeat / shutdown-imminent / safe-to-cut + декод входящих MCU→SoC. +pub struct CoprocessorClient { + link: Arc, + seq: u8, + decoder: FrameDecoder, +} + +impl CoprocessorClient { + pub fn new(link: Arc) -> Self { + Self { + link, + seq: 0, + decoder: FrameDecoder::default(), + } + } + fn send(&mut self, msg: SocToMcu) { + self.seq = self.seq.wrapping_add(1); + let f = encode_frame(self.seq, msg.wire_type(), &msg.payload()); + self.link.tx(&f); + } + pub fn heartbeat(&mut self) { + self.send(SocToMcu::Heartbeat); + } + pub fn shutdown_imminent(&mut self, budget: u8) { + self.send(SocToMcu::ShutdownImminent { budget }); + } + pub fn safe_to_cut(&mut self) { + self.send(SocToMcu::SafeToCut); + } + pub fn poll(&mut self) -> Vec { + let bytes = self.link.rx(); + self.decoder + .push(&bytes) + .into_iter() + .filter_map(|f| McuToSoc::from_wire(f.msg_type, &f.payload)) + .collect() + } +} + +#[derive(Default)] +struct MockState { + soc_decoder: FrameDecoder, + mcu_seq: u8, + out: Vec, + last_heartbeat: u64, + shutdown_at: Option, + safe_to_cut: bool, + hung: bool, + now: u64, +} + +/// Мок MCU: декодит SoC-кадры (через реальный codec), моделирует B09-таймер, эмитит MCU→SoC. +#[derive(Clone, Default)] +pub struct MockCoprocessor { + st: Arc>, +} + +impl MockCoprocessor { + pub fn new() -> Self { + Self::default() + } + /// MCU → SoC (dev-mock кормит ACC/voltage отсюда). + pub fn emit(&self, msg: McuToSoc) { + let mut s = self.st.lock().unwrap(); + s.mcu_seq = s.mcu_seq.wrapping_add(1); + let seq = s.mcu_seq; + let f = encode_frame(seq, msg.wire_type(), &msg.payload()); + s.out.extend_from_slice(&f); + } + /// `HangSoc()` — SoC «завис»: heartbeat больше не освежает таймер → сработает B09. + pub fn hang(&self) { + self.st.lock().unwrap().hung = true; + } +} + +impl Coprocessor for MockCoprocessor { + fn tx(&self, bytes: &[u8]) { + let mut s = self.st.lock().unwrap(); + let now = s.now; + for f in s.soc_decoder.push(bytes) { + if let Some(msg) = SocToMcu::from_wire(f.msg_type, &f.payload) { + match msg { + SocToMcu::Heartbeat => { + if !s.hung { + s.last_heartbeat = now; + } + } + SocToMcu::ShutdownImminent { .. } => s.shutdown_at = Some(now), + SocToMcu::SafeToCut => s.safe_to_cut = true, + } + } + } + } + fn rx(&self) -> Vec { + std::mem::take(&mut self.st.lock().unwrap().out) + } + fn failsafe_due(&self) -> bool { + let s = self.st.lock().unwrap(); + match s.shutdown_at { + // running: тишина heartbeat дольше FAILSAFE_MISS окон → SoC завис → cut + None => { + s.last_heartbeat != 0 + && s.now.saturating_sub(s.last_heartbeat) > FAILSAFE_MISS * HEARTBEAT_SECS + } + // shutdown: бюджет истёк без safe-to-cut → cut + Some(t0) => !s.safe_to_cut && s.now.saturating_sub(t0) > HOLDUP_BUDGET_SECS, + } + } + fn set_now(&self, secs: u64) { + self.st.lock().unwrap().now = secs; + } +} + +/// Прод-стаб: реальный UART/I2C — HW-bring-up-подфаза. В v0.4 не активен (прод-источника событий нет). +#[derive(Default)] +pub struct SerialCoprocessor; +impl SerialCoprocessor { + pub fn new() -> Self { + Self + } +} +impl Coprocessor for SerialCoprocessor { + fn tx(&self, _bytes: &[u8]) {} + fn rx(&self) -> Vec { + Vec::new() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn client_heartbeat_decoded_by_mock() { + let mock = Arc::new(MockCoprocessor::new()); + mock.set_now(1); + let mut client = CoprocessorClient::new(mock.clone()); + client.heartbeat(); + // heartbeat освежил таймер → нет failsafe + assert!(!mock.failsafe_due()); + } + + #[test] + fn mcu_to_soc_roundtrip() { + let mock = Arc::new(MockCoprocessor::new()); + let mut client = CoprocessorClient::new(mock.clone()); + mock.emit(McuToSoc::Acc { on: true }); + mock.emit(McuToSoc::Voltage { mv: 13_800 }); + assert_eq!( + client.poll(), + vec![McuToSoc::Acc { on: true }, McuToSoc::Voltage { mv: 13_800 }] + ); + } + + #[test] + fn failsafe_on_soc_hang() { + let mock = Arc::new(MockCoprocessor::new()); + let mut client = CoprocessorClient::new(mock.clone()); + mock.set_now(1); + client.heartbeat(); // last_heartbeat = 1 + mock.hang(); // SoC завис + mock.set_now(2); + client.heartbeat(); // hung → таймер НЕ освежается + assert!(!mock.failsafe_due()); // 2−1=1, ≤ 3 + mock.set_now(5); // 5−1=4 > FAILSAFE_MISS*HEARTBEAT (3) + assert!(mock.failsafe_due()); + } + + #[test] + fn failsafe_on_holdup_budget() { + let mock = Arc::new(MockCoprocessor::new()); + let mut client = CoprocessorClient::new(mock.clone()); + mock.set_now(10); + client.shutdown_imminent(2); // shutdown_at = 10 + mock.set_now(14); + assert!(!mock.failsafe_due()); // 14−10=4 ≤ 5 + mock.set_now(16); // > HOLDUP_BUDGET (5) + assert!(mock.failsafe_due()); + // safe-to-cut снимает failsafe + mock.set_now(11); + client.safe_to_cut(); + mock.set_now(20); + assert!(!mock.failsafe_due()); + } +} diff --git a/crates/core/shturman-power/src/lib.rs b/crates/core/shturman-power/src/lib.rs index 95f560b..e30ce6a 100644 --- a/crates/core/shturman-power/src/lib.rs +++ b/crates/core/shturman-power/src/lib.rs @@ -2,6 +2,7 @@ //! (`fsm`), сервис оборачивает его (D-Bus state/signals из FSM; dev-mock кормит входы). pub mod codec; +pub mod coprocessor; pub mod fsm; pub mod protocol; pub mod service;