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;
pub fn encode_frame(frame: &RawFrame, out: &mut Vec<u8>) {
let unix_ms = frame
.timestamp
.as_deref()
.and_then(to_unix_ms)
.unwrap_or(0)
.max(0) as u64;
let ticks = (unix_ms + FILETIME_TO_UNIX_MS).saturating_mul(10_000);
out.push(ESC);
out.push(SOH);
push_header_byte(out, EBL_TIMESTAMP);
for &b in &ticks.to_le_bytes() {
push_header_byte(out, b);
}
out.push(ESC);
out.push(LF);
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);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::engine::format::ngt1::{EblHeader, Ngt1Decoder, NgtEvent};
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(),
}
}
#[test]
fn encode_round_trips_through_with_ebl_decoder() {
let f = frame(&[0x00, 0x10, 0x1b, 0xff, 0x10, 0x1b, 0x25, 0x02]);
let mut wire = Vec::new();
encode_frame(&f, &mut wire);
let events = Ngt1Decoder::with_ebl().push_bytes(&wire);
assert_eq!(events.len(), 2, "events: {events:?}");
match &events[0] {
NgtEvent::Header(EblHeader::Timestamp(ms)) => assert_eq!(*ms, 1_745_600_721_993),
other => panic!("expected timestamp header, got {other:?}"),
}
match &events[1] {
NgtEvent::Message(m) => {
let back = m.to_raw_frame().expect("received frame");
assert_eq!(back.prio, f.prio);
assert_eq!(back.pgn, f.pgn);
assert_eq!(back.src, f.src);
assert_eq!(back.dst, f.dst);
assert_eq!(back.data.as_slice(), f.data.as_slice());
}
other => panic!("expected message, got {other:?}"),
}
}
#[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);
let events = Ngt1Decoder::with_ebl().push_bytes(&wire);
match &events[0] {
NgtEvent::Header(EblHeader::Timestamp(ms)) => assert_eq!(*ms, 0),
other => panic!("got {other:?}"),
}
}
}