860a591f16
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: Alexander <akotenev2003@gmail.com>
209 lines
7.1 KiB
Rust
209 lines
7.1 KiB
Rust
//! 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());
|
||
}
|
||
}
|