use crate::engine::frame::RawFrame;
pub(crate) const DLE: u8 = 0x10;
pub(crate) const STX: u8 = 0x02;
pub(crate) const ETX: u8 = 0x03;
pub(crate) const ESC: u8 = 0x1b;
pub(crate) const SOH: u8 = 0x01;
pub(crate) const LF: u8 = 0x0a;
pub(crate) const FILETIME_TO_UNIX_MS: u64 = 11_644_473_600_000;
pub(crate) const EBL_TIMESTAMP: u8 = 0x03;
pub const N2K_MSG_RECEIVED: u8 = 0x93;
pub const N2K_MSG_SEND: u8 = 0x94;
pub const NGT_MSG_SEND: u8 = 0xA1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NgtMessage {
pub command: u8,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
pub enum NgtError {
#[error("NGT message shorter than 3 bytes (cmd + len + checksum)")]
ShortMessage,
#[error("NGT checksum mismatch")]
BadChecksum,
#[error("NGT declared payload length {declared} does not match received {actual}")]
LengthMismatch { declared: u8, actual: usize },
#[error("NGT escape byte 0x{0:02x} not followed by STX/ETX/DLE")]
BadEscape(u8),
#[error("NGT message longer than {MAX_COLLECT} bytes without an end, discarded")]
TooLong,
}
const MAX_COLLECT: usize = 1024;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EblHeader {
Timestamp(u64),
Unknown { kind: u8, payload: Vec<u8> },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum NgtEvent {
Message(NgtMessage),
Header(EblHeader),
Error(NgtError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Idle,
InFrame,
InHeader,
Escape { prev: PrevState },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PrevState {
Idle,
InFrame,
InHeader,
}
pub struct Ngt1Decoder {
state: State,
buf: Vec<u8>,
ebl: bool,
}
impl Default for Ngt1Decoder {
fn default() -> Self {
Self::new()
}
}
impl Ngt1Decoder {
pub fn new() -> Self {
Self {
state: State::Idle,
buf: Vec::with_capacity(258),
ebl: false,
}
}
pub fn with_ebl() -> Self {
let mut d = Self::new();
d.ebl = true;
d
}
pub fn push_byte(&mut self, b: u8) -> Option<NgtEvent> {
match self.state {
State::Idle => {
if self.is_escape(b) {
self.state = State::Escape {
prev: PrevState::Idle,
};
}
None
}
State::InFrame => {
if self.is_escape(b) {
self.state = State::Escape {
prev: PrevState::InFrame,
};
} else {
return self.collect(b);
}
None
}
State::InHeader => {
if self.ebl && b == ESC {
self.state = State::Escape {
prev: PrevState::InHeader,
};
} else {
return self.collect(b);
}
None
}
State::Escape { prev } => match b {
STX => {
self.buf.clear();
self.state = State::InFrame;
None
}
ETX => {
let r = self.finish_frame();
self.state = State::Idle;
r
}
SOH if self.ebl => {
self.buf.clear();
self.state = State::InHeader;
None
}
LF if self.ebl => {
let r = self.finish_header();
self.state = State::Idle;
r
}
DLE => {
self.state = restore(prev);
if matches!(prev, PrevState::InFrame | PrevState::InHeader) {
return self.collect(DLE);
}
None
}
ESC if self.ebl => {
self.state = restore(prev);
if matches!(prev, PrevState::InFrame | PrevState::InHeader) {
return self.collect(ESC);
}
None
}
other => {
let was_collecting = matches!(prev, PrevState::InFrame | PrevState::InHeader);
self.state = State::Idle;
self.buf.clear();
if was_collecting {
Some(NgtEvent::Error(NgtError::BadEscape(other)))
} else {
None
}
}
},
}
}
fn collect(&mut self, b: u8) -> Option<NgtEvent> {
if self.buf.len() >= MAX_COLLECT {
self.buf.clear();
self.state = State::Idle;
return Some(NgtEvent::Error(NgtError::TooLong));
}
self.buf.push(b);
None
}
fn is_escape(&self, b: u8) -> bool {
b == DLE || (self.ebl && b == ESC)
}
pub fn push_bytes(&mut self, bytes: &[u8]) -> Vec<NgtEvent> {
let mut out = Vec::new();
for &b in bytes {
if let Some(ev) = self.push_byte(b) {
out.push(ev);
}
}
out
}
fn finish_frame(&mut self) -> Option<NgtEvent> {
let raw = std::mem::take(&mut self.buf);
self.state = State::Idle;
if raw.len() < 3 {
return Some(NgtEvent::Error(NgtError::ShortMessage));
}
let cksum: u8 = raw.iter().copied().fold(0u8, u8::wrapping_add);
if cksum != 0 {
return Some(NgtEvent::Error(NgtError::BadChecksum));
}
let command = raw[0];
let declared = raw[1];
let actual_payload_len = raw.len() - 3; if declared as usize != actual_payload_len {
return Some(NgtEvent::Error(NgtError::LengthMismatch {
declared,
actual: actual_payload_len,
}));
}
let payload = raw[2..raw.len() - 1].to_vec();
Some(NgtEvent::Message(NgtMessage { command, payload }))
}
fn finish_header(&mut self) -> Option<NgtEvent> {
let raw = std::mem::take(&mut self.buf);
if raw.is_empty() {
return None;
}
match raw[0] {
EBL_TIMESTAMP if raw.len() >= 9 => {
let ticks = u64::from_le_bytes([
raw[1], raw[2], raw[3], raw[4], raw[5], raw[6], raw[7], raw[8],
]);
let unix_ms = (ticks / 10_000).saturating_sub(FILETIME_TO_UNIX_MS);
Some(NgtEvent::Header(EblHeader::Timestamp(unix_ms)))
}
kind => Some(NgtEvent::Header(EblHeader::Unknown {
kind,
payload: raw[1..].to_vec(),
})),
}
}
}
fn restore(prev: PrevState) -> State {
match prev {
PrevState::Idle => State::Idle,
PrevState::InFrame => State::InFrame,
PrevState::InHeader => State::InHeader,
}
}
pub fn encode_ngt_message(cmd: u8, payload: &[u8], out: &mut Vec<u8>) {
assert!(
payload.len() <= u8::MAX as usize,
"NGT payload exceeds 255 bytes"
);
out.push(DLE);
out.push(STX);
write_stuffed(out, cmd);
write_stuffed(out, payload.len() as u8);
let mut sum = cmd.wrapping_add(payload.len() as u8);
for &b in payload {
write_stuffed(out, b);
sum = sum.wrapping_add(b);
}
let checksum = 0u8.wrapping_sub(sum);
write_stuffed(out, checksum);
out.push(DLE);
out.push(ETX);
}
fn write_stuffed(out: &mut Vec<u8>, b: u8) {
out.push(b);
if b == DLE {
out.push(DLE);
}
}
pub fn encode_n2k_send_payload(frame: &RawFrame) -> Vec<u8> {
let mut out = Vec::with_capacity(6 + frame.data.len());
out.push(frame.prio);
out.push((frame.pgn & 0xff) as u8);
out.push(((frame.pgn >> 8) & 0xff) as u8);
out.push(((frame.pgn >> 16) & 0xff) as u8);
out.push(frame.dst);
out.push(frame.data.len() as u8);
out.extend_from_slice(&frame.data);
out
}
pub fn encode_n2k_send_frame(frame: &RawFrame) -> Vec<u8> {
let payload = encode_n2k_send_payload(frame);
let mut out = Vec::with_capacity(payload.len() + 8);
encode_ngt_message(N2K_MSG_SEND, &payload, &mut out);
out
}
pub fn encode_n2k_received_frame(frame: &RawFrame, timestamp_ms: u32) -> Option<Vec<u8>> {
const HEADER: usize = 11;
if HEADER + frame.data.len() > u8::MAX as usize {
return None;
}
let mut payload = Vec::with_capacity(HEADER + frame.data.len());
payload.push(frame.prio);
payload.extend_from_slice(&frame.pgn.to_le_bytes()[..3]);
payload.push(frame.dst);
payload.push(frame.src);
payload.extend_from_slice(×tamp_ms.to_le_bytes());
payload.push(frame.data.len() as u8);
payload.extend_from_slice(&frame.data);
let mut out = Vec::with_capacity(payload.len() + 8);
encode_ngt_message(N2K_MSG_RECEIVED, &payload, &mut out);
Some(out)
}
pub const NGT_STARTUP_SEQ: [u8; 3] = [0x11, 0x02, 0x00];
pub fn encode_startup_ping() -> Vec<u8> {
let mut out = Vec::with_capacity(NGT_STARTUP_SEQ.len() + 8);
encode_ngt_message(NGT_MSG_SEND, &NGT_STARTUP_SEQ, &mut out);
out
}
impl NgtMessage {
pub fn to_raw_frame(&self) -> Option<RawFrame> {
if self.command != N2K_MSG_RECEIVED {
return None;
}
const HEADER: usize = 11;
if self.payload.len() < HEADER {
return None;
}
let prio = self.payload[0];
let pgn = u32::from(self.payload[1])
| (u32::from(self.payload[2]) << 8)
| (u32::from(self.payload[3]) << 16);
let dst = self.payload[4];
let src = self.payload[5];
let ts_ms = u32::from(self.payload[6])
| (u32::from(self.payload[7]) << 8)
| (u32::from(self.payload[8]) << 16)
| (u32::from(self.payload[9]) << 24);
let dlen = self.payload[10] as usize;
if HEADER + dlen > self.payload.len() {
return None;
}
let data = self.payload[HEADER..HEADER + dlen]
.iter()
.copied()
.collect::<smallvec::SmallVec<[u8; 8]>>();
Some(RawFrame {
timestamp: Some(ts_ms.to_string()),
prio,
pgn,
src,
dst,
data,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_unterminated_message_is_given_up_on() {
let mut d = Ngt1Decoder::new();
let mut junk = vec![DLE, STX];
junk.extend(std::iter::repeat_n(0x55, 10 * MAX_COLLECT));
let events = d.push_bytes(&junk);
assert_eq!(events, [NgtEvent::Error(NgtError::TooLong)]);
assert!(d.buf.len() <= MAX_COLLECT);
let frame = RawFrame::new(None, 2, 130306, 35, 255, [1, 2, 3, 4, 5, 6, 7, 8]);
let ok = d.push_bytes(&encode_n2k_received_frame(&frame, 0).unwrap());
assert!(matches!(&ok[..], [NgtEvent::Message(_)]), "{ok:?}");
}
#[test]
fn received_frame_round_trips() {
let frame = RawFrame::new(None, 2, 130306, 35, 255, [0x10, 0x10, 3, 4, 5, 6, 7, 8]);
let bytes = encode_n2k_received_frame(&frame, 1234).expect("fits");
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&bytes);
let [NgtEvent::Message(msg)] = &events[..] else {
panic!("expected one message, got {events:?}")
};
let back = msg.to_raw_frame().expect("an N2K frame");
assert_eq!(back.timestamp.as_deref(), Some("1234"));
assert_eq!(
(back.prio, back.pgn, back.src, back.dst),
(2, 130306, 35, 255)
);
assert_eq!(back.data, frame.data);
}
#[test]
fn received_frame_longer_than_one_message_is_refused() {
let mut frame = RawFrame::new(None, 6, 126996, 35, 255, [0u8; 8]);
frame.data = std::iter::repeat_n(0xab, 244).collect();
let bytes = encode_n2k_received_frame(&frame, 0).expect("244 bytes fit");
let events = Ngt1Decoder::new().push_bytes(&bytes);
let [NgtEvent::Message(msg)] = &events[..] else {
panic!("expected one message, got {events:?}")
};
assert_eq!(msg.to_raw_frame().expect("a frame").data.len(), 244);
frame.data.push(0xab);
assert_eq!(encode_n2k_received_frame(&frame, 0), None);
}
fn encode_frame(cmd: u8, payload: &[u8]) -> Vec<u8> {
let mut inner = vec![cmd, payload.len() as u8];
inner.extend_from_slice(payload);
let cksum = inner.iter().copied().fold(0u8, u8::wrapping_add);
inner.push(0u8.wrapping_sub(cksum));
let mut out = vec![DLE, STX];
for b in &inner {
out.push(*b);
if *b == DLE {
out.push(DLE);
}
}
out.extend_from_slice(&[DLE, ETX]);
out
}
#[test]
fn decodes_a_simple_frame() {
let frame = encode_frame(0x42, &[0x01, 0x02, 0x03]);
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&frame);
assert_eq!(events.len(), 1);
match &events[0] {
NgtEvent::Message(m) => {
assert_eq!(m.command, 0x42);
assert_eq!(m.payload, vec![0x01, 0x02, 0x03]);
}
other => panic!("expected Message, got {other:?}"),
}
}
#[test]
fn unescapes_dle_in_payload() {
let frame = encode_frame(0xA0, &[0xAA, DLE, 0xBB]);
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&frame);
assert_eq!(events.len(), 1);
match &events[0] {
NgtEvent::Message(m) => assert_eq!(m.payload, vec![0xAA, DLE, 0xBB]),
other => panic!("expected Message, got {other:?}"),
}
}
#[test]
fn rejects_bad_checksum() {
let mut frame = encode_frame(0x42, &[0x01]);
let n = frame.len();
frame[n - 3] = frame[n - 3].wrapping_add(1);
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&frame);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], NgtEvent::Error(NgtError::BadChecksum)));
}
#[test]
fn split_across_pushes() {
let frame = encode_frame(0x42, &[0x01, 0x02]);
let mut d = Ngt1Decoder::new();
let mut total = Vec::new();
for b in &frame {
if let Some(ev) = d.push_byte(*b) {
total.push(ev);
}
}
assert_eq!(total.len(), 1);
match &total[0] {
NgtEvent::Message(m) => assert_eq!(m.command, 0x42),
other => panic!("expected Message, got {other:?}"),
}
}
#[test]
fn ignores_garbage_before_frame() {
let mut bytes = vec![0xFF, 0xAA, 0x10, 0x99]; bytes.extend_from_slice(&encode_frame(0x42, &[0x01]));
bytes.extend_from_slice(&[0x55, 0x66]);
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&bytes);
assert_eq!(events.len(), 1);
assert!(matches!(events[0], NgtEvent::Message(_)));
}
#[test]
fn encoder_round_trips_through_decoder() {
let payload = vec![0x00, 0x10, 0xff, 0x10, 0xab];
let mut wire = Vec::new();
encode_ngt_message(0x42, &payload, &mut wire);
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&wire);
assert_eq!(events.len(), 1);
match &events[0] {
NgtEvent::Message(m) => {
assert_eq!(m.command, 0x42);
assert_eq!(m.payload, payload);
}
other => panic!("expected Message, got {other:?}"),
}
}
#[test]
fn encoder_handles_dle_in_length_and_checksum() {
let payload = vec![0u8; 0x10];
let mut wire = Vec::new();
encode_ngt_message(0x42, &payload, &mut wire);
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&wire);
assert_eq!(events.len(), 1);
match &events[0] {
NgtEvent::Message(m) => {
assert_eq!(m.command, 0x42);
assert_eq!(m.payload.len(), 0x10);
}
other => panic!("got {other:?}"),
}
}
#[test]
fn n2k_send_payload_layout_matches_canboat() {
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn: 60928,
src: 0, dst: 255,
data: smallvec::smallvec![0xfb, 0x9b, 0x70, 0x22, 0x00, 0x9b, 0x50, 0xc0],
};
let payload = encode_n2k_send_payload(&frame);
assert_eq!(payload.len(), 14);
assert_eq!(payload[0], 6);
assert_eq!(payload[1], 0x00);
assert_eq!(payload[2], 0xee);
assert_eq!(payload[3], 0x00);
assert_eq!(payload[4], 0xff);
assert_eq!(payload[5], 8);
assert_eq!(&payload[6..], &frame.data[..]);
}
#[test]
fn startup_ping_decodes_to_ngt_msg_send() {
let bytes = encode_startup_ping();
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&bytes);
assert_eq!(events.len(), 1);
match &events[0] {
NgtEvent::Message(m) => {
assert_eq!(m.command, NGT_MSG_SEND);
assert_eq!(m.payload, NGT_STARTUP_SEQ);
}
other => panic!("got {other:?}"),
}
}
#[test]
fn n2k_msg_received_decodes_to_raw_frame() {
let pgn = 129025u32;
let mut payload = vec![
3, (pgn & 0xff) as u8, ((pgn >> 8) & 0xff) as u8,
((pgn >> 16) & 0xff) as u8,
0xff, 36, 0,
0,
0,
0, 8, ];
payload.extend_from_slice(&[0xe6, 0xf1, 0x3a, 0x80, 0x9c, 0xc6, 0x0d, 0xb3]);
let frame = encode_frame(N2K_MSG_RECEIVED, &payload);
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&frame);
assert_eq!(events.len(), 1);
let m = match &events[0] {
NgtEvent::Message(m) => m,
other => panic!("{other:?}"),
};
let raw = m.to_raw_frame().expect("convert");
assert_eq!(raw.prio, 3);
assert_eq!(raw.pgn, 129025);
assert_eq!(raw.src, 36);
assert_eq!(raw.dst, 0xff);
assert_eq!(
&raw.data[..],
&[0xe6, 0xf1, 0x3a, 0x80, 0x9c, 0xc6, 0x0d, 0xb3]
);
}
#[test]
fn ebl_timestamp_header_decodes() {
let body = [0x90, 0xe3, 0xc8, 0x3a, 0x04, 0xb6, 0xdb, 0x01];
let mut wire = vec![ESC, SOH, EBL_TIMESTAMP];
wire.extend_from_slice(&body);
wire.extend_from_slice(&[ESC, LF]);
let mut d = Ngt1Decoder::with_ebl();
let events = d.push_bytes(&wire);
assert_eq!(events.len(), 1);
match &events[0] {
NgtEvent::Header(EblHeader::Timestamp(ms)) => {
assert_eq!(*ms, 1_745_600_721_993);
}
other => panic!("expected Timestamp, got {other:?}"),
}
}
#[test]
fn esc_in_payload_is_literal_when_not_ebl() {
let payload = vec![0x1b, 0x00, 0x1b, 0xff];
let mut wire = Vec::new();
encode_ngt_message(0x42, &payload, &mut wire);
let mut d = Ngt1Decoder::new();
let events = d.push_bytes(&wire);
assert_eq!(events.len(), 1);
match &events[0] {
NgtEvent::Message(m) => assert_eq!(m.payload, payload),
other => panic!("got {other:?}"),
}
}
#[test]
fn ebl_escapes_esc_within_header() {
let mut wire = vec![ESC, SOH, 0xAA, 0x11, ESC, ESC, 0x22];
wire.extend_from_slice(&[ESC, LF]);
let mut d = Ngt1Decoder::with_ebl();
let events = d.push_bytes(&wire);
assert_eq!(events.len(), 1);
match &events[0] {
NgtEvent::Header(EblHeader::Unknown { kind, payload }) => {
assert_eq!(*kind, 0xAA);
assert_eq!(payload, &vec![0x11, ESC, 0x22]);
}
other => panic!("got {other:?}"),
}
}
#[test]
fn ebl_decodes_two_timestamps_then_n2k_frame() {
let mut wire = vec![
ESC,
SOH,
EBL_TIMESTAMP,
0x90,
0xe3,
0xc8,
0x3a,
0x04,
0xb6,
0xdb,
0x01,
ESC,
LF,
];
wire.extend_from_slice(&[
ESC,
SOH,
EBL_TIMESTAMP,
0xa0,
0xe3,
0xc8,
0x3a,
0x04,
0xb6,
0xdb,
0x01,
ESC,
LF,
]);
wire.extend_from_slice(&encode_frame(N2K_MSG_RECEIVED, &[0u8; 11]));
let mut d = Ngt1Decoder::with_ebl();
let events = d.push_bytes(&wire);
assert_eq!(events.len(), 3);
assert!(matches!(
events[0],
NgtEvent::Header(EblHeader::Timestamp(_))
));
assert!(matches!(
events[1],
NgtEvent::Header(EblHeader::Timestamp(_))
));
assert!(matches!(events[2], NgtEvent::Message(_)));
}
}