feat(v0.4): Coprocessor trait + MockCoprocessor (B09-модель) + клиент (B08)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
This commit is contained in:
@@ -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<u8>; // 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<dyn Coprocessor>,
|
||||
seq: u8,
|
||||
decoder: FrameDecoder,
|
||||
}
|
||||
|
||||
impl CoprocessorClient {
|
||||
pub fn new(link: Arc<dyn Coprocessor>) -> 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<McuToSoc> {
|
||||
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<u8>,
|
||||
last_heartbeat: u64,
|
||||
shutdown_at: Option<u64>,
|
||||
safe_to_cut: bool,
|
||||
hung: bool,
|
||||
now: u64,
|
||||
}
|
||||
|
||||
/// Мок MCU: декодит SoC-кадры (через реальный codec), моделирует B09-таймер, эмитит MCU→SoC.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MockCoprocessor {
|
||||
st: Arc<Mutex<MockState>>,
|
||||
}
|
||||
|
||||
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<u8> {
|
||||
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<u8> {
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user