use super::bst::BdtpDecoder;
use crate::engine::format::ngt1::{
DLE, EBL_TIMESTAMP, ESC, ETX, FILETIME_TO_UNIX_MS, LF, N2K_MSG_RECEIVED, SOH, STX,
};
use crate::engine::format::timestamp::to_unix_ms;
use crate::engine::frame::RawFrame;
const EBL_VERSION: u8 = 0x01;
const EBL_DESCRIPTION: u8 = 0x02;
const EBL_DIRECTION: u8 = 0x05;
const EBL_BST_RAW_FRAME: u8 = 0x07;
pub const EBL_FORMAT_VERSION: u32 = 1002;
const EBL_TAG_CAPACITY: usize = 1800;
const MAX_TAG: usize = 64 * 1024;
fn push_tag(out: &mut Vec<u8>, tag: u8, data: &[u8]) {
out.push(ESC);
out.push(SOH);
push_header_byte(out, tag);
for &b in data {
push_header_byte(out, b);
}
out.push(ESC);
out.push(LF);
}
fn filetime(unix_ms: u64) -> u64 {
(unix_ms + FILETIME_TO_UNIX_MS).saturating_mul(10_000)
}
pub fn encode_preamble(unix_ms: u64, out: &mut Vec<u8>) {
push_tag(out, EBL_TIMESTAMP, &filetime(unix_ms).to_le_bytes());
push_tag(out, EBL_VERSION, &EBL_FORMAT_VERSION.to_le_bytes());
}
pub fn frame_unix_ms(frame: &RawFrame) -> u64 {
frame
.timestamp
.as_deref()
.and_then(to_unix_ms)
.unwrap_or(0)
.max(0) as u64
}
pub fn encode_frame(frame: &RawFrame, out: &mut Vec<u8>) {
push_tag(
out,
EBL_TIMESTAMP,
&filetime(frame_unix_ms(frame)).to_le_bytes(),
);
let mut payload = Vec::with_capacity(11 + frame.data.len());
payload.push(frame.prio);
payload.push((frame.pgn & 0xff) as u8);
payload.push(((frame.pgn >> 8) & 0xff) as u8);
payload.push(((frame.pgn >> 16) & 0xff) as u8);
payload.push(frame.dst);
payload.push(frame.src);
payload.extend_from_slice(&0u32.to_le_bytes());
payload.push(frame.data.len() as u8);
payload.extend_from_slice(&frame.data);
encode_message(N2K_MSG_RECEIVED, &payload, out);
}
fn push_header_byte(out: &mut Vec<u8>, b: u8) {
out.push(b);
if b == ESC {
out.push(ESC);
}
}
fn push_message_byte(out: &mut Vec<u8>, b: u8) {
out.push(b);
if b == DLE {
out.push(DLE);
} else if b == ESC {
out.push(ESC);
}
}
fn encode_message(cmd: u8, payload: &[u8], out: &mut Vec<u8>) {
out.push(DLE);
out.push(STX);
push_message_byte(out, cmd);
push_message_byte(out, payload.len() as u8);
let mut sum = cmd.wrapping_add(payload.len() as u8);
for &b in payload {
push_message_byte(out, b);
sum = sum.wrapping_add(b);
}
push_message_byte(out, 0u8.wrapping_sub(sum));
out.push(DLE);
out.push(ETX);
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Rx,
Tx,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EblEvent {
Message { bytes: Vec<u8>, raw: bool },
Time(u64),
Version(u32),
Direction(Direction),
Description(String),
Unknown { tag: u8, data: Vec<u8> },
Warning(String),
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum EblState {
#[default]
Stream,
StreamEsc,
Tag,
TagEsc,
}
#[derive(Debug, Default)]
pub struct EblDecoder {
state: EblState,
tag: Vec<u8>,
bdtp: BdtpDecoder,
}
impl EblDecoder {
pub fn new() -> Self {
Self::default()
}
pub fn push_bytes(&mut self, bytes: &[u8], events: &mut Vec<EblEvent>) {
for &b in bytes {
self.push_byte(b, events);
}
}
pub fn finish(&mut self, events: &mut Vec<EblEvent>) {
if matches!(self.state, EblState::Tag | EblState::TagEsc) {
events.push(EblEvent::Warning(
"the file ends inside a metatag".to_string(),
));
}
self.state = EblState::Stream;
self.tag.clear();
}
fn push_byte(&mut self, b: u8, events: &mut Vec<EblEvent>) {
match (self.state, b) {
(EblState::Stream, ESC) => self.state = EblState::StreamEsc,
(EblState::Stream, _) => self.stream(b, events),
(EblState::StreamEsc, ESC) => {
self.state = EblState::Stream;
self.stream(ESC, events);
}
(EblState::StreamEsc, SOH) | (EblState::TagEsc, SOH) => {
if self.state == EblState::TagEsc {
events.push(EblEvent::Warning(
"a metatag starts inside another; the first is dropped".to_string(),
));
}
self.tag.clear();
self.state = EblState::Tag;
}
(EblState::StreamEsc, other) => {
self.state = EblState::Stream;
events.push(EblEvent::Warning(format!(
"ESC {other:#04x} in the stream; skipped"
)));
}
(EblState::Tag, ESC) => self.state = EblState::TagEsc,
(EblState::Tag, _) => {
if self.tag.len() < MAX_TAG {
self.tag.push(b);
}
}
(EblState::TagEsc, ESC) => {
self.state = EblState::Tag;
if self.tag.len() < MAX_TAG {
self.tag.push(ESC);
}
}
(EblState::TagEsc, LF) => {
self.state = EblState::Stream;
let tag = std::mem::take(&mut self.tag);
self.metatag(tag, events);
}
(EblState::TagEsc, other) => {
self.state = EblState::Stream;
self.tag.clear();
events.push(EblEvent::Warning(format!(
"ESC {other:#04x} inside a metatag; the metatag is dropped"
)));
}
}
}
fn stream(&mut self, b: u8, events: &mut Vec<EblEvent>) {
let dropped = self.bdtp.dropped;
if let Some(bytes) = self.bdtp.push_byte(b) {
events.push(EblEvent::Message { bytes, raw: false });
}
if self.bdtp.dropped != dropped {
events.push(EblEvent::Warning(
"a damaged BDTP frame in the stream; skipped".to_string(),
));
}
}
fn metatag(&mut self, tag: Vec<u8>, events: &mut Vec<EblEvent>) {
let Some((&id, data)) = tag.split_first() else {
events.push(EblEvent::Warning("an empty metatag".to_string()));
return;
};
if tag.len() > EBL_TAG_CAPACITY {
events.push(EblEvent::Warning(format!(
"metatag {id:#04x} is {} bytes, more than the {EBL_TAG_CAPACITY} the format allows",
tag.len()
)));
}
let event = match (id, data) {
(EBL_TIMESTAMP, t) if t.len() >= 8 => {
let ticks = u64::from_le_bytes([t[0], t[1], t[2], t[3], t[4], t[5], t[6], t[7]]);
EblEvent::Time((ticks / 10_000).saturating_sub(FILETIME_TO_UNIX_MS))
}
(EBL_VERSION, [a, b, c, d, ..]) => {
EblEvent::Version(u32::from_le_bytes([*a, *b, *c, *d]))
}
(EBL_DIRECTION, [0, ..]) => EblEvent::Direction(Direction::Rx),
(EBL_DIRECTION, [1, ..]) => EblEvent::Direction(Direction::Tx),
(EBL_DESCRIPTION, text) => EblEvent::Description(String::from_utf8_lossy(text).into()),
(EBL_BST_RAW_FRAME, bytes) if bytes.len() >= 2 => EblEvent::Message {
bytes: bytes.to_vec(),
raw: true,
},
(EBL_TIMESTAMP | EBL_VERSION | EBL_DIRECTION | EBL_BST_RAW_FRAME, _) => {
EblEvent::Warning(format!(
"metatag {id:#04x} with bad data {data:02x?}; skipped"
))
}
_ => EblEvent::Unknown {
tag: id,
data: data.to_vec(),
},
};
events.push(event);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::format::bst::to_raw_frame;
fn frame(data: &[u8]) -> RawFrame {
RawFrame {
timestamp: Some("2025-04-25T17:05:21.993Z".into()),
prio: 3,
pgn: 127251,
src: 0x17,
dst: 0xff,
data: data.iter().copied().collect(),
}
}
fn decode(wire: &[u8]) -> Vec<EblEvent> {
let mut d = EblDecoder::new();
let mut events = Vec::new();
d.push_bytes(wire, &mut events);
d.finish(&mut events);
events
}
fn hex(s: &str) -> Vec<u8> {
s.split_whitespace()
.map(|b| u8::from_str_radix(b, 16).unwrap())
.collect()
}
#[test]
fn encode_round_trips() {
let f = frame(&[0x00, 0x10, 0x1b, 0xff, 0x10, 0x1b, 0x25, 0x02]);
let mut wire = Vec::new();
encode_preamble(1_745_600_721_000, &mut wire);
encode_frame(&f, &mut wire);
let events = decode(&wire);
let [
EblEvent::Time(_),
EblEvent::Version(EBL_FORMAT_VERSION),
EblEvent::Time(ms),
EblEvent::Message { bytes, raw: false },
] = &events[..]
else {
panic!("events: {events:?}");
};
assert_eq!(*ms, 1_745_600_721_993);
let back = to_raw_frame(bytes, false).expect("received frame");
assert_eq!(
(back.prio, back.pgn, back.src, back.dst),
(f.prio, f.pgn, f.src, f.dst)
);
assert_eq!(back.data.as_slice(), f.data.as_slice());
}
#[test]
fn encode_without_timestamp_uses_epoch() {
let mut f = frame(&[1, 2, 3]);
f.timestamp = None;
let mut wire = Vec::new();
encode_frame(&f, &mut wire);
assert_eq!(decode(&wire)[0], EblEvent::Time(0));
}
#[test]
fn time_utc_from_a_real_file() {
let wire = hex("1b 01 03 90 e3 c8 3a 04 b6 db 01 1b 0a");
assert_eq!(decode(&wire), [EblEvent::Time(1_745_600_721_993)]);
}
#[test]
fn sdk_classic_capture() {
let wire = hex("1B 01 03 40 1B 1B E1 60 CE 40 DA 01 1B 0A \
1B 01 01 EA 03 00 00 1B 0A \
1B 01 03 80 24 E2 60 CE 40 DA 01 1B 0A \
10 02 93 0C 02 00 EE 01 FF EE 00 00 00 00 01 42 40 10 03");
let events = decode(&wire);
let ticks = |t: u64| (t / 10_000).saturating_sub(FILETIME_TO_UNIX_MS);
assert_eq!(events[0], EblEvent::Time(ticks(0x01DA40CE60E11B40)));
assert_eq!(events[1], EblEvent::Version(1002));
assert_eq!(events[2], EblEvent::Time(ticks(0x01DA40CE60E22480)));
let EblEvent::Message { bytes, raw: false } = &events[3] else {
panic!("{events:?}");
};
let f = to_raw_frame(bytes, false).unwrap();
assert_eq!(
(f.pgn, f.src, f.data.as_slice()),
(126464, 0xee, &[0x42][..])
);
assert_eq!(events.len(), 4);
}
#[test]
fn sdk_wire_trace() {
let wire = hex("1B 01 03 40 1B 1B E1 60 CE 40 DA 01 1B 0A \
1B 01 01 EA 03 00 00 1B 0A \
1B 01 03 80 24 E2 60 CE 40 DA 01 1B 0A \
1B 01 05 01 1B 0A \
10 02 94 02 03 04 F9 10 03 \
1B 01 03 00 30 E5 60 CE 40 DA 01 1B 0A \
1B 01 05 00 1B 0A \
1B 01 07 93 0C 02 00 EE 01 FF EE 00 00 00 00 01 42 1B 0A");
let events = decode(&wire);
assert_eq!(events[3], EblEvent::Direction(Direction::Tx));
assert!(matches!(&events[4], EblEvent::Message { bytes, raw: false } if bytes[0] == 0x94));
assert_eq!(events[6], EblEvent::Direction(Direction::Rx));
let EblEvent::Message { bytes, raw: true } = &events[7] else {
panic!("{events:?}");
};
let f = to_raw_frame(bytes, true).unwrap();
assert_eq!((f.pgn, f.src), (126464, 0xee));
assert_eq!(events.len(), 8);
}
#[test]
fn a_metatag_inside_a_message_leaves_it_whole() {
let f = frame(&[1, 2, 3, 4, 5, 6, 7, 8]);
let mut message = Vec::new();
encode_frame(&f, &mut message);
let start = message.iter().position(|&b| b == DLE).unwrap();
let mut wire = message[..start + 6].to_vec();
push_tag(&mut wire, EBL_TIMESTAMP, &filetime(1000).to_le_bytes());
wire.extend_from_slice(&message[start + 6..]);
let events = decode(&wire);
assert!(events.contains(&EblEvent::Time(1000)), "{events:?}");
let EblEvent::Message { bytes, .. } = events.last().unwrap() else {
panic!("{events:?}");
};
assert_eq!(
to_raw_frame(bytes, false).unwrap().data.as_slice(),
&f.data[..]
);
}
#[test]
fn damage_is_stepped_over() {
let mut f = Vec::new();
encode_frame(&frame(&[9]), &mut f);
let mut wire = vec![ESC, 0x42];
wire.extend(&f);
wire.extend([ESC, SOH, EBL_TIMESTAMP, 1, ESC, 0x42]);
wire.extend(&f);
wire.extend([ESC, SOH, 0x08, 1, 2, ESC, LF, ESC, SOH, ESC, LF]);
wire.extend([ESC, SOH, EBL_DIRECTION, 7, ESC, LF]);
wire.extend(&f);
wire.extend([ESC, SOH, EBL_TIMESTAMP, 1, 2]);
let events = decode(&wire);
let messages = events
.iter()
.filter(|e| matches!(e, EblEvent::Message { .. }))
.count();
assert_eq!(messages, 3, "{events:?}");
assert!(events.contains(&EblEvent::Unknown {
tag: 0x08,
data: vec![1, 2]
}));
let warnings = events
.iter()
.filter(|e| matches!(e, EblEvent::Warning(_)))
.count();
assert_eq!(warnings, 5, "{events:?}");
}
}