use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::engine::RawFrame;
use crate::engine::format::aos::{self, Answers, AosInfo};
use crate::engine::format::ikonvert::{NetworkStatus, build_network_status};
use crate::engine::format::ngt1::{
BEM_ERROR_REPORT, BEM_NEGATIVE_ACK, BEM_OPERATING_MODE, BEM_PRODUCT_INFO, BEM_STARTUP_STATUS,
BEM_SYSTEM_STATUS, BemResponse, Ngt1Decoder, NgtEvent, OperatingMode, ProductInfo,
describe_error, encode_get_product_info, encode_n2k_send_frame, encode_set_operating_mode,
startup_status,
};
use crate::engine::pgn_list::{self, PgnListStatus, PgnListSupport, PgnLists, TxGate};
use super::ngt1_tx_list::{NGT_MSG_RECEIVED, TxListRecord, TxListSync};
use super::{Codec, Event, Refused, SYNTHETIC_PGN_START};
use crate::engine::format_iso_ms;
#[deprecated(note = "the NGT-1 codec sends no keepalive; `Ngt1::keepalive` returns `None`")]
pub const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(20);
const ACTISENSE_SYSTEM_STATUS_PGN: u32 = ACTISENSE_BEM_PGN + BEM_SYSTEM_STATUS as u32;
const ACTISENSE_BEM_PGN: u32 = SYNTHETIC_PGN_START;
const NETWORK_STATUS_INTERVAL_MS: u64 = 5_000;
pub const PROBE_AFTER_MS: u64 = 2_000;
const PROBE_ANSWER_MS: u64 = 3_000;
#[derive(Debug, Clone)]
pub struct Config {
pub operating_mode: OperatingMode,
pub pgn_lists: PgnLists,
pub extra_tx_pgns: Vec<u32>,
pub tx_list_record: Arc<Mutex<TxListRecord>>,
}
impl Default for Config {
fn default() -> Self {
Self {
operating_mode: OperatingMode::NgTransferRxAll,
pgn_lists: PgnLists::default(),
extra_tx_pgns: Vec::new(),
tx_list_record: Arc::default(),
}
}
}
fn wanted_tx_pgns(named: &[u32], extra: &[u32]) -> (Vec<u32>, Vec<u32>) {
if named.is_empty() {
return (Vec::new(), Vec::new());
}
pgn_list::merge(&[], &[named, extra].concat())
}
pub fn pgn_list_status(named: &PgnLists, extra: &[u32]) -> PgnListStatus {
PgnListStatus {
tx: if named.tx.is_empty() {
PgnListSupport::Untouched
} else {
PgnListSupport::Pushed
},
rx: if named.rx.is_empty() {
PgnListSupport::Untouched
} else {
PgnListSupport::Unsupported
},
dropped: wanted_tx_pgns(&named.tx, extra).1,
}
}
pub struct Ngt1 {
inner: Ngt1Decoder,
net: NetworkStatusState,
tx_list: TxListSync,
gate: Option<TxGate>,
mode: OperatingMode,
reported_mode: Option<OperatingMode>,
product_info: ProductInfo,
bem_seen: bool,
probe: Probe,
answers: Answers,
aos: Option<AosInfo>,
settle: Settle,
held: Vec<Vec<u8>>,
warned: Vec<u32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Probe {
NotYet,
Sent { at: u64 },
Done,
}
const MODE_SETTLE_MS: u64 = 500;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Settle {
Settled,
Opened,
Until(u64),
}
struct NetworkStatusState {
start_ms: Option<u64>,
next_ms: u64,
seen: [bool; 256],
load_pct: Option<u8>,
errors: Option<u32>,
}
impl Ngt1 {
pub fn new(config: Config) -> Self {
let (tx_pgns, dropped) = wanted_tx_pgns(&config.pgn_lists.tx, &config.extra_tx_pgns);
if !dropped.is_empty() {
log::warn!(
"ngt1: not enabling transmit PGNs {dropped:?} (invalid, or the list is full)"
);
}
Self {
inner: Ngt1Decoder::new(),
net: NetworkStatusState {
start_ms: None,
next_ms: 0,
seen: [false; 256],
load_pct: None,
errors: None,
},
tx_list: TxListSync::new(tx_pgns, config.tx_list_record.clone()),
gate: TxGate::new("ngt1", &config.pgn_lists.tx, &config.extra_tx_pgns),
mode: config.operating_mode,
reported_mode: None,
product_info: ProductInfo::default(),
bem_seen: false,
probe: Probe::NotYet,
answers: Answers::default(),
aos: None,
settle: Settle::Settled,
held: Vec::new(),
warned: Vec::new(),
}
}
pub fn aos(&self) -> Option<&AosInfo> {
self.aos.as_ref()
}
pub fn answers_bem(&self) -> bool {
self.bem_seen
}
fn probe(&mut self, now_ms: u64, events: &mut Vec<Event>) {
if self.bem_seen {
self.probe = Probe::Done;
return;
}
match self.probe {
Probe::NotYet if now_ms >= self.net.start_ms.unwrap_or(now_ms) + PROBE_AFTER_MS => {
self.inner.collect_text();
events.push(Event::Send(aos::GET_INFO.to_vec()));
self.probe = Probe::Sent { at: now_ms };
}
Probe::Sent { at } if now_ms >= at + PROBE_ANSWER_MS => {
log::warn!(
"ngt1: the gateway answers no BEM command: no product information, operating mode or transmit list"
);
self.probe = Probe::Done;
}
_ => {}
}
}
fn on_text(&mut self) {
let text = self.inner.take_text();
if text.is_empty() {
return;
}
for answer in self.answers.push(&text) {
let Some(info) = aos::parse_get_info(&answer) else {
continue;
};
if info.product_type != aos::SIMULATOR_TYPE {
log::info!(
"ngt1: the gateway is an AOS product of type {}, not the simulator",
info.product_type
);
continue;
}
log::info!(
"ngt1: gateway AOS NMEA 2000/0183 Simulator, serial {}, hardware {}, firmware {}; it takes no BEM command and transmits every PGN",
info.serial,
info.hardware_version,
info.firmware_version
);
self.tx_list.abandon();
self.gate = None;
self.aos = Some(info);
self.probe = Probe::Done;
}
}
fn warn_if_dropped(&mut self, pgn: u32) {
if self.gate.is_some() || self.warned.contains(&pgn) {
return;
}
let Some(complete) = self.tx_list.lacks(pgn) else {
return;
};
self.warned.push(pgn);
let fix = "name it, and every PGN you send, with --tx-pgn (pgn_lists.tx in the library)";
if complete {
log::warn!(
"ngt1: PGN {pgn} is not on the NGT-1's transmit list, which drops it; {fix}"
);
} else {
log::warn!(
"ngt1: PGN {pgn} is not on the NGT-1's transmit list as read (Format 1, which \
can miss some), so it may be dropped; {fix}"
);
}
}
fn settle(&mut self, now_ms: u64, events: &mut Vec<Event>) {
match self.settle {
Settle::Opened => self.settle = Settle::Until(now_ms + MODE_SETTLE_MS),
Settle::Until(until) if now_ms >= until => {
self.settle = Settle::Settled;
if !self.held.is_empty() {
log::debug!(
"ngt1: sending {} frames held after Set Operating Mode",
self.held.len()
);
}
send_all(events, std::mem::take(&mut self.held));
}
_ => {}
}
}
fn on_bem(&mut self, payload: &[u8], now_ms: u64, events: &mut Vec<Event>) {
let Some(response) = BemResponse::parse(payload) else {
return;
};
match response.bem {
BEM_OPERATING_MODE => self.on_operating_mode(&response),
BEM_PRODUCT_INFO => {
if self.product_info.add(&response) {
let info = &self.product_info;
log::info!(
"ngt1: gateway {} serial {}, software {}, hardware {}, product code {}, NMEA 2000 {}.{:03}",
info.model,
info.serial_number,
info.software_version,
info.hardware_version,
info.product_code,
info.nmea2000_version / 1000,
info.nmea2000_version % 1000
);
if let Some(firmware) = info.firmware() {
self.tx_list.set_firmware(firmware);
}
}
}
BEM_STARTUP_STATUS => {
if let Some((firmware, reset)) = startup_status(&response) {
log::warn!(
"ngt1: the gateway restarted (firmware {}.{:03}, reset status {reset:#x}); setting it up again",
firmware / 1000,
firmware % 1000
);
}
self.reported_mode = None;
self.product_info = ProductInfo::default();
self.tx_list.restart();
events.push(Event::Send(encode_get_product_info()));
events.push(Event::Send(encode_set_operating_mode(self.mode)));
self.settle = Settle::Until(now_ms + MODE_SETTLE_MS);
}
BEM_ERROR_REPORT => log::warn!(
"ngt1: the gateway reports error {}",
describe_error(response.error)
),
BEM_NEGATIVE_ACK => log::warn!(
"ngt1: the gateway refused a command: error {}",
describe_error(response.error)
),
_ => {}
}
let frame = RawFrame::new(
Some(format_iso_ms(now_ms)),
0,
ACTISENSE_BEM_PGN + u32::from(response.bem),
0,
0,
payload[1..].iter().copied(),
);
self.note_frame(frame, now_ms, events);
}
fn on_operating_mode(&mut self, response: &BemResponse<'_>) {
let Some(mode) = response.operating_mode() else {
return;
};
if response.error != 0 {
log::warn!(
"ngt1: the gateway refused {} (error {}); it stays in {mode}",
self.mode,
describe_error(response.error)
);
}
if self.reported_mode == Some(mode) {
return;
}
self.reported_mode = Some(mode);
if mode == self.mode {
log::info!(
"ngt1: gateway model {:#06x}, serial {}, in {mode}",
response.model_id,
response.serial
);
} else {
log::warn!(
"ngt1: gateway model {:#06x}, serial {}, is in {mode}, not {}",
response.model_id,
response.serial,
self.mode
);
}
}
fn started(&mut self, now_ms: u64) {
if self.net.start_ms.is_none() {
self.net.start_ms = Some(now_ms);
self.net.next_ms = now_ms + NETWORK_STATUS_INTERVAL_MS;
}
}
fn note_frame(&mut self, frame: RawFrame, now_ms: u64, events: &mut Vec<Event>) {
if frame.pgn < SYNTHETIC_PGN_START {
self.net.seen[frame.src as usize] = true;
}
if frame.pgn == ACTISENSE_SYSTEM_STATUS_PGN {
if frame.data.len() >= 11 {
self.net.errors = Some(u32::from_le_bytes([
frame.data[7],
frame.data[8],
frame.data[9],
frame.data[10],
]));
}
if frame.data.len() >= 14 {
self.net.load_pct = Some(frame.data[13]);
}
events.push(Event::Frame(frame));
self.emit_network_status(now_ms, events);
return;
}
events.push(Event::Frame(frame));
}
fn emit_network_status(&mut self, now_ms: u64, events: &mut Vec<Event>) {
self.started(now_ms);
let start_ms = self.net.start_ms.unwrap_or(now_ms);
let device_count = self.net.seen.iter().filter(|s| **s).count().min(255) as u8;
let frame = build_network_status(
NetworkStatus {
load_pct: self.net.load_pct,
errors: self.net.errors,
device_count: Some(device_count),
uptime_s: Some(now_ms.saturating_sub(start_ms).div_euclid(1000) as u32),
gateway_addr: 0xff,
rejected_tx: None,
},
Some(format_iso_ms(now_ms)),
);
events.push(Event::Frame(frame));
self.net.next_ms = now_ms + NETWORK_STATUS_INTERVAL_MS;
}
}
impl Default for Ngt1 {
fn default() -> Self {
Self::new(Config::default())
}
}
fn send_all(events: &mut Vec<Event>, commands: Vec<Vec<u8>>) {
events.extend(commands.into_iter().map(Event::Send));
}
impl Codec for Ngt1 {
fn open(&mut self) -> Vec<u8> {
self.settle = Settle::Opened;
let mut out = encode_get_product_info();
out.extend(encode_set_operating_mode(self.mode));
out
}
fn receive(&mut self, bytes: &[u8], now_ms: u64, events: &mut Vec<Event>) {
self.tick(now_ms, events);
for ev in self.inner.push_bytes(bytes) {
match ev {
NgtEvent::Message(msg) if msg.command == NGT_MSG_RECEIVED => {
self.bem_seen = true;
send_all(events, self.tx_list.on_message(&msg.payload, now_ms));
self.on_bem(&msg.payload, now_ms, events);
}
NgtEvent::Message(msg) => {
if let Some(mut frame) = msg.to_raw_frame() {
frame.timestamp = Some(format_iso_ms(now_ms));
self.note_frame(frame, now_ms, events);
}
}
NgtEvent::Error(e) => events.push(Event::Error(e.to_string())),
}
}
if self.probe != Probe::Done {
self.on_text();
}
}
fn tick(&mut self, now_ms: u64, events: &mut Vec<Event>) {
self.started(now_ms);
self.settle(now_ms, events);
if now_ms >= self.net.next_ms {
self.emit_network_status(now_ms, events);
}
if self.probe != Probe::Done {
self.probe(now_ms, events);
}
if !self.tx_list.is_done() {
send_all(events, self.tx_list.on_tick(now_ms));
}
}
fn send(&mut self, frame: &RawFrame) -> Result<Vec<u8>, Refused> {
if frame.pgn >= SYNTHETIC_PGN_START {
log::debug!("ngt1: skipping synthetic PGN {}", frame.pgn);
return Err(Refused::Synthetic);
}
if let Some(gate) = &self.gate
&& !gate.allows(frame.pgn)
{
return Err(Refused::NotOnTxList);
}
if 6 + frame.data.len() > u8::MAX as usize {
return Err(Refused::TooLarge);
}
self.warn_if_dropped(frame.pgn);
let bytes = encode_n2k_send_frame(frame);
if self.settle == Settle::Settled {
Ok(bytes)
} else {
self.held.push(bytes);
Ok(Vec::new())
}
}
fn keepalive(&self) -> Option<(Duration, Vec<u8>)> {
None
}
}
#[cfg(test)]
mod settle_tests {
use super::*;
const NOW: u64 = 1_780_082_164_826;
fn frame(pgn: u32) -> RawFrame {
RawFrame::new(None, 6, pgn, 0, 21, vec![0x14, 0xf0, 0x01])
}
fn sent(events: &[Event]) -> Vec<&Vec<u8>> {
events
.iter()
.filter_map(|e| match e {
Event::Send(b) => Some(b),
_ => None,
})
.collect()
}
#[test]
fn frames_go_straight_out_without_open() {
let mut d = Ngt1::default();
assert_eq!(
d.send(&frame(59904)),
Ok(encode_n2k_send_frame(&frame(59904)))
);
}
#[test]
fn frames_after_open_are_held_until_the_mode_settles() {
let mut d = Ngt1::default();
d.open();
assert_eq!(d.send(&frame(59904)), Ok(Vec::new()));
let mut events = Vec::new();
d.tick(NOW, &mut events);
assert!(sent(&events).is_empty());
assert_eq!(d.send(&frame(126208)), Ok(Vec::new()));
d.tick(NOW + MODE_SETTLE_MS - 1, &mut events);
assert!(sent(&events).is_empty(), "still settling");
d.tick(NOW + MODE_SETTLE_MS, &mut events);
assert_eq!(
sent(&events),
[
&encode_n2k_send_frame(&frame(59904)),
&encode_n2k_send_frame(&frame(126208))
]
);
assert_eq!(
d.send(&frame(59904)),
Ok(encode_n2k_send_frame(&frame(59904)))
);
}
#[test]
fn a_refused_frame_is_not_held() {
let mut d = Ngt1::default();
d.open();
assert_eq!(d.send(&frame(SYNTHETIC_PGN_START)), Err(Refused::Synthetic));
assert!(d.held.is_empty());
}
#[test]
fn a_startup_status_holds_frames_again() {
let mut d = Ngt1::default();
let mut payload = vec![BEM_STARTUP_STATUS, 1, 0x0e, 0];
payload.extend_from_slice(&[0; 8]);
payload.extend_from_slice(&[0x8e, 0x0a, 0]);
let mut wire = Vec::new();
crate::engine::format::ngt1::encode_ngt_message(NGT_MSG_RECEIVED, &payload, &mut wire);
let mut events = Vec::new();
d.receive(&wire, NOW, &mut events);
assert_eq!(d.send(&frame(59904)), Ok(Vec::new()));
events.clear();
d.tick(NOW + MODE_SETTLE_MS, &mut events);
assert_eq!(sent(&events), [&encode_n2k_send_frame(&frame(59904))]);
}
}
#[cfg(test)]
mod tx_list_warning_tests {
use super::*;
fn frame(pgn: u32) -> RawFrame {
RawFrame::new(None, 2, pgn, 0, 255, vec![1, 2, 3])
}
#[test]
fn a_pgn_the_gateway_drops_is_warned_about_once() {
let mut d = Ngt1::new(Config::default());
assert!(d.send(&frame(127250)).is_ok());
assert!(d.warned.is_empty(), "the list is not read yet");
d.tx_list.read_for_test(&[59904, 127508], true);
assert!(d.send(&frame(127508)).is_ok());
assert!(d.send(&frame(127250)).is_ok());
assert!(d.send(&frame(127250)).is_ok());
assert!(d.send(&frame(130306)).is_ok());
assert_eq!(d.warned, [127250, 130306]);
}
#[test]
fn a_named_transmit_list_warns_nothing() {
let mut d = Ngt1::new(Config {
pgn_lists: PgnLists {
tx: vec![127250],
rx: Vec::new(),
},
..Default::default()
});
d.tx_list.read_for_test(&[59904], true);
assert!(d.send(&frame(127250)).is_ok());
assert_eq!(d.send(&frame(130306)), Err(Refused::NotOnTxList));
assert!(d.warned.is_empty());
}
}
#[cfg(test)]
mod network_status_tests {
use super::*;
const NOW: u64 = 1_780_082_164_826;
#[test]
fn system_status_yields_a_network_status_record() {
let mut d = Ngt1::default();
let mut events = Vec::new();
let mut data = vec![0u8; 16];
data[7..11].copy_from_slice(&1234u32.to_le_bytes());
data[13] = 42; d.net.seen[3] = true;
d.net.seen[9] = true;
d.note_frame(
RawFrame {
timestamp: None,
prio: 0,
pgn: ACTISENSE_SYSTEM_STATUS_PGN,
src: 0,
dst: 0,
data: data.into(),
},
NOW,
&mut events,
);
let status = events
.iter()
.filter_map(|e| match e {
Event::Frame(f) if f.pgn == 0x40100 => Some(f),
_ => None,
})
.next()
.expect("a network status frame");
assert_eq!(status.data.len(), 15);
assert_eq!(status.data[0], 42, "Ch1 Rx Load");
assert_eq!(
u32::from_le_bytes([
status.data[1],
status.data[2],
status.data[3],
status.data[4]
]),
1234,
"Error ID"
);
assert_eq!(status.data[5], 2, "device count");
assert_eq!(status.data[10], 0xff, "gateway address sentinel");
assert_eq!(&status.data[11..15], &[0xff; 4], "rejected TX sentinel");
assert_eq!(
status.timestamp.as_deref(),
Some("2026-05-29T19:16:04.826Z")
);
}
#[test]
fn network_status_runs_on_the_callers_clock() {
let mut d = Ngt1::default();
let mut events = Vec::new();
let frames = |events: &mut Vec<Event>| {
events.retain(|e| matches!(e, Event::Frame(_)));
events.len()
};
d.tick(NOW, &mut events);
assert_eq!(frames(&mut events), 0, "the first status is 5 s out");
d.tick(NOW + 4_999, &mut events);
assert_eq!(frames(&mut events), 0);
d.tick(NOW + 7_000, &mut events);
frames(&mut events);
let [Event::Frame(f)] = &events[..] else {
panic!("expected one status frame, got {events:?}")
};
assert_eq!(f.pgn, 0x40100);
assert_eq!(
u32::from_le_bytes([f.data[6], f.data[7], f.data[8], f.data[9]]),
7,
"uptime in seconds"
);
}
#[test]
fn received_frames_carry_the_callers_time() {
let frame = RawFrame::new(
None,
2,
127250,
52,
255,
[0xff, 0xac, 0xc9, 0xff, 0x7f, 0xff, 0x7f, 0xfd],
);
let wire =
crate::engine::format::ngt1::encode_n2k_received_frame(&frame, 148).expect("fits");
let mut d = Ngt1::default();
let mut events = Vec::new();
d.receive(&wire, NOW, &mut events);
let [Event::Frame(f)] = &events[..] else {
panic!("expected one frame, got {events:?}")
};
assert_eq!(f.pgn, 127250);
assert_eq!(f.timestamp.as_deref(), Some("2026-05-29T19:16:04.826Z"));
}
#[test]
fn gateway_answers_are_actisense_pgns() {
let mut d = Ngt1::new(Config {
pgn_lists: PgnLists {
tx: vec![127508],
rx: vec![],
},
..Default::default()
});
let mut payload = vec![0x11, 1, 0x0e, 0];
payload.extend_from_slice(&[0; 8]);
payload.extend_from_slice(&[2, 0]);
let mut wire = Vec::new();
crate::engine::format::ngt1::encode_ngt_message(NGT_MSG_RECEIVED, &payload, &mut wire);
let mut events = Vec::new();
d.receive(&wire, NOW, &mut events);
let pgns: Vec<u32> = events
.iter()
.filter_map(|e| match e {
Event::Frame(f) => Some(f.pgn),
_ => None,
})
.collect();
assert_eq!(pgns, [262161], "Actisense: Operating mode");
assert!(!d.tx_list.is_done(), "startup confirmed, list read pending");
}
#[test]
fn extra_pgns_alone_leave_the_list_alone() {
let d = Ngt1::new(Config {
extra_tx_pgns: vec![127258],
..Default::default()
});
assert!(
d.tx_list.lacks(127258).is_none(),
"nothing written, nor read yet"
);
let status = pgn_list_status(&PgnLists::default(), &[127258]);
assert_eq!(status.tx, PgnListSupport::Untouched);
let status = pgn_list_status(
&PgnLists {
tx: vec![],
rx: vec![129025],
},
&[],
);
assert_eq!(status.tx, PgnListSupport::Untouched);
assert_eq!(status.rx, PgnListSupport::Unsupported);
}
#[test]
fn a_named_transmit_list_refuses_the_rest() {
let frame = |pgn| RawFrame {
timestamp: None,
prio: 6,
pgn,
src: 0,
dst: 255,
data: vec![0; 8].into(),
};
let mut strict = Ngt1::new(Config {
pgn_lists: PgnLists {
tx: vec![127508],
rx: vec![],
},
extra_tx_pgns: vec![127258],
..Default::default()
});
for pgn in [127508, 127258, 59904] {
assert!(strict.send(&frame(pgn)).is_ok(), "{pgn}");
}
assert_eq!(strict.send(&frame(127506)), Err(Refused::NotOnTxList));
assert_eq!(strict.send(&frame(0x40100)), Err(Refused::Synthetic));
let mut long = frame(127508);
long.data = std::iter::repeat_n(0, 250).collect();
assert_eq!(strict.send(&long), Err(Refused::TooLarge));
long.data.pop();
assert!(strict.send(&long).is_ok(), "249 bytes of data fit");
let mut open = Ngt1::new(Config {
extra_tx_pgns: vec![127258],
..Default::default()
});
assert!(open.send(&frame(127506)).is_ok());
}
#[test]
fn without_system_status_load_and_errors_are_sentinel() {
let mut d = Ngt1::default();
let mut events = Vec::new();
d.emit_network_status(NOW, &mut events);
let Event::Frame(f) = &events[0] else {
panic!("expected a frame")
};
assert_eq!(f.data[0], 0xff, "load sentinel");
assert_eq!(&f.data[1..5], &[0xff; 4], "errors sentinel");
}
}
#[cfg(test)]
mod operating_mode_tests {
use super::*;
use crate::engine::format::ngt1::{NGT_MSG_SEND, encode_ngt_message};
fn answer(mode: u16, error: u32) -> Vec<u8> {
let mut payload = vec![BEM_OPERATING_MODE, 0, 0x0e, 0x00];
payload.extend_from_slice(&1234u32.to_le_bytes());
payload.extend_from_slice(&error.to_le_bytes());
payload.extend_from_slice(&mode.to_le_bytes());
let mut wire = Vec::new();
encode_ngt_message(NGT_MSG_RECEIVED, &payload, &mut wire);
wire
}
#[test]
fn open_sets_the_configured_mode_and_nothing_keeps_it_alive() {
let mut d = Ngt1::default();
let mut open = encode_get_product_info();
open.extend(encode_set_operating_mode(OperatingMode::NgTransferRxAll));
assert_eq!(d.open(), open);
let mut d = Ngt1::new(Config {
operating_mode: OperatingMode::NgTransferNormal,
..Config::default()
});
let mut wire = Vec::new();
encode_ngt_message(NGT_MSG_SEND, &[BEM_OPERATING_MODE, 1, 0], &mut wire);
assert!(d.open().ends_with(&wire));
assert_eq!(d.keepalive(), None);
}
#[test]
fn the_reported_mode_is_noted_and_never_a_frame() {
let mut d = Ngt1::default();
let mut events = Vec::new();
d.receive(&answer(2, 0), NOW, &mut events);
assert_eq!(d.reported_mode, Some(OperatingMode::NgTransferRxAll));
d.receive(&answer(1, 0x8000_0001), NOW, &mut events);
assert_eq!(d.reported_mode, Some(OperatingMode::NgTransferNormal));
assert!(
!events
.iter()
.any(|e| matches!(e, Event::Frame(f) if f.pgn < SYNTHETIC_PGN_START)),
"{events:?}"
);
}
fn bem(bem: u8, sequence: u8, error: i32, data: &[u8]) -> Vec<u8> {
let mut payload = vec![bem, sequence, 0x0e, 0x00];
payload.extend_from_slice(&1234u32.to_le_bytes());
payload.extend_from_slice(&error.to_le_bytes());
payload.extend_from_slice(data);
let mut wire = Vec::new();
encode_ngt_message(NGT_MSG_RECEIVED, &payload, &mut wire);
wire
}
fn padded(text: &str) -> Vec<u8> {
let mut v = text.as_bytes().to_vec();
v.resize(32, 0xff);
v
}
#[test]
fn a_live_system_status_reaches_the_network_status() {
let mut d = Ngt1::default();
let mut data = vec![0u8; 4];
data[2] = 37; let mut events = Vec::new();
d.receive(&bem(BEM_SYSTEM_STATUS, 0, 0, &data), NOW, &mut events);
assert_eq!(d.net.load_pct, Some(37));
assert!(
events
.iter()
.any(|e| matches!(e, Event::Frame(f) if f.pgn == 262386))
);
assert!(
events
.iter()
.any(|e| matches!(e, Event::Frame(f) if f.pgn == 0x40100))
);
}
#[test]
fn product_info_in_five_parts() {
let mut d = Ngt1::default();
let mut events = Vec::new();
let mut main = 2100u16.to_le_bytes().to_vec();
main.extend_from_slice(&1234u16.to_le_bytes());
main.extend_from_slice(&[0, 1]);
d.receive(&bem(BEM_PRODUCT_INFO, 1, 0, &main), NOW, &mut events);
for (part, text) in [(2, "NGT-1"), (3, "2.190"), (4, "Rev B"), (5, "110763")] {
d.receive(
&bem(BEM_PRODUCT_INFO, part, 0, &padded(text)),
NOW,
&mut events,
);
}
assert_eq!(d.product_info.model, "NGT-1");
assert_eq!(d.product_info.software_version, "2.190");
assert_eq!(d.product_info.serial_number, "110763");
assert_eq!(d.product_info.nmea2000_version, 2100);
let mut probe = d.product_info.clone();
let header = [BEM_PRODUCT_INFO, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let mut payload = header.to_vec();
payload.extend(padded("x"));
assert!(
probe.add(&BemResponse::parse(&payload).unwrap()),
"complete"
);
}
#[test]
fn product_info_in_one_message() {
let mut data = 17u32.to_le_bytes().to_vec();
data.extend_from_slice(&2100u16.to_le_bytes());
data.extend_from_slice(&1234u16.to_le_bytes());
for text in ["W2K-1", "1.010", "B", "123456"] {
data.extend(padded(text));
}
data.extend_from_slice(&[0, 2]);
let mut d = Ngt1::default();
let mut events = Vec::new();
d.receive(&bem(BEM_PRODUCT_INFO, 6, 0, &data), NOW, &mut events);
assert_eq!(d.product_info.model, "W2K-1");
assert_eq!(d.product_info.hardware_version, "B");
assert_eq!(d.product_info.product_code, 1234);
}
#[test]
fn a_startup_status_sets_the_gateway_up_again() {
let mut d = Ngt1::new(Config {
pgn_lists: PgnLists {
tx: vec![127508],
rx: vec![],
},
..Default::default()
});
d.tx_list.on_message(&[BEM_OPERATING_MODE, 1], NOW);
let mut events = Vec::new();
d.receive(
&bem(BEM_STARTUP_STATUS, 0, 0, &[0x8e, 0x08, 0]),
NOW,
&mut events,
);
let sent: Vec<&Vec<u8>> = events
.iter()
.filter_map(|e| match e {
Event::Send(b) => Some(b),
_ => None,
})
.collect();
assert_eq!(
sent,
[
&encode_get_product_info(),
&encode_set_operating_mode(OperatingMode::NgTransferRxAll)
]
);
assert!(
events
.iter()
.any(|e| matches!(e, Event::Frame(f) if f.pgn == 262384))
);
assert!(!d.tx_list.is_done());
}
#[test]
fn errors_have_the_sdk_names() {
assert_eq!(describe_error(-1158), "-1158 (command timeout)");
assert_eq!(describe_error(-697), "-697");
let mut d = Ngt1::default();
let mut events = Vec::new();
d.receive(
&bem(BEM_NEGATIVE_ACK, 0, -1158, &[1, 0, 0, 0]),
NOW,
&mut events,
);
d.receive(
&bem(BEM_ERROR_REPORT, 0, -1497, &[4, 1, 0, 0, 0]),
NOW,
&mut events,
);
let pgns: Vec<u32> = events
.iter()
.filter_map(|e| match e {
Event::Frame(f) => Some(f.pgn),
_ => None,
})
.collect();
assert_eq!(pgns, [262388, 262385]);
}
const NOW: u64 = 1_780_082_164_826;
}
#[cfg(test)]
mod aos_probe_tests {
use super::*;
use crate::engine::format::ngt1::{
NGT_MSG_RECEIVED as BEM_RECEIVED, encode_n2k_received_frame, encode_ngt_message,
};
const NOW: u64 = 1_780_082_164_826;
const ANSWER: &[u8] = b"{\r\n \"cmdr\": \"GetInfo\",\r\n \"Serial\": {\r\n \"Type\": 24,\r\n \"Sno\": 2154,\r\n \"HwVersion\": \"2.0\",\r\n \"FwVersion\": \"1.6.3.843\"\r\n }\r\n}\r\n";
fn probes(events: &[Event]) -> usize {
events
.iter()
.filter(|e| matches!(e, Event::Send(b) if b == aos::GET_INFO))
.count()
}
fn named_tx() -> Ngt1 {
Ngt1::new(Config {
pgn_lists: PgnLists {
tx: vec![127508],
rx: vec![],
},
..Config::default()
})
}
#[test]
fn a_silent_gateway_is_asked_and_the_simulator_recognised() {
let mut d = named_tx();
let mut events = Vec::new();
d.tick(NOW, &mut events);
d.tick(NOW + PROBE_AFTER_MS - 1, &mut events);
assert_eq!(probes(&events), 0);
d.tick(NOW + PROBE_AFTER_MS, &mut events);
d.tick(NOW + PROBE_AFTER_MS + 10, &mut events);
assert_eq!(probes(&events), 1);
assert!(d.aos().is_none());
let frame = RawFrame::new(None, 2, 129025, 21, 255, [1, 2, 3, 4, 5, 6, 7, 8]);
let wire = encode_n2k_received_frame(&frame, 1).unwrap();
let mut stream = wire.clone();
stream.extend_from_slice(ANSWER);
stream.extend_from_slice(&wire);
let (a, b) = stream.split_at(wire.len() + 30);
events.clear();
d.receive(a, NOW + PROBE_AFTER_MS + 20, &mut events);
d.receive(b, NOW + PROBE_AFTER_MS + 21, &mut events);
let info = d.aos().expect("recognised");
assert_eq!(info.serial, 2154);
assert_eq!(info.firmware_version, "1.6.3.843");
assert_eq!(
events
.iter()
.filter(|e| matches!(e, Event::Frame(f) if f.pgn == 129025))
.count(),
2
);
assert!(
d.send(&RawFrame::new(None, 6, 130306, 0, 255, [0; 8]))
.is_ok()
);
assert!(d.tx_list.is_done());
}
#[test]
fn a_gateway_that_answers_bem_is_not_asked() {
let mut d = Ngt1::default();
let mut payload = vec![BEM_PRODUCT_INFO, 0, 0x0e, 0x00];
payload.extend_from_slice(&[0; 8]);
let mut wire = Vec::new();
encode_ngt_message(BEM_RECEIVED, &payload, &mut wire);
let mut events = Vec::new();
d.receive(&wire, NOW, &mut events);
d.tick(NOW + 10 * PROBE_AFTER_MS, &mut events);
assert_eq!(probes(&events), 0);
assert_eq!(d.probe, Probe::Done);
}
#[test]
fn another_aos_product_is_not_the_simulator() {
let mut d = named_tx();
let mut events = Vec::new();
d.tick(NOW, &mut events);
d.tick(NOW + PROBE_AFTER_MS, &mut events);
let other = String::from_utf8_lossy(ANSWER).replace("\"Type\": 24", "\"Type\": 7");
d.receive(other.as_bytes(), NOW + PROBE_AFTER_MS + 20, &mut events);
assert!(d.aos().is_none());
assert!(d.gate.is_some());
assert!(!d.tx_list.is_done());
d.tick(NOW + PROBE_AFTER_MS + PROBE_ANSWER_MS, &mut events);
assert_eq!(d.probe, Probe::Done);
assert!(d.aos().is_none());
}
#[test]
fn a_gateway_that_answers_nothing_is_given_up_on() {
let mut d = Ngt1::default();
let mut events = Vec::new();
d.tick(NOW, &mut events);
d.tick(NOW + PROBE_AFTER_MS, &mut events);
d.tick(NOW + PROBE_AFTER_MS + PROBE_ANSWER_MS, &mut events);
d.tick(NOW + 100 * PROBE_AFTER_MS, &mut events);
assert_eq!(probes(&events), 1);
assert_eq!(d.probe, Probe::Done);
assert!(d.aos().is_none());
}
}