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 NgtEvent {
Message(NgtMessage),
Error(NgtError),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum State {
Idle,
InFrame,
Escape { in_frame: bool },
}
pub struct Ngt1Decoder {
state: State,
buf: Vec<u8>,
text: Option<Vec<u8>>,
}
impl Default for Ngt1Decoder {
fn default() -> Self {
Self::new()
}
}
impl Ngt1Decoder {
pub fn new() -> Self {
Self {
state: State::Idle,
buf: Vec::with_capacity(258),
text: None,
}
}
pub fn collect_text(&mut self) {
self.text.get_or_insert_with(Vec::new);
}
pub fn take_text(&mut self) -> Vec<u8> {
self.text.as_mut().map(std::mem::take).unwrap_or_default()
}
pub fn push_byte(&mut self, b: u8) -> Option<NgtEvent> {
match self.state {
State::Idle => {
if b == DLE {
self.state = State::Escape { in_frame: false };
} else if let Some(text) = &mut self.text {
if text.len() >= MAX_COLLECT {
text.remove(0);
}
text.push(b);
}
None
}
State::InFrame => {
if b == DLE {
self.state = State::Escape { in_frame: true };
None
} else {
self.collect(b)
}
}
State::Escape { in_frame } => match b {
STX => {
self.buf.clear();
self.state = State::InFrame;
None
}
ETX => {
let r = self.finish_frame();
self.state = State::Idle;
r
}
DLE if in_frame => {
self.state = State::InFrame;
self.collect(DLE)
}
DLE => {
self.state = State::Idle;
None
}
other => {
self.state = State::Idle;
self.buf.clear();
in_frame.then_some(NgtEvent::Error(NgtError::BadEscape(other)))
}
},
}
}
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
}
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 }))
}
}
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_MSG_RECEIVED: u8 = 0xA0;
pub const BEM_OPERATING_MODE: u8 = 0x11;
pub const BEM_PRODUCT_INFO: u8 = 0x41;
pub const BEM_STARTUP_STATUS: u8 = 0xF0;
pub const BEM_ERROR_REPORT: u8 = 0xF1;
pub const BEM_SYSTEM_STATUS: u8 = 0xF2;
pub const BEM_NEGATIVE_ACK: u8 = 0xF4;
pub fn error_name(code: i32) -> Option<&'static str> {
Some(match code {
-1137 => "BST-BEM message not valid",
-1138 => "model ID unknown",
-1139 => "no definition for the datatype",
-1140 => "bad comms data",
-1152 => "command does not fit the model",
-1153 => "invalid stream",
-1154 => "invalid address",
-1156 => "unexpected datatype",
-1158 => "command timeout",
-1159 => "command data out of range",
-1160 => "command buffer overrun",
-1168 => "invalid checksum",
-1169 => "buffer underflow",
-1170 => "buffer overflow",
-1173 => "invalid baud rate",
-1176 => "port does not exist",
-1177 => "port number out of range",
-1497 => "EEPROM sector error",
-1498 => "malloc/free error",
-1499 => "model ID invalid",
-1995 => "null value",
-1997 => "bad pointer",
-1998 => "null pointer",
_ => return None,
})
}
pub fn describe_error(code: i32) -> String {
match error_name(code) {
Some(name) => format!("{code} ({name})"),
None => code.to_string(),
}
}
pub fn encode_get_product_info() -> Vec<u8> {
let mut out = Vec::with_capacity(8);
encode_ngt_message(NGT_MSG_SEND, &[BEM_PRODUCT_INFO], &mut out);
out
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ProductInfo {
pub nmea2000_version: u16,
pub product_code: u16,
pub model: String,
pub software_version: String,
pub hardware_version: String,
pub serial_number: String,
parts: u8,
}
impl ProductInfo {
pub fn add(&mut self, response: &BemResponse<'_>) -> bool {
let d = response.data;
let u16_at = |i: usize| u16::from_le_bytes([d[i], d[i + 1]]);
match (response.sequence, d.len()) {
(6, 138..) => {
self.nmea2000_version = u16_at(4);
self.product_code = u16_at(6);
self.model = product_string(&d[8..40]);
self.software_version = product_string(&d[40..72]);
self.hardware_version = product_string(&d[72..104]);
self.serial_number = product_string(&d[104..136]);
self.parts = 0b11_1110;
}
(1, 6..) => {
self.nmea2000_version = u16_at(0);
self.product_code = u16_at(2);
self.parts |= 1 << 1;
}
(part @ 2..=5, 32..) => {
let text = product_string(&d[..32]);
match part {
2 => self.model = text,
3 => self.software_version = text,
4 => self.hardware_version = text,
_ => self.serial_number = text,
}
self.parts |= 1 << part;
}
_ => {}
}
self.parts == 0b11_1110
}
pub fn firmware(&self) -> Option<u16> {
self.software_version
.split(|c: char| !c.is_ascii_digit() && c != '.')
.filter_map(|word| {
let (major, minor) = word.split_once('.')?;
let major: u16 = major.parse().ok()?;
if minor.is_empty() || minor.len() > 3 {
return None;
}
let minor: u16 = format!("{minor:0<3}").parse().ok()?;
major.checked_mul(1000)?.checked_add(minor)
})
.next_back()
}
}
fn product_string(bytes: &[u8]) -> String {
let end = bytes
.iter()
.position(|&b| b == 0 || b == 0xff)
.unwrap_or(bytes.len());
String::from_utf8_lossy(&bytes[..end]).trim().to_string()
}
pub fn startup_status(response: &BemResponse<'_>) -> Option<(u16, u32)> {
match response.data {
[v0, v1, r0, r1, r2, r3, ..] => Some((
u16::from_le_bytes([*v0, *v1]),
u32::from_le_bytes([*r0, *r1, *r2, *r3]),
)),
[v0, v1, r0, ..] => Some((u16::from_le_bytes([*v0, *v1]), u32::from(*r0))),
_ => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum OperatingMode {
NgTransferNormal,
NgTransferRxAll,
NgTransferRaw,
NgConvertNormal,
CanPacket,
CanPacketAscii,
Other(u16),
}
impl OperatingMode {
pub fn code(self) -> u16 {
match self {
Self::NgTransferNormal => 1,
Self::NgTransferRxAll => 2,
Self::NgTransferRaw => 3,
Self::NgConvertNormal => 4,
Self::CanPacket => 5,
Self::CanPacketAscii => 6,
Self::Other(code) => code,
}
}
pub fn from_code(code: u16) -> Self {
match code {
1 => Self::NgTransferNormal,
2 => Self::NgTransferRxAll,
3 => Self::NgTransferRaw,
4 => Self::NgConvertNormal,
5 => Self::CanPacket,
6 => Self::CanPacketAscii,
_ => Self::Other(code),
}
}
}
impl std::fmt::Display for OperatingMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NgTransferNormal => f.write_str("NGT Transfer Normal Mode"),
Self::NgTransferRxAll => f.write_str("NGT Transfer Rx All Mode"),
Self::NgTransferRaw => f.write_str("NGT Transfer Raw Mode"),
Self::NgConvertNormal => f.write_str("NGW Convert Normal Mode"),
Self::CanPacket => f.write_str("CAN Packet Mode"),
Self::CanPacketAscii => f.write_str("CAN Packet ASCII Mode"),
Self::Other(code) => write!(f, "operating mode {code}"),
}
}
}
pub fn encode_set_operating_mode(mode: OperatingMode) -> Vec<u8> {
let [lo, hi] = mode.code().to_le_bytes();
let mut out = Vec::with_capacity(12);
encode_ngt_message(NGT_MSG_SEND, &[BEM_OPERATING_MODE, lo, hi], &mut out);
out
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BemResponse<'a> {
pub bem: u8,
pub sequence: u8,
pub model_id: u16,
pub serial: u32,
pub error: i32,
pub data: &'a [u8],
}
impl<'a> BemResponse<'a> {
pub fn parse(payload: &'a [u8]) -> Option<Self> {
if payload.len() < 12 {
return None;
}
Some(Self {
bem: payload[0],
sequence: payload[1],
model_id: u16::from_le_bytes([payload[2], payload[3]]),
serial: u32::from_le_bytes([payload[4], payload[5], payload[6], payload[7]]),
error: i32::from_le_bytes([payload[8], payload[9], payload[10], payload[11]]),
data: &payload[12..],
})
}
pub fn operating_mode(&self) -> Option<OperatingMode> {
match (self.bem, self.data) {
(BEM_OPERATING_MODE, [lo, hi, ..]) => {
Some(OperatingMode::from_code(u16::from_le_bytes([*lo, *hi])))
}
_ => None,
}
}
}
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 firmware_is_the_last_version_in_the_software_version() {
let firmware = |software: &str| {
ProductInfo {
software_version: software.into(),
..Default::default()
}
.firmware()
};
assert_eq!(firmware("1.100, 2.690"), Some(2_690));
assert_eq!(firmware("2.190"), Some(2_190));
assert_eq!(firmware("v2.500"), Some(2_500));
assert_eq!(firmware("2.5"), Some(2_500));
assert_eq!(firmware("Rev B"), None);
assert_eq!(firmware(""), None);
}
#[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 set_operating_mode_matches_the_sdk_example() {
let bytes = encode_set_operating_mode(OperatingMode::NgTransferRxAll);
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, [0x11, 0x02, 0x00]);
}
other => panic!("got {other:?}"),
}
}
#[test]
fn bem_response_reports_the_operating_mode() {
let payload = [
0x11, 0x05, 0x0e, 0x00, 0x78, 0x56, 0x34, 0x12, 0, 0, 0, 0, 0x02, 0x00,
];
let r = BemResponse::parse(&payload).unwrap();
assert_eq!(r.bem, BEM_OPERATING_MODE);
assert_eq!(r.sequence, 5);
assert_eq!(r.model_id, 0x000e);
assert_eq!(r.serial, 0x1234_5678);
assert_eq!(r.error, 0);
assert_eq!(r.operating_mode(), Some(OperatingMode::NgTransferRxAll));
assert_eq!(
r.operating_mode().unwrap().to_string(),
"NGT Transfer Rx All Mode"
);
assert!(BemResponse::parse(&payload[..11]).is_none());
}
#[test]
fn operating_mode_codes_round_trip() {
for code in 0..=8 {
assert_eq!(OperatingMode::from_code(code).code(), code);
}
assert_eq!(OperatingMode::from_code(512), OperatingMode::Other(512));
}
#[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 esc_in_payload_is_literal() {
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:?}"),
}
}
}