use std::sync::{Arc, Mutex};
use std::time::Duration;
use crate::engine::RawFrame;
use crate::engine::format::ikonvert::{NetworkStatus, build_network_status};
use crate::engine::format::{
encode_n2k_send_frame, encode_startup_ping,
ngt1::{Ngt1Decoder, NgtEvent},
};
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;
pub const KEEPALIVE_INTERVAL: Duration = Duration::from_secs(20);
const ACTISENSE_SYSTEM_STATUS_PGN: u32 = SYNTHETIC_PGN_START + 0xf2;
const NETWORK_STATUS_INTERVAL_MS: u64 = 5_000;
#[derive(Debug, Clone, Default)]
pub struct Config {
pub pgn_lists: PgnLists,
pub extra_tx_pgns: Vec<u32>,
pub tx_list_record: Arc<Mutex<TxListRecord>>,
}
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>,
}
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),
}
}
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>) {
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> {
encode_startup_ping()
}
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 => {
send_all(events, self.tx_list.on_message(&msg.payload, now_ms));
}
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())),
NgtEvent::Header(_) => {}
}
}
}
fn tick(&mut self, now_ms: u64, events: &mut Vec<Event>) {
self.started(now_ms);
if now_ms >= self.net.next_ms {
self.emit_network_status(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);
}
Ok(encode_n2k_send_frame(frame))
}
fn keepalive(&self) -> Option<(Duration, Vec<u8>)> {
Some((KEEPALIVE_INTERVAL, encode_startup_ping()))
}
}
#[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], 3, "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();
d.tick(NOW, &mut events);
assert!(events.is_empty(), "the first status is 5 s out");
d.tick(NOW + 4_999, &mut events);
assert!(events.is_empty());
d.tick(NOW + 7_000, &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_not_frames() {
let mut d = Ngt1::new(Config {
pgn_lists: PgnLists {
tx: vec![127508],
rx: vec![],
},
..Default::default()
});
let mut wire = Vec::new();
crate::engine::format::ngt1::encode_ngt_message(NGT_MSG_RECEIVED, &[0x11, 1], &mut wire);
let mut events = Vec::new();
d.receive(&wire, NOW, &mut events);
assert!(
!events
.iter()
.any(|e| matches!(e, Event::Frame(f) if f.pgn != 0x40100)),
"{events:?}"
);
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.is_done(), "nothing to write");
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");
}
}