use std::collections::{HashMap, VecDeque};
use std::sync::mpsc;
use std::time::{SystemTime, UNIX_EPOCH};
use smallvec::SmallVec;
use crate::engine::fastpacket;
use crate::engine::format::ikonvert::{NetworkStatus, build_network_status};
use crate::engine::format::{days_to_ymd, iso11783_compose, iso11783_decompose};
use crate::engine::frame::RawFrame;
use crate::engine::iso_tp::{PGN_TP_CM, TpSender};
use crate::engine::pgn_list::{self, PgnLists};
use crate::engine::{
ADDR_GLOBAL, ADDR_NULL, BusProtocol, FramePacketType, Reassembled, Reassembler,
};
use crate::io::address_claim::{AddressClaim, ClaimState};
use crate::io::device::WriterCmd;
use crate::io::nmea_responder::{self, ProductInfo};
use super::socketcan::{BUILTIN_RX_PGNS as RX_PGN_LIST, BUILTIN_TX_PGNS as TX_PGN_LIST};
pub(super) const CAN_EFF_MASK: u32 = 0x1FFF_FFFF;
pub(super) const CAN_SFF_MASK: u32 = 0x7FF;
#[derive(Debug, Clone)]
pub struct Config {
pub address: u8,
pub unique: u32,
pub manufacturer: u16,
pub system_instance: u8,
pub heartbeat_ms: u64,
pub no_claim: bool,
pub timeout_secs: u64,
pub model_version: Option<&'static str>,
#[cfg_attr(not(target_os = "linux"), allow(dead_code))]
pub configure_link: bool,
pub pgn_lists: PgnLists,
pub learn_tx_pgns: bool,
pub protocol: BusProtocol,
pub bitrate: u32,
pub state_dir: Option<std::path::PathBuf>,
}
impl Default for Config {
fn default() -> Self {
Self {
address: 0,
unique: 0,
manufacturer: 999,
system_instance: 15,
heartbeat_ms: 60_000,
no_claim: false,
timeout_secs: 0,
model_version: None,
configure_link: false,
pgn_lists: PgnLists::default(),
learn_tx_pgns: true,
protocol: BusProtocol::Nmea2000,
bitrate: 250_000,
state_dir: None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(super) struct TxFrame {
pub(super) id: u32,
pub(super) standard: bool,
pub(super) data: SmallVec<[u8; 8]>,
}
pub(super) trait LinkStats: Send {
fn bitrate(&self) -> Option<u32> {
None
}
fn errors(&self) -> Option<u32> {
None
}
fn rejected_tx(&self) -> Option<u32> {
None
}
fn load_sample(&self, _now_ms: u64) -> Option<LoadSample> {
None
}
}
pub(super) struct NoStats;
impl LinkStats for NoStats {}
const NETWORK_STATUS_INTERVAL_MS: u64 = 5_000;
const CAN_EFF_OVERHEAD_BITS: u64 = 67;
const STUFFING_NUMER: u64 = 120;
const STUFFING_DENOM: u64 = 100;
#[derive(Debug, Clone, Copy)]
pub(super) struct LoadSample {
pub(super) rx_bytes: u64,
pub(super) tx_bytes: u64,
pub(super) rx_packets: u64,
pub(super) tx_packets: u64,
pub(super) at_ms: u64,
}
const PGN_ISO_REQUEST: u32 = 59904;
const PGN_ISO_ADDRESS_CLAIM: u32 = 60928;
const PGN_GROUP_FUNCTION: u32 = 126208;
const PGN_PGN_LIST: u32 = 126464;
const PGN_HEARTBEAT: u32 = 126993;
const PGN_PRODUCT_INFO: u32 = 126996;
const CANBOAT_PGN_START: u32 = 0x40000;
const N2K_DB_VERSION: u16 = 2100; const PRODUCT_CODE: u16 = 1;
const CERTIFICATION_LEVEL: u8 = 0; const LOAD_EQUIVALENCY: u8 = 1; const MODEL_ID: &str = "CANboat";
const DEFAULT_MODEL_VERSION: &str = "socketcan-serial-rs";
const GROUP_FUNCTION_REQUEST: u8 = 0;
const GROUP_FUNCTION_ACK: u8 = 2;
const TX_INTERVAL_NO_CHANGE: u32 = 0xffff_ffff;
const TX_INTERVAL_RESTORE_DEFAULT: u32 = 0xffff_fffe;
pub(super) const TX_BUFFER_CAPACITY: usize = 1024;
pub(super) const MAX_POLL_MS: u64 = 100;
pub(super) fn now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0)
}
pub(super) fn format_iso(ms: u64) -> String {
let secs = (ms / 1000) as i64;
let millis = ms % 1000;
let days = secs.div_euclid(86_400);
let tod = secs.rem_euclid(86_400);
let (y, mo, d) = days_to_ymd(days);
let (h, mi, s) = (tod / 3600, (tod % 3600) / 60, tod % 60);
format!("{y:04}-{mo:02}-{d:02}T{h:02}:{mi:02}:{s:02}.{millis:03}Z")
}
fn advertised_pgns(builtin: &[u32], extra: &[u32], which: &str) -> Vec<u32> {
let (list, dropped) = pgn_list::merge(builtin, extra);
if !dropped.is_empty() {
log::warn!("CAN: not advertising {which} PGNs {dropped:?} (invalid, or the list is full)");
}
list
}
pub(super) fn build_name(config: &Config) -> u64 {
let unique = if config.unique != 0 {
config.unique
} else {
crate::engine::os::unique_number(config.state_dir.as_deref())
};
let industry_group = match config.protocol {
BusProtocol::J1939 => 0,
_ => 4,
};
crate::io::name::Name::new(config.manufacturer, unique)
.device_function(130)
.device_class(25)
.industry_group(industry_group)
.system_instance(config.system_instance)
.to_u64()
}
pub(super) struct TxBuffer {
pub(super) queue: VecDeque<TxFrame>,
pub(super) overflowed: usize,
fast_seq: HashMap<(u32, u8), u8>,
protocol: BusProtocol,
pub(super) tp: TpSender,
pub(super) messages: Option<VecDeque<RawFrame>>,
}
impl TxBuffer {
#[cfg(test)]
fn new() -> Self {
Self::with_protocol(BusProtocol::Nmea2000)
}
pub(super) fn with_protocol(protocol: BusProtocol) -> Self {
Self {
queue: VecDeque::with_capacity(64),
overflowed: 0,
fast_seq: HashMap::new(),
protocol,
tp: TpSender::new(),
messages: None,
}
}
pub(super) fn whole_messages() -> Self {
Self {
messages: Some(VecDeque::with_capacity(16)),
..Self::with_protocol(BusProtocol::Nmea2000)
}
}
pub(super) fn push_frame(&mut self, f: &RawFrame) {
self.push(iso11783_compose(f.prio, f.pgn, f.src, f.dst), &f.data);
}
pub(super) fn poll_tp(&mut self, now: u64) {
for f in self.tp.poll(now) {
self.push_frame(&f);
}
}
fn next_fast_seq(&mut self, pgn: u32, src: u8) -> u8 {
let slot = self.fast_seq.entry((pgn, src)).or_insert(0);
let s = *slot;
*slot = (s + 1) & 0x07;
s
}
fn push(&mut self, can_id: u32, data: &[u8]) {
if data.len() > 8 {
log::error!("could not build CAN frame for id {can_id:#x}");
return;
}
self.enqueue(TxFrame {
id: can_id & CAN_EFF_MASK,
standard: false,
data: SmallVec::from_slice(data),
});
}
fn push_standard(&mut self, id: u32, data: &[u8]) {
if id > CAN_SFF_MASK {
log::warn!("CAN: not sending {id:#x}: not an 11-bit identifier");
return;
}
if data.len() > 8 {
log::warn!(
"CAN: not sending {id:#x}: {} bytes do not fit one frame",
data.len()
);
return;
}
self.enqueue(TxFrame {
id,
standard: true,
data: SmallVec::from_slice(data),
});
}
fn enqueue(&mut self, frame: TxFrame) {
if self.queue.len() >= TX_BUFFER_CAPACITY {
if self.overflowed == 0 {
log::error!(
"CAN TX buffer full ({TX_BUFFER_CAPACITY} frames), dropping outbound frame"
);
}
self.overflowed += 1;
return;
}
self.queue.push_back(frame);
}
pub(super) fn pop_sent(&mut self) {
self.queue.pop_front();
if self.overflowed > 0 && self.queue.len() < TX_BUFFER_CAPACITY / 2 {
log::info!(
"CAN TX buffer recovered ({} frames had been dropped)",
self.overflowed
);
self.overflowed = 0;
}
}
pub(super) fn is_empty(&self) -> bool {
self.queue.is_empty()
}
}
pub(super) struct Bus<'a> {
pub(super) tx_buf: &'a mut TxBuffer,
pub(super) frames_tx: &'a mpsc::Sender<RawFrame>,
}
impl<'a> Bus<'a> {
fn send_pgn(&mut self, prio: u8, pgn: u32, src: u8, dst: u8, data: &[u8], emit: bool) {
let can_id = iso11783_compose(prio, pgn, src, dst);
let is_fast = self.tx_buf.protocol.packet_type(pgn) == FramePacketType::Fast;
if let Some(messages) = &mut self.tx_buf.messages {
if !is_fast && data.len() > 8 {
log::warn!(
"CAN: not sending PGN {pgn}: {} bytes do not fit one frame",
data.len()
);
return;
}
if messages.len() >= TX_BUFFER_CAPACITY {
log::error!("TX buffer full ({TX_BUFFER_CAPACITY} messages), dropping PGN {pgn}");
return;
}
messages.push_back(RawFrame::new(
None,
prio,
pgn,
src,
dst,
data.iter().copied(),
));
} else if !is_fast && data.len() > 8 {
if self.tx_buf.protocol == BusProtocol::Nmea2000 {
log::warn!(
"CAN: not sending PGN {pgn}: {} bytes do not fit one frame",
data.len()
);
return;
}
match self.tx_buf.tp.send(now_ms(), pgn, src, dst, data) {
Ok(frames) => {
for f in &frames {
self.tx_buf.push_frame(f);
}
}
Err(e) => {
log::warn!("CAN: not sending PGN {pgn} over ISO TP: {e}");
return;
}
}
} else if !is_fast {
self.tx_buf.push(can_id, data);
} else {
let seq = self.tx_buf.next_fast_seq(pgn, src);
let Some(frames) = fastpacket::fragment(seq, data) else {
log::warn!(
"CAN: not sending PGN {pgn}: {} bytes exceed one fast-packet",
data.len()
);
return;
};
for frame in frames {
self.tx_buf.push(can_id, &frame);
}
}
if emit {
let f = RawFrame::new(
Some(format_iso(now_ms())),
prio,
pgn,
src,
dst,
data.iter().copied(),
);
let _ = self.frames_tx.send(f);
}
}
}
pub(super) struct NmeaDevice {
pub(super) claim: AddressClaim,
pub(super) protocol: BusProtocol,
pub(super) heartbeat_interval: u64, heartbeat_seq: u8,
pub(super) next_heartbeat: u64, last_product_info: u64, model_version: &'static str,
tx_pgns: Vec<u32>,
rx_pgns: Vec<u32>,
learn_tx_pgns: bool,
tx_pgns_full_warned: bool,
stats: Box<dyn LinkStats>,
start_ms: u64,
next_network_status: u64,
seen_addrs: [bool; 256],
bitrate_bps: u32,
prev_load_sample: Option<LoadSample>,
dropped_unclaimed: u64,
}
fn emit(bus: &mut Bus<'_>, frames: Vec<RawFrame>) {
for f in frames {
bus.send_pgn(f.prio, f.pgn, f.src, f.dst, &f.data, true);
}
}
impl NmeaDevice {
pub(super) fn new(config: &Config, stats: Box<dyn LinkStats>) -> Self {
let name = build_name(config);
let claim = if config.no_claim || config.protocol == BusProtocol::Quick {
AddressClaim::disabled(name)
} else {
AddressClaim::new(name, config.address, true)
};
let nmea2000 = config.protocol == BusProtocol::Nmea2000;
Self {
claim,
protocol: config.protocol,
heartbeat_interval: if nmea2000 { config.heartbeat_ms } else { 0 },
heartbeat_seq: 0,
next_heartbeat: 0,
last_product_info: 0,
model_version: config.model_version.unwrap_or(DEFAULT_MODEL_VERSION),
tx_pgns: advertised_pgns(&TX_PGN_LIST, &config.pgn_lists.tx, "transmit"),
rx_pgns: advertised_pgns(&RX_PGN_LIST, &config.pgn_lists.rx, "receive"),
learn_tx_pgns: config.learn_tx_pgns && nmea2000,
tx_pgns_full_warned: false,
start_ms: now_ms(),
next_network_status: 0,
seen_addrs: [false; 256],
bitrate_bps: stats.bitrate().unwrap_or(config.bitrate),
stats,
prev_load_sample: None,
dropped_unclaimed: 0,
}
}
fn addr(&self) -> u8 {
self.claim.send_address().unwrap_or(ADDR_NULL)
}
fn name(&self) -> u64 {
self.claim.name()
}
pub(super) fn start(&mut self, bus: &mut Bus<'_>) {
emit(bus, self.claim.start(now_ms()));
}
fn on_claim(&mut self, bus: &mut Bus<'_>, src: u8, data: &[u8]) {
if data.len() < 8 {
return;
}
let their_name = u64::from_le_bytes(data[..8].try_into().unwrap());
emit(bus, self.claim.on_address_claim(now_ms(), src, their_name));
}
fn on_request(&mut self, bus: &mut Bus<'_>, src: u8, dst: u8, data: &[u8]) {
if data.len() < 3 {
return;
}
let requested = data[0] as u32 | (data[1] as u32) << 8 | (data[2] as u32) << 16;
let addressed = dst == self.addr();
if !addressed && dst != ADDR_GLOBAL {
return; }
if requested == PGN_ISO_ADDRESS_CLAIM {
if let Some(f) = self.claim.respond_to_claim_request() {
emit(bus, vec![f]);
}
return;
}
if !self.claim.is_claimed() {
return; }
let nmea2000 = self.protocol == BusProtocol::Nmea2000;
match requested {
PGN_PRODUCT_INFO if nmea2000 => self.send_product_info(bus),
PGN_PGN_LIST if nmea2000 => self.send_pgn_list(bus, src),
PGN_HEARTBEAT if nmea2000 => self.send_heartbeat(bus),
_ => {
if addressed {
self.send_iso_ack(bus, src, 1 , requested);
}
}
}
}
fn send_product_info(&mut self, bus: &mut Bus<'_>) {
let now = now_ms();
if now - self.last_product_info < 1000 {
return;
}
self.last_product_info = now;
let serial = (self.name() & 0x1fffff).to_string();
let pi = ProductInfo {
db_version: i64::from(N2K_DB_VERSION),
product_code: i64::from(PRODUCT_CODE),
model_id: MODEL_ID,
software_version: env!("CARGO_PKG_VERSION"),
model_version: self.model_version,
model_serial: &serial,
certification_level: i64::from(CERTIFICATION_LEVEL),
load_equivalency: i64::from(LOAD_EQUIVALENCY),
};
emit(bus, pi.frame(self.addr()).into_iter().collect());
}
fn learn_tx_pgn(&mut self, pgn: u32) {
if pgn > pgn_list::MAX_PGN || !self.learn_tx_pgns || self.tx_pgns.contains(&pgn) {
return;
}
if self.tx_pgns.len() >= pgn_list::MAX_PGN_LIST_LEN {
if !self.tx_pgns_full_warned {
log::warn!("CAN: transmit PGN list is full; not advertising PGN {pgn}");
self.tx_pgns_full_warned = true;
}
return;
}
log::info!("CAN: advertising transmit PGN {pgn}, first sent now");
self.tx_pgns.push(pgn);
}
fn send_pgn_list(&self, bus: &mut Bus<'_>, dst: u8) {
emit(
bus,
nmea_responder::pgn_list_frames(self.addr(), dst, &self.tx_pgns, &self.rx_pgns),
);
}
fn send_iso_ack(&self, bus: &mut Bus<'_>, dst: u8, control: u8, pgn: u32) {
emit(
bus,
vec![nmea_responder::iso_ack_frame(
self.addr(),
dst,
control,
pgn,
)],
);
}
fn send_ack_group_function(
&self,
bus: &mut Bus<'_>,
dst: u8,
pgn: u32,
pgn_err: u8,
param_err: u8,
) {
let p = pgn.to_le_bytes();
let data = [
GROUP_FUNCTION_ACK,
p[0],
p[1],
p[2],
(pgn_err & 0x0f) | ((param_err & 0x0f) << 4),
0, ];
bus.send_pgn(6, PGN_GROUP_FUNCTION, self.addr(), dst, &data, true);
}
fn handle_group_function(&mut self, bus: &mut Bus<'_>, src: u8, data: &[u8]) {
if data.len() < 8 || data[0] != GROUP_FUNCTION_REQUEST || !self.claim.is_claimed() {
return;
}
let target = data[1] as u32 | (data[2] as u32) << 8 | (data[3] as u32) << 16;
if target != PGN_HEARTBEAT {
return;
}
let interval = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
let mut param_err = 0u8;
match interval {
TX_INTERVAL_NO_CHANGE => {}
TX_INTERVAL_RESTORE_DEFAULT => {
self.heartbeat_interval = 60000;
self.next_heartbeat = now_ms() + self.heartbeat_interval;
log::info!("Heartbeat interval restored to default 60000 ms");
}
0 => {
self.heartbeat_interval = 0;
log::info!("Heartbeat disabled by group function");
}
1000..=60000 => {
self.heartbeat_interval = interval as u64;
self.next_heartbeat = now_ms() + self.heartbeat_interval;
log::info!("Heartbeat interval set to {interval} ms by group function");
}
_ => {
param_err = 2; log::error!("Requested heartbeat interval {interval} ms out of range");
}
}
self.send_ack_group_function(bus, src, PGN_HEARTBEAT, 0, param_err);
}
pub(super) fn tick(&mut self, bus: &mut Bus<'_>, now: u64) {
let was_claimed = self.claim.is_claimed();
emit(bus, self.claim.tick(now));
if !was_claimed && self.claim.is_claimed() {
log::info!("Address {} claimed", self.addr());
if self.protocol == BusProtocol::Nmea2000 {
self.send_product_info(bus);
}
if self.heartbeat_interval > 0 {
self.next_heartbeat = now + self.heartbeat_interval;
}
self.next_network_status = now + NETWORK_STATUS_INTERVAL_MS;
}
if self.claim.is_claimed() && self.heartbeat_interval > 0 && now >= self.next_heartbeat {
self.send_heartbeat(bus);
self.next_heartbeat = now + self.heartbeat_interval;
}
if self.claim.is_claimed() && now >= self.next_network_status {
self.emit_network_status(bus, now);
self.next_network_status = now + NETWORK_STATUS_INTERVAL_MS;
}
}
fn note_seen(&mut self, src: u8) {
if (src as usize) < self.seen_addrs.len() {
self.seen_addrs[src as usize] = true;
}
}
fn emit_network_status(&mut self, bus: &mut Bus<'_>, now: u64) {
let uptime_s = ((now - self.start_ms) / 1000) as u32;
let device_count = self
.seen_addrs
.iter()
.filter(|seen| **seen)
.count()
.min(u8::MAX as usize) as u8;
let errors = self.stats.errors();
let rejected_tx = self.stats.rejected_tx();
let curr_sample = self.stats.load_sample(now);
let load_pct = match (self.prev_load_sample, curr_sample) {
(Some(prev), Some(curr)) => compute_load_pct(prev, curr, self.bitrate_bps),
_ => None,
};
if let Some(curr) = curr_sample {
self.prev_load_sample = Some(curr);
}
let frame = build_network_status(
NetworkStatus {
load_pct,
errors,
device_count: Some(device_count),
uptime_s: Some(uptime_s),
gateway_addr: self.addr(),
rejected_tx,
},
Some(format_iso(now)),
);
let _ = bus.frames_tx.send(frame);
}
fn send_heartbeat(&mut self, bus: &mut Bus<'_>) {
match nmea_responder::heartbeat_frame(
self.addr(),
self.heartbeat_seq,
self.heartbeat_interval,
) {
Ok(frame) => emit(bus, vec![frame]),
Err(e) => {
log::error!("Heartbeat disabled: {e}");
self.heartbeat_interval = 0;
return;
}
}
self.heartbeat_seq = if self.heartbeat_seq >= 252 {
0
} else {
self.heartbeat_seq + 1
};
}
}
pub(super) fn compute_load_pct(prev: LoadSample, curr: LoadSample, bitrate_bps: u32) -> Option<u8> {
let dt_ms = curr.at_ms.saturating_sub(prev.at_ms);
if dt_ms == 0 || bitrate_bps == 0 {
return None;
}
if curr.rx_bytes < prev.rx_bytes
|| curr.tx_bytes < prev.tx_bytes
|| curr.rx_packets < prev.rx_packets
|| curr.tx_packets < prev.tx_packets
{
return None;
}
let d_bytes = (curr.rx_bytes - prev.rx_bytes) + (curr.tx_bytes - prev.tx_bytes);
let d_packets = (curr.rx_packets - prev.rx_packets) + (curr.tx_packets - prev.tx_packets);
let bits_raw = d_bytes
.saturating_mul(8)
.saturating_add(d_packets.saturating_mul(CAN_EFF_OVERHEAD_BITS));
let bits_on_wire = bits_raw
.saturating_mul(STUFFING_NUMER)
.saturating_div(STUFFING_DENOM);
let denom = (bitrate_bps as u64).saturating_mul(dt_ms);
if denom == 0 {
return None;
}
let pct = bits_on_wire.saturating_mul(100_000) / denom;
Some(pct.min(100) as u8)
}
pub(super) fn handle_frame(
bus: &mut Bus<'_>,
claimer: &mut NmeaDevice,
reasm: &mut Reassembler,
can_id: u32,
data: &[u8],
when: u64,
) {
reasm.tick(when);
let (prio, pgn, src, dst) = iso11783_decompose(can_id);
let single_frame = RawFrame::new(
Some(format_iso(when)),
prio,
pgn,
src,
dst,
data.iter().copied(),
);
if claimer.protocol == BusProtocol::J1939 && pgn == PGN_TP_CM {
for f in bus.tx_buf.tp.on_frame(when, &single_frame) {
bus.tx_buf.push_frame(&f);
}
}
on_frame(bus, claimer, pgn, src, dst, data);
let pt = claimer.protocol.packet_type(pgn);
match reasm.push(single_frame, pt) {
Reassembled::PassThrough(f) | Reassembled::Complete(f) => on_message(bus, claimer, f),
Reassembled::Partial => {}
Reassembled::Error(e) => log::debug!("reassembly: {e}"),
}
}
pub(super) fn handle_message(bus: &mut Bus<'_>, claimer: &mut NmeaDevice, frame: RawFrame) {
on_frame(bus, claimer, frame.pgn, frame.src, frame.dst, &frame.data);
on_message(bus, claimer, frame);
}
fn on_frame(bus: &mut Bus<'_>, claimer: &mut NmeaDevice, pgn: u32, src: u8, dst: u8, data: &[u8]) {
if claimer.claim.state() != ClaimState::Disabled {
if pgn == PGN_ISO_ADDRESS_CLAIM {
claimer.on_claim(bus, src, data);
} else if pgn == PGN_ISO_REQUEST {
claimer.on_request(bus, src, dst, data);
}
}
if src != ADDR_NULL && src != ADDR_GLOBAL {
claimer.note_seen(src);
}
}
fn on_message(bus: &mut Bus<'_>, claimer: &mut NmeaDevice, f: RawFrame) {
if claimer.claim.state() != ClaimState::Disabled
&& claimer.protocol == BusProtocol::Nmea2000
&& f.pgn == PGN_GROUP_FUNCTION
{
claimer.handle_group_function(bus, f.src, &f.data);
}
let _ = bus.frames_tx.send(f);
}
pub(super) fn dispatch_cmd(bus: &mut Bus<'_>, claimer: &mut NmeaDevice, cmd: WriterCmd) {
match cmd {
WriterCmd::Frame(f) => {
if f.pgn >= CANBOAT_PGN_START {
return; }
if claimer.protocol == BusProtocol::Quick {
bus.tx_buf.push_standard(f.pgn, &f.data);
return;
}
if f.src == ADDR_NULL && !may_send_from_null(f.pgn, &f.data) {
log::debug!("CAN: dropping PGN {} from the null address", f.pgn);
return;
}
let own = claimer.claim.send_address();
let src = if f.src == 0 || f.src == ADDR_GLOBAL {
let Some(own) = own else {
if claimer.dropped_unclaimed == 0 {
log::warn!(
"CAN: no source address claimed yet; dropping outbound \
frames until one is"
);
}
claimer.dropped_unclaimed += 1;
return;
};
own
} else {
f.src
};
if own.is_some() && claimer.dropped_unclaimed > 0 {
log::info!(
"CAN: sending from address {} again; dropped {} frames without one",
claimer.addr(),
claimer.dropped_unclaimed
);
claimer.dropped_unclaimed = 0;
}
if Some(src) == own {
claimer.learn_tx_pgn(f.pgn);
}
bus.send_pgn(f.prio, f.pgn, src, f.dst, &f.data, false);
}
WriterCmd::Shutdown(_) => {}
WriterCmd::Bytes(_) => {
log::debug!("WriterCmd::Bytes ignored by the CAN node");
}
}
}
fn may_send_from_null(pgn: u32, data: &[u8]) -> bool {
pgn == PGN_ISO_ADDRESS_CLAIM
|| (pgn == PGN_ISO_REQUEST
&& data.len() >= 3
&& u32::from_le_bytes([data[0], data[1], data[2], 0]) == PGN_ISO_ADDRESS_CLAIM)
}
#[cfg(test)]
mod tests {
use super::*;
fn send(tx_buf: &mut TxBuffer, pgn: u32, data: &[u8]) -> Vec<Vec<u8>> {
let before = tx_buf.queue.len();
let (frames_tx, _frames_rx) = mpsc::channel();
let mut bus = Bus {
tx_buf,
frames_tx: &frames_tx,
};
bus.send_pgn(6, pgn, 42, ADDR_GLOBAL, data, false);
bus.tx_buf
.queue
.iter()
.skip(before)
.map(|f| f.data.to_vec())
.collect()
}
#[test]
fn whole_messages_go_out_unsplit() {
let mut tx_buf = TxBuffer::whole_messages();
let (frames_tx, _frames_rx) = mpsc::channel();
let mut bus = Bus {
tx_buf: &mut tx_buf,
frames_tx: &frames_tx,
};
bus.send_pgn(6, PGN_PRODUCT_INFO, 42, ADDR_GLOBAL, &[7; 134], false);
bus.send_pgn(6, 127508, 42, ADDR_GLOBAL, &[1; 8], false);
bus.send_pgn(6, 127508, 42, ADDR_GLOBAL, &[1; 9], false);
assert!(tx_buf.queue.is_empty());
let messages: Vec<_> = tx_buf.messages.take().unwrap().into_iter().collect();
assert_eq!(messages.len(), 2);
assert_eq!((messages[0].pgn, messages[0].src), (PGN_PRODUCT_INFO, 42));
assert_eq!(messages[0].data.len(), 134);
assert_eq!((messages[1].pgn, messages[1].data.len()), (127508, 8));
}
fn sample(at_ms: u64, rx_bytes: u64, rx_packets: u64) -> LoadSample {
LoadSample {
rx_bytes,
tx_bytes: 0,
rx_packets,
tx_packets: 0,
at_ms,
}
}
#[test]
fn an_idle_bus_reads_zero_percent() {
let pct = compute_load_pct(sample(0, 0, 0), sample(1000, 0, 0), 250_000);
assert_eq!(pct, Some(0));
}
#[test]
fn load_includes_frame_overhead_and_stuffing() {
let pct = compute_load_pct(sample(0, 0, 0), sample(1000, 800, 100), 250_000);
assert_eq!(pct, Some(6));
}
#[test]
fn tx_and_rx_both_count() {
let prev = sample(0, 0, 0);
let mut curr = sample(1000, 800, 100);
curr.tx_bytes = 800;
curr.tx_packets = 100;
let both = compute_load_pct(prev, curr, 250_000).unwrap();
let rx_only = compute_load_pct(prev, sample(1000, 800, 100), 250_000).unwrap();
assert_eq!(both, rx_only * 2);
}
#[test]
fn load_saturates_at_one_hundred() {
let pct = compute_load_pct(sample(0, 0, 0), sample(1000, 10_000_000, 1), 250_000);
assert_eq!(pct, Some(100));
}
#[test]
fn a_counter_past_four_gigabytes_is_not_a_wrap() {
let prev = sample(0, u32::MAX as u64 - 399, 50);
let curr = sample(1000, u32::MAX as u64 + 401, 150);
assert_eq!(compute_load_pct(prev, curr, 250_000), Some(6));
}
#[test]
fn a_counter_reset_reports_no_load() {
let prev = sample(0, 5_000_000, 100_000);
let curr = sample(1000, 0, 0);
assert_eq!(compute_load_pct(prev, curr, 250_000), None);
}
#[test]
fn the_interval_after_a_reset_measures_normally() {
let after_reset = sample(1000, 0, 0);
let next = sample(2000, 800, 100);
assert_eq!(compute_load_pct(after_reset, next, 250_000), Some(6));
}
#[test]
fn a_zero_interval_has_no_load() {
assert_eq!(
compute_load_pct(sample(5, 0, 0), sample(5, 800, 100), 250_000),
None
);
}
#[test]
fn a_zero_bitrate_has_no_load() {
assert_eq!(
compute_load_pct(sample(0, 0, 0), sample(1000, 800, 100), 0),
None
);
}
#[test]
fn a_slower_bus_reads_a_higher_load() {
let prev = sample(0, 0, 0);
let curr = sample(1000, 800, 100);
let slow = compute_load_pct(prev, curr, 125_000).unwrap();
let fast = compute_load_pct(prev, curr, 250_000).unwrap();
assert!(slow > fast, "{slow} should exceed {fast}");
}
#[test]
fn a_link_without_a_bitrate_falls_back_to_the_configured_one() {
let config = Config {
bitrate: 500_000,
..j1939_config()
};
let dev = NmeaDevice::new(&config, Box::new(NoStats));
assert_eq!(dev.bitrate_bps, 500_000);
}
#[test]
fn iso_timestamps_match_canboat_c() {
assert_eq!(format_iso(0), "1970-01-01T00:00:00.000Z");
assert_eq!(format_iso(90_061_500), "1970-01-02T01:01:01.500Z");
assert_eq!(format_iso(1_764_500_000_123), "2025-11-30T10:53:20.123Z");
}
#[test]
fn the_builtin_lists_are_the_housekeeping_pgns() {
assert_eq!(
TX_PGN_LIST,
[
59392, PGN_ISO_REQUEST,
PGN_ISO_ADDRESS_CLAIM,
PGN_GROUP_FUNCTION,
PGN_PGN_LIST,
PGN_HEARTBEAT,
PGN_PRODUCT_INFO,
]
);
assert_eq!(
RX_PGN_LIST,
[PGN_ISO_REQUEST, PGN_ISO_ADDRESS_CLAIM, PGN_GROUP_FUNCTION]
);
}
#[test]
fn the_pgn_lists_carry_the_application_pgns() {
use crate::engine::pgn_list::{PgnListSupport, PgnLists};
let config = Config {
pgn_lists: PgnLists {
tx: vec![127508, 127506, 0x40000],
rx: vec![127245],
},
..Default::default()
};
let dev = NmeaDevice::new(&config, Box::new(NoStats));
assert_eq!(&dev.tx_pgns[..7], &TX_PGN_LIST);
assert_eq!(&dev.tx_pgns[7..], [127508, 127506]);
assert_eq!(&dev.rx_pgns[3..], [127245]);
let status = super::super::socketcan::pgn_list_status(&config.pgn_lists);
assert_eq!(status.tx, PgnListSupport::Answered);
assert_eq!(status.dropped, [0x40000]);
}
#[test]
fn transmitted_pgns_are_learned() {
fn send(dev: &mut NmeaDevice, pgn: u32, src: u8) {
let mut tx_buf = TxBuffer::new();
let (frames_tx, _frames_rx) = mpsc::channel();
let mut bus = Bus {
tx_buf: &mut tx_buf,
frames_tx: &frames_tx,
};
let frame = RawFrame {
timestamp: None,
prio: 6,
pgn,
src,
dst: ADDR_GLOBAL,
data: vec![0; 8].into(),
};
dispatch_cmd(&mut bus, dev, WriterCmd::Frame(frame));
}
fn claimed(config: &Config) -> NmeaDevice {
let mut dev = NmeaDevice::new(config, Box::new(NoStats));
let mut tx_buf = TxBuffer::new();
let (frames_tx, _frames_rx) = mpsc::channel();
let mut bus = Bus {
tx_buf: &mut tx_buf,
frames_tx: &frames_tx,
};
dev.start(&mut bus);
for _ in 0..2 {
let deadline = dev.claim.deadline();
dev.tick(&mut bus, deadline);
}
assert!(dev.claim.is_claimed());
dev
}
let mut dev = NmeaDevice::new(&Config::default(), Box::new(NoStats));
send(&mut dev, 127508, 0);
assert_eq!(dev.tx_pgns, TX_PGN_LIST, "dropped while unclaimed");
let mut dev = claimed(&Config::default());
send(&mut dev, 127508, 0);
send(&mut dev, 127508, 0);
send(&mut dev, 130824, 24); send(&mut dev, 0x40100, 0); send(&mut dev, 0x2_0000, 0); assert_eq!(&dev.tx_pgns[TX_PGN_LIST.len()..], [127508]);
let config = Config {
learn_tx_pgns: false,
..Default::default()
};
let mut dev = claimed(&config);
send(&mut dev, 127508, 0);
assert_eq!(dev.tx_pgns, TX_PGN_LIST);
}
#[test]
fn the_iso_name_carries_function_class_and_unique() {
let config = Config {
unique: 0x12345,
manufacturer: 717,
system_instance: 3,
..Default::default()
};
let name = build_name(&config);
assert_eq!((name & 0x1F_FFFF) as u32, 0x12345, "unique id");
assert_eq!(((name >> 21) & 0x7FF) as u16, 717, "manufacturer");
assert_eq!(((name >> 40) & 0xFF) as u8, 130, "device function");
assert_eq!(((name >> 49) & 0x7F) as u8, 25, "device class");
assert_eq!(((name >> 56) & 0x0F) as u8, 3, "system instance");
assert_eq!(((name >> 60) & 0x07) as u8, 4, "marine industry group");
assert_eq!(name >> 63, 1, "arbitrary-address-capable");
}
#[test]
fn a_zero_unique_is_derived_from_the_machine() {
let a = build_name(&Config::default());
let b = build_name(&Config::default());
assert_eq!(a, b, "stable across calls on one host");
assert_ne!(a & 0x1F_FFFF, 0, "a derived id is not zero");
}
#[test]
fn an_explicit_unique_overrides_the_derived_one() {
let derived = build_name(&Config::default());
let explicit = build_name(&Config {
unique: 7,
..Default::default()
});
assert_eq!(explicit & 0x1F_FFFF, 7);
assert_ne!(derived, explicit);
}
#[test]
fn a_maximum_fast_packet_payload_chunks_completely() {
let data: Vec<u8> = (0..=255u8).cycle().take(223).collect();
let frames = send(&mut TxBuffer::new(), 126996, &data);
assert_eq!(frames.len(), 32);
let carried: Vec<u8> = frames
.iter()
.enumerate()
.flat_map(|(i, f)| if i == 0 { &f[2..] } else { &f[1..] }.to_vec())
.take(223)
.collect();
assert_eq!(carried, data);
}
#[test]
fn the_fast_packet_sequence_wraps_after_eight() {
let mut tx_buf = TxBuffer::new();
let seqs: Vec<u8> = (0..9)
.map(|_| send(&mut tx_buf, 126996, &[0xaa; 10])[0][0])
.collect();
assert_eq!(
seqs,
vec![0x00, 0x20, 0x40, 0x60, 0x80, 0xa0, 0xc0, 0xe0, 0x00]
);
}
#[test]
fn sequence_counters_are_per_pgn() {
let mut tx_buf = TxBuffer::new();
send(&mut tx_buf, 126996, &[0xaa; 10]);
send(&mut tx_buf, 126998, &[0xbb; 10]);
let second_126996 = send(&mut tx_buf, 126996, &[0xaa; 10]);
assert_eq!(second_126996[0][0], 0x20, "126998 must not advance 126996");
}
#[test]
fn single_frame_pgn_goes_out_verbatim() {
let data: Vec<u8> = (1..=8).collect();
let frames = send(&mut TxBuffer::new(), 127508, &data);
assert_eq!(frames, vec![data]);
}
#[test]
fn fast_packet_frame_layout_is_pinned() {
let data: Vec<u8> = (1..=17).collect();
let frames = send(&mut TxBuffer::new(), 126996, &data);
assert_eq!(
frames,
vec![
vec![0x00, 17, 1, 2, 3, 4, 5, 6],
vec![0x01, 7, 8, 9, 10, 11, 12, 13],
vec![0x02, 14, 15, 16, 17, 0xff, 0xff, 0xff],
]
);
}
#[test]
fn short_fast_packet_payload_keeps_the_shell() {
let frames = send(&mut TxBuffer::new(), 126208, &[9, 8, 7, 6, 5, 4]);
assert_eq!(frames, vec![vec![0x00, 6, 9, 8, 7, 6, 5, 4]]);
}
#[test]
fn fast_seq_advances_between_sends() {
let mut tx_buf = TxBuffer::new();
let first = send(&mut tx_buf, 126996, &[0xaa; 10]);
let second = send(&mut tx_buf, 126996, &[0xaa; 10]);
assert_eq!(first[0][0], 0x00);
assert_eq!(second[0][0], 0x20);
}
fn j1939_config() -> Config {
Config {
protocol: BusProtocol::J1939,
..Default::default()
}
}
#[test]
fn j1939_sends_single_frames_only() {
let mut tx_buf = TxBuffer::with_protocol(BusProtocol::J1939);
let data: Vec<u8> = (1..=8).collect();
assert_eq!(send(&mut tx_buf, 130816, &data), vec![data]);
assert_eq!(
send(&mut tx_buf, 126208, &[9, 8, 7, 6, 5, 4]),
vec![vec![9, 8, 7, 6, 5, 4]],
"no fast-packet shell on J1939"
);
}
#[test]
fn j1939_sends_long_messages_over_iso_tp() {
let mut tx_buf = TxBuffer::with_protocol(BusProtocol::J1939);
let data: Vec<u8> = (1..=12).collect();
let announced = send(&mut tx_buf, 130816, &data);
assert_eq!(
announced,
vec![vec![32, 12, 0, 2, 0xFF, 0x00, 0xFF, 0x01]],
"BAM for PGN 130816, 12 bytes in 2 packets"
);
let id = tx_buf.queue.back().unwrap().id;
assert_eq!((id >> 8) & 0x3FFFF, 0xECFF, "TP.CM to global");
let at = tx_buf.tp.next_deadline().unwrap();
let before = tx_buf.queue.len();
tx_buf.poll_tp(at);
tx_buf.poll_tp(at + crate::engine::iso_tp::BAM_GAP_MS);
let packets: Vec<Vec<u8>> = tx_buf
.queue
.iter()
.skip(before)
.map(|f| f.data.to_vec())
.collect();
assert_eq!(
packets,
vec![
vec![1, 1, 2, 3, 4, 5, 6, 7],
vec![2, 8, 9, 10, 11, 12, 0xFF, 0xFF]
]
);
assert!(tx_buf.tp.is_idle());
}
#[test]
fn j1939_node_has_no_nmea2000_housekeeping() {
let name = build_name(&j1939_config());
assert_eq!(((name >> 60) & 0x07) as u8, 0, "global industry group");
let dev = NmeaDevice::new(&j1939_config(), Box::new(NoStats));
assert_eq!(dev.heartbeat_interval, 0);
assert!(!dev.learn_tx_pgns);
}
#[test]
fn j1939_naks_a_product_information_request() {
let mut dev = NmeaDevice::new(&j1939_config(), Box::new(NoStats));
let mut tx_buf = TxBuffer::with_protocol(BusProtocol::J1939);
let (frames_tx, frames_rx) = mpsc::channel();
let mut bus = Bus {
tx_buf: &mut tx_buf,
frames_tx: &frames_tx,
};
dev.start(&mut bus);
for _ in 0..2 {
let deadline = dev.claim.deadline();
dev.tick(&mut bus, deadline);
}
assert!(dev.claim.is_claimed());
while frames_rx.try_recv().is_ok() {}
let p = PGN_PRODUCT_INFO.to_le_bytes();
dev.on_request(&mut bus, 0x21, dev.addr(), &[p[0], p[1], p[2]]);
let sent: Vec<RawFrame> = frames_rx.try_iter().collect();
assert_eq!(sent.len(), 1, "{sent:?}");
assert_eq!(sent[0].pgn, 59392, "ISO Acknowledgement");
assert_eq!(sent[0].data[0], 1, "NAK");
}
#[test]
fn drops_own_frames_until_an_address_is_claimed() {
let config = Config::default();
let mut dev = NmeaDevice::new(&config, Box::new(NoStats));
let mut tx_buf = TxBuffer::new();
let (frames_tx, _frames_rx) = mpsc::channel();
let mut bus = Bus {
tx_buf: &mut tx_buf,
frames_tx: &frames_tx,
};
let frame = |src| WriterCmd::Frame(RawFrame::new(None, 2, 127250, src, 255, [0; 8]));
let sources = |bus: &mut Bus<'_>| -> Vec<u8> {
bus.tx_buf
.queue
.drain(..)
.map(|f| (f.id & 0xff) as u8)
.collect()
};
dev.start(&mut bus);
sources(&mut bus);
dispatch_cmd(&mut bus, &mut dev, frame(0));
dispatch_cmd(&mut bus, &mut dev, frame(ADDR_GLOBAL));
assert_eq!(sources(&mut bus), Vec::<u8>::new(), "scanning");
let deadline = dev.claim.deadline();
dev.tick(&mut bus, deadline);
sources(&mut bus);
dispatch_cmd(&mut bus, &mut dev, frame(0));
assert_eq!(sources(&mut bus), Vec::<u8>::new(), "claim settling");
dispatch_cmd(&mut bus, &mut dev, frame(52));
assert_eq!(sources(&mut bus), vec![52], "impersonation goes out");
dispatch_cmd(&mut bus, &mut dev, frame(ADDR_NULL));
assert_eq!(sources(&mut bus), Vec::<u8>::new(), "not from 254");
let claim = RawFrame::new(None, 6, PGN_ISO_ADDRESS_CLAIM, ADDR_NULL, 255, [0xff; 8]);
dispatch_cmd(&mut bus, &mut dev, WriterCmd::Frame(claim));
let p = PGN_ISO_ADDRESS_CLAIM.to_le_bytes();
let request = RawFrame::new(None, 6, PGN_ISO_REQUEST, ADDR_NULL, 255, [p[0], p[1], p[2]]);
dispatch_cmd(&mut bus, &mut dev, WriterCmd::Frame(request));
assert_eq!(sources(&mut bus), vec![ADDR_NULL, ADDR_NULL]);
let deadline = dev.claim.deadline();
dev.tick(&mut bus, deadline);
assert!(dev.claim.is_claimed());
sources(&mut bus);
dispatch_cmd(&mut bus, &mut dev, frame(0));
assert_eq!(sources(&mut bus), vec![dev.addr()]);
assert_eq!(dev.dropped_unclaimed, 0, "reset once sending again");
}
}