feat(v0.4): SoC↔MCU протокол + кодек (CRC/replay/desync-guard, 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,150 @@
|
|||||||
|
//! Кадр SoC↔MCU + защита линка (B08, спека v0.4 §6.2). Кадр: [SYNC][LEN][SEQ][TYPE][PAYLOAD..][CRC16].
|
||||||
|
//! CRC16-CCITT по LEN..=PAYLOAD. Декодер: resync по SYNC, drop при битом CRC, drop-replay (seq==last).
|
||||||
|
|
||||||
|
pub const SYNC: u8 = 0xA5;
|
||||||
|
|
||||||
|
pub fn crc16_ccitt(data: &[u8]) -> u16 {
|
||||||
|
let mut crc: u16 = 0xFFFF;
|
||||||
|
for &b in data {
|
||||||
|
crc ^= (b as u16) << 8;
|
||||||
|
for _ in 0..8 {
|
||||||
|
crc = if crc & 0x8000 != 0 {
|
||||||
|
(crc << 1) ^ 0x1021
|
||||||
|
} else {
|
||||||
|
crc << 1
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crc
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn encode_frame(seq: u8, msg_type: u8, payload: &[u8]) -> Vec<u8> {
|
||||||
|
let mut body = Vec::with_capacity(3 + payload.len());
|
||||||
|
body.push(payload.len() as u8); // LEN
|
||||||
|
body.push(seq); // SEQ
|
||||||
|
body.push(msg_type); // TYPE
|
||||||
|
body.extend_from_slice(payload);
|
||||||
|
let crc = crc16_ccitt(&body);
|
||||||
|
let mut frame = Vec::with_capacity(1 + body.len() + 2);
|
||||||
|
frame.push(SYNC);
|
||||||
|
frame.extend_from_slice(&body);
|
||||||
|
frame.push((crc >> 8) as u8);
|
||||||
|
frame.push((crc & 0xff) as u8);
|
||||||
|
frame
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct DecodedFrame {
|
||||||
|
pub seq: u8,
|
||||||
|
pub msg_type: u8,
|
||||||
|
pub payload: Vec<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Потоковый декодер: накапливает байты, выдаёт валидные кадры. Resync/replay-guard внутри.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct FrameDecoder {
|
||||||
|
buf: Vec<u8>,
|
||||||
|
last_seq: Option<u8>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FrameDecoder {
|
||||||
|
pub fn push(&mut self, bytes: &[u8]) -> Vec<DecodedFrame> {
|
||||||
|
self.buf.extend_from_slice(bytes);
|
||||||
|
let mut out = Vec::new();
|
||||||
|
loop {
|
||||||
|
// resync: отбросить мусор до SYNC
|
||||||
|
while !self.buf.is_empty() && self.buf[0] != SYNC {
|
||||||
|
self.buf.remove(0);
|
||||||
|
}
|
||||||
|
if self.buf.len() < 4 {
|
||||||
|
break; // нужно минимум SYNC+LEN+SEQ+TYPE
|
||||||
|
}
|
||||||
|
let len = self.buf[1] as usize;
|
||||||
|
let frame_len = 1 + 3 + len + 2;
|
||||||
|
if self.buf.len() < frame_len {
|
||||||
|
break; // кадр ещё не дочитан
|
||||||
|
}
|
||||||
|
let body = &self.buf[1..1 + 3 + len];
|
||||||
|
let crc_rx = ((self.buf[1 + 3 + len] as u16) << 8) | self.buf[1 + 3 + len + 1] as u16;
|
||||||
|
if crc_rx != crc16_ccitt(body) {
|
||||||
|
self.buf.remove(0); // битый CRC → сдвиг, resync
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let seq = self.buf[2];
|
||||||
|
let msg_type = self.buf[3];
|
||||||
|
let payload = self.buf[4..4 + len].to_vec();
|
||||||
|
self.buf.drain(0..frame_len);
|
||||||
|
let replay = matches!(self.last_seq, Some(l) if l == seq);
|
||||||
|
self.last_seq = Some(seq);
|
||||||
|
if !replay {
|
||||||
|
out.push(DecodedFrame {
|
||||||
|
seq,
|
||||||
|
msg_type,
|
||||||
|
payload,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn roundtrip() {
|
||||||
|
let f = encode_frame(7, 0x02, &[42]);
|
||||||
|
let mut d = FrameDecoder::default();
|
||||||
|
let out = d.push(&f);
|
||||||
|
assert_eq!(
|
||||||
|
out,
|
||||||
|
vec![DecodedFrame {
|
||||||
|
seq: 7,
|
||||||
|
msg_type: 0x02,
|
||||||
|
payload: vec![42]
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn corruption_dropped_then_resyncs() {
|
||||||
|
let mut d = FrameDecoder::default();
|
||||||
|
let mut f = encode_frame(1, 0x01, &[]);
|
||||||
|
f[4] ^= 0xFF; // флип в CRC/payload-зоне → битый CRC
|
||||||
|
assert_eq!(d.push(&f), vec![]); // отброшен
|
||||||
|
let g = encode_frame(2, 0x01, &[]);
|
||||||
|
assert_eq!(
|
||||||
|
d.push(&g),
|
||||||
|
vec![DecodedFrame {
|
||||||
|
seq: 2,
|
||||||
|
msg_type: 0x01,
|
||||||
|
payload: vec![]
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn replay_dropped() {
|
||||||
|
let mut d = FrameDecoder::default();
|
||||||
|
let f = encode_frame(5, 0x01, &[]);
|
||||||
|
assert_eq!(d.push(&f).len(), 1);
|
||||||
|
assert_eq!(d.push(&f), vec![]); // тот же seq → replay drop
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn desync_garbage_before_sync() {
|
||||||
|
let mut d = FrameDecoder::default();
|
||||||
|
let mut stream = vec![0x00, 0xFF, 0x13]; // мусор
|
||||||
|
stream.extend_from_slice(&encode_frame(9, 0x83, &[0x2B, 0x67]));
|
||||||
|
let out = d.push(&stream);
|
||||||
|
assert_eq!(
|
||||||
|
out,
|
||||||
|
vec![DecodedFrame {
|
||||||
|
seq: 9,
|
||||||
|
msg_type: 0x83,
|
||||||
|
payload: vec![0x2B, 0x67]
|
||||||
|
}]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
//! `ru.shturman.Power1` — питание/жизненный цикл (домен B). v0.3: реальный lifecycle-FSM
|
//! `ru.shturman.Power1` — питание/жизненный цикл (домен B). v0.3: реальный lifecycle-FSM
|
||||||
//! (`fsm`), сервис оборачивает его (D-Bus state/signals из FSM; dev-mock кормит входы).
|
//! (`fsm`), сервис оборачивает его (D-Bus state/signals из FSM; dev-mock кормит входы).
|
||||||
|
|
||||||
|
pub mod codec;
|
||||||
pub mod fsm;
|
pub mod fsm;
|
||||||
|
pub mod protocol;
|
||||||
pub mod service;
|
pub mod service;
|
||||||
pub mod thermal;
|
pub mod thermal;
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
//! Типы сообщений SoC↔MCU (B08, спека v0.4 §6.1). seq — поле кадра (codec), не сообщения.
|
||||||
|
|
||||||
|
pub mod wire {
|
||||||
|
pub const HEARTBEAT: u8 = 0x01;
|
||||||
|
pub const SHUTDOWN_IMMINENT: u8 = 0x02;
|
||||||
|
pub const SAFE_TO_CUT: u8 = 0x03;
|
||||||
|
pub const ACK: u8 = 0x81;
|
||||||
|
pub const ACC: u8 = 0x82;
|
||||||
|
pub const VOLTAGE: u8 = 0x83;
|
||||||
|
pub const CUT_WARNING: u8 = 0x84;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum SocToMcu {
|
||||||
|
Heartbeat,
|
||||||
|
ShutdownImminent { budget: u8 },
|
||||||
|
SafeToCut,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum McuToSoc {
|
||||||
|
Ack,
|
||||||
|
Acc { on: bool },
|
||||||
|
Voltage { mv: u16 },
|
||||||
|
CutWarning,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SocToMcu {
|
||||||
|
pub fn wire_type(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
SocToMcu::Heartbeat => wire::HEARTBEAT,
|
||||||
|
SocToMcu::ShutdownImminent { .. } => wire::SHUTDOWN_IMMINENT,
|
||||||
|
SocToMcu::SafeToCut => wire::SAFE_TO_CUT,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn payload(&self) -> Vec<u8> {
|
||||||
|
match self {
|
||||||
|
SocToMcu::ShutdownImminent { budget } => vec![*budget],
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_wire(t: u8, p: &[u8]) -> Option<Self> {
|
||||||
|
match t {
|
||||||
|
wire::HEARTBEAT => Some(SocToMcu::Heartbeat),
|
||||||
|
wire::SHUTDOWN_IMMINENT => Some(SocToMcu::ShutdownImminent { budget: *p.first()? }),
|
||||||
|
wire::SAFE_TO_CUT => Some(SocToMcu::SafeToCut),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl McuToSoc {
|
||||||
|
pub fn wire_type(&self) -> u8 {
|
||||||
|
match self {
|
||||||
|
McuToSoc::Ack => wire::ACK,
|
||||||
|
McuToSoc::Acc { .. } => wire::ACC,
|
||||||
|
McuToSoc::Voltage { .. } => wire::VOLTAGE,
|
||||||
|
McuToSoc::CutWarning => wire::CUT_WARNING,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn payload(&self) -> Vec<u8> {
|
||||||
|
match self {
|
||||||
|
McuToSoc::Acc { on } => vec![*on as u8],
|
||||||
|
McuToSoc::Voltage { mv } => mv.to_be_bytes().to_vec(),
|
||||||
|
_ => vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pub fn from_wire(t: u8, p: &[u8]) -> Option<Self> {
|
||||||
|
match t {
|
||||||
|
wire::ACK => Some(McuToSoc::Ack),
|
||||||
|
wire::ACC => Some(McuToSoc::Acc { on: *p.first()? != 0 }),
|
||||||
|
wire::VOLTAGE => Some(McuToSoc::Voltage {
|
||||||
|
mv: u16::from_be_bytes([*p.first()?, *p.get(1)?]),
|
||||||
|
}),
|
||||||
|
wire::CUT_WARNING => Some(McuToSoc::CutWarning),
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user