//! 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()); } }