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 кормит входы).
|
//! (`fsm`), сервис оборачивает его (D-Bus state/signals из FSM; dev-mock кормит входы).
|
||||||
|
|
||||||
pub mod codec;
|
pub mod codec;
|
||||||
|
pub mod coprocessor;
|
||||||
pub mod fsm;
|
pub mod fsm;
|
||||||
pub mod protocol;
|
pub mod protocol;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
|
|||||||
Reference in New Issue
Block a user