use smallvec::SmallVec;
use crate::engine::frame::{FASTPACKET_MAX_SIZE, RawFrame};
pub const FRAME_SYNC: u8 = 0xA5;
pub const F1_SYNC_BIT: u8 = 0x80;
pub const TX_SET_MODE_BINARY_MSG: &str = "SET_MODE\tBINARY";
pub const RX_CONNECTED: &str = "CONNECTED";
pub const RX_NO: &str = "NO";
pub const RX_SERVER_VERSION: &str = "SERVER_VERSION";
pub const RX_INSTANCE_DATA: &str = "INSTANCE_DATA";
pub const RX_LICENSES_USED: &str = "LICENSES_USED";
pub const RX_DETAILED_LICENSES_USED: &str = "DETAILED_LICENSES_USED";
pub const VIDEO_PREFIX: u8 = 0x33; pub const ASCII_PREFIX: u8 = 0x32;
#[derive(Debug)]
pub enum ParseOutcome {
Frame {
frame: MaretronFrame,
consumed: usize,
},
Text { line: String, consumed: usize },
NeedMore,
Drop { consumed: usize },
}
#[derive(Debug, Clone)]
pub struct MaretronFrame {
pub pgn: u32,
pub prio: u8,
pub src: u8,
pub dst: u8,
#[allow(dead_code)]
pub msg_type: u8,
pub payload: Vec<u8>,
}
impl MaretronFrame {
pub fn to_raw(&self, timestamp: Option<String>) -> RawFrame {
let mut data: SmallVec<[u8; 8]> =
SmallVec::with_capacity(self.payload.len().min(FASTPACKET_MAX_SIZE));
let take = self.payload.len().min(FASTPACKET_MAX_SIZE);
data.extend_from_slice(&self.payload[..take]);
RawFrame {
timestamp,
prio: self.prio,
pgn: self.pgn,
src: self.src,
dst: self.dst,
data,
}
}
}
pub fn parse(buf: &[u8], state: SessionState) -> ParseOutcome {
if buf.is_empty() {
return ParseOutcome::NeedMore;
}
let first = buf[0];
if state == SessionState::AwaitHandshake {
return parse_nul_terminated_text(buf);
}
if first == FRAME_SYNC {
return parse_binary_frame(buf);
}
if first == VIDEO_PREFIX || first == ASCII_PREFIX {
return match buf.iter().position(|&b| b == 0) {
Some(idx) => ParseOutcome::Drop { consumed: idx + 1 },
None => ParseOutcome::NeedMore,
};
}
if first & 0x80 == 0 {
return parse_nul_terminated_text(buf);
}
ParseOutcome::Drop { consumed: 1 }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SessionState {
AwaitHandshake,
Streaming,
}
fn parse_nul_terminated_text(buf: &[u8]) -> ParseOutcome {
let Some(nul_idx) = buf.iter().position(|&b| b == 0) else {
return ParseOutcome::NeedMore;
};
let line = String::from_utf8_lossy(&buf[..nul_idx]).into_owned();
ParseOutcome::Text {
line,
consumed: nul_idx + 1,
}
}
fn parse_binary_frame(buf: &[u8]) -> ParseOutcome {
if buf.len() < 6 {
return ParseOutcome::NeedMore;
}
let f1 = buf[1];
if f1 & F1_SYNC_BIT == 0 {
return ParseOutcome::Drop { consumed: 1 };
}
let pf = buf[2];
let ps = buf[3];
let sa = buf[4];
let prio = (f1 >> 4) & 0x07;
let msg_type = (f1 >> 1) & 0x03;
let dp = f1 & 0x01;
let (payload_start, payload_len) = if msg_type == 3 {
if buf.len() < 7 {
return ParseOutcome::NeedMore;
}
let len = (buf[5] as u16) | ((buf[6] as u16) << 8);
(7usize, len as usize)
} else {
(6usize, buf[5] as usize)
};
let total = payload_start + payload_len;
if buf.len() < total {
return ParseOutcome::NeedMore;
}
let (pgn, dst) = if pf < 0xF0 {
((dp as u32) << 16 | (pf as u32) << 8, ps)
} else {
((dp as u32) << 16 | (pf as u32) << 8 | ps as u32, 0xFFu8)
};
let payload = buf[payload_start..total].to_vec();
ParseOutcome::Frame {
frame: MaretronFrame {
pgn,
prio,
src: sa,
dst,
msg_type,
payload,
},
consumed: total,
}
}
pub fn build_frame(pgn: u32, prio: u8, dst: u8, payload: &[u8]) -> Option<Vec<u8>> {
if payload.len() > FASTPACKET_MAX_SIZE {
return None;
}
let msg_type: u8 = if payload.len() <= 8 { 1 } else { 2 };
let dp = ((pgn >> 16) & 0x01) as u8;
let pf = ((pgn >> 8) & 0xFF) as u8;
let ps = if pf < 0xF0 { dst } else { (pgn & 0xFF) as u8 };
let mut out = Vec::with_capacity(6 + payload.len());
out.push(FRAME_SYNC);
out.push(F1_SYNC_BIT | ((prio & 0x07) << 4) | ((msg_type & 0x03) << 1) | (dp & 0x01));
out.push(pf);
out.push(ps);
out.push(0xFF);
out.push(payload.len() as u8);
out.extend_from_slice(payload);
Some(out)
}
pub fn build_connect(password: &str) -> Vec<u8> {
let mut s = format!("CONNECT\t\"{}\"\t\tMOBILE", password).into_bytes();
s.push(0);
s
}
pub fn build_set_mode_binary() -> Vec<u8> {
let mut s = TX_SET_MODE_BINARY_MSG.as_bytes().to_vec();
s.push(0);
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_single_frame() {
let buf = [
0xA5, 0b11000100, 0xFD, 0x02, 0x16, 0x04, 0xAA, 0xBB, 0xCC, 0xDD,
];
let out = parse(&buf, SessionState::Streaming);
match out {
ParseOutcome::Frame { frame, consumed } => {
assert_eq!(consumed, 10);
assert_eq!(frame.pgn, 0xFD02);
assert_eq!(frame.prio, 4);
assert_eq!(frame.src, 0x16);
assert_eq!(frame.dst, 0xFF);
assert_eq!(frame.payload, vec![0xAA, 0xBB, 0xCC, 0xDD]);
}
other => panic!("expected Frame, got {other:?}"),
}
}
#[test]
fn need_more_on_partial_frame() {
let buf = [0xA5, 0xC4, 0xFD, 0x02, 0x16, 0x04, 0xAA, 0xBB];
assert!(matches!(
parse(&buf, SessionState::Streaming),
ParseOutcome::NeedMore
));
}
#[test]
fn handshake_message_is_text() {
let buf = b"CONNECTED\tABC123\0extra";
let out = parse(buf, SessionState::AwaitHandshake);
match out {
ParseOutcome::Text { line, consumed } => {
assert_eq!(line, "CONNECTED\tABC123");
assert_eq!(consumed, 17);
}
other => panic!("expected Text, got {other:?}"),
}
}
#[test]
fn build_then_parse_roundtrip() {
let frame = build_frame(0xFD02, 3, 0xFF, &[1, 2, 3, 4]).unwrap();
match parse(&frame, SessionState::Streaming) {
ParseOutcome::Frame { frame, .. } => {
assert_eq!(frame.pgn, 0xFD02);
assert_eq!(frame.prio, 3);
assert_eq!(frame.payload, vec![1, 2, 3, 4]);
assert_eq!(frame.msg_type, 1);
}
o => panic!("expected Frame, got {o:?}"),
}
}
}