canboat 9.1.0

NMEA 2000 decoding, encoding and live-bus access: the CANboat PGN database as a library, plus the canboat CLI (convert / interface / server / tui).
Documentation
// (C) 2009-2026, Kees Verruijt, Harlingen, The Netherlands.

//! The PGNs a node advertises in its PGN 126464 Transmit / Receive PGN lists.
//!
//! Other devices ask a node what it sends and reads with an ISO Request for
//! PGN 126464. NMEA 2000 expects the answer to cover every PGN the node
//! transmits, so a gateway should advertise the PGNs its *application*
//! sends, not only the ISO housekeeping PGNs it originates itself. How much
//! a given display relies on the list (for instance when choosing data
//! sources) varies by make and firmware.
//!
//! [`PgnLists`] is what the application declares, before the device is
//! opened. How a backend honours it differs per gateway, which
//! [`PgnListSupport`] reports: the SocketCAN gateway answers 126464 itself and
//! can advertise anything, while a stand-alone gateway (NGT-1, iKonvert,
//! YDWG, …) answers with lists held in the device.

/// Most PGNs one PGN 126464 list can carry: the list is a single fast-packet
/// message (at most 223 bytes), one function-code byte plus three bytes per
/// PGN.
pub const MAX_PGN_LIST_LEN: usize = 74;

/// Highest valid PGN; anything above it (canboat's synthetic `>= 0x40000`
/// PGNs included) never goes on the wire, so it is never advertised.
pub(crate) const MAX_PGN: u32 = 0x1_FFFF;

/// ISO and NMEA 2000 network-management PGNs: acknowledgement, request,
/// transport protocol, address claim, group function, PGN list, heartbeat,
/// product and configuration information. Always allowed out, whatever the
/// transmit list says.
pub const NETWORK_MANAGEMENT_PGNS: [u32; 10] = [
    59392, 59904, 60160, 60416, 60928, 126208, 126464, 126993, 126996, 126998,
];

/// What `--rx-filter` lets through, where canboat filters rather than the
/// gateway: the PGNs named, and always the network-management PGNs
/// ([`NETWORK_MANAGEMENT_PGNS`], which device discovery runs on) and
/// canboat's own (the gateway status and such, at or above
/// [`crate::engine::codec::SYNTHETIC_PGN_START`]).
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct RxFilter(Vec<u32>);

impl RxFilter {
    /// A filter on `pgns`; `None` when there are none, which filters
    /// nothing.
    pub(crate) fn new(pgns: &[u32]) -> Option<Self> {
        let mut pgns = pgns.to_vec();
        pgns.sort_unstable();
        pgns.dedup();
        (!pgns.is_empty()).then_some(Self(pgns))
    }

    /// Whether a frame of `pgn` is passed on.
    pub(crate) fn passes(&self, pgn: u32) -> bool {
        pgn >= crate::engine::codec::SYNTHETIC_PGN_START
            || NETWORK_MANAGEMENT_PGNS.contains(&pgn)
            || self.0.binary_search(&pgn).is_ok()
    }
}

/// The PGNs the application transmits and receives, to advertise on top of
/// the gateway's own ISO housekeeping PGNs. Set before the device opens.
///
/// # ⚠️ ON AN iKONVERT OR NGT-1, `tx` IS THE WHOLE TRANSMIT LIST
///
/// **ONCE `tx` NAMES ANY PGN, EVERY PGN NOT IN IT IS REFUSED — NOT SENT.**
/// The gateway itself only transmits the PGNs in its transmit list, and the
/// driver sets that list once, before the gateway starts sending, so it refuses
/// the rest up front (logging each refused PGN once) rather than handing the
/// gateway frames it will reject. Decide the complete list *before* opening
/// the device. The network-management PGNs ([`NETWORK_MANAGEMENT_PGNS`])
/// are always allowed. With `tx` empty nothing is refused, and the
/// gateway's own list decides.
///
/// On SocketCAN the lists are advertisements only: nothing is filtered.
/// `rx` never filters anything.
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PgnLists {
    /// PGNs the application transmits (PGN 126464 function code 0). **On an
    /// iKonvert or NGT-1, the only PGNs (besides network management) it will
    /// send; see the type's documentation.**
    pub tx: Vec<u32>,
    /// PGNs the application receives (PGN 126464 function code 1).
    pub rx: Vec<u32>,
}

impl PgnLists {
    /// Whether neither list names a PGN.
    pub fn is_empty(&self) -> bool {
        self.tx.is_empty() && self.rx.is_empty()
    }
}

/// How a backend honours one of the [`PgnLists`].
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum PgnListSupport {
    /// canboat answers PGN 126464 itself and advertises the list (the
    /// SocketCAN gateway).
    Answered,
    /// The list is written into the gateway, which answers PGN 126464 with
    /// it (iKonvert, NGT-1). On these gateways the Transmit list also
    /// decides which PGNs the gateway will put on the bus at all.
    Pushed,
    /// The backend has no known way to advertise the list; it was ignored.
    Unsupported,
    /// Nothing was named for this list, so the gateway's own list is left
    /// as it is.
    Untouched,
}

/// What a backend did with the [`PgnLists`] it was given, per list.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PgnListStatus {
    pub tx: PgnListSupport,
    pub rx: PgnListSupport,
    /// PGNs from lists the backend supports that are nevertheless not
    /// advertised: invalid PGNs, and those past [`MAX_PGN_LIST_LEN`].
    pub dropped: Vec<u32>,
}

/// Refuses PGNs missing from a transmit list the client named, for the
/// gateways that only send what is on their list (see [`PgnLists`]).
pub(crate) struct TxGate {
    label: &'static str,
    allowed: Vec<u32>,
    /// PGNs refused so far, so each is logged once.
    refused: std::sync::Mutex<Vec<u32>>,
}

impl TxGate {
    /// A gate allowing `named`, canboat's own `extra` PGNs and the
    /// network-management PGNs — or `None` when the client named none, so
    /// nothing is refused. `extra` alone never closes the gate.
    ///
    /// `named` then `extra` go through [`merge`], exactly as the list
    /// written into the gateway does, so a PGN that list leaves out
    /// (invalid, or past [`MAX_PGN_LIST_LEN`]) is refused here too rather
    /// than sent to be rejected by the gateway.
    pub(crate) fn new(label: &'static str, named: &[u32], extra: &[u32]) -> Option<Self> {
        if named.is_empty() {
            return None;
        }
        let (listed, _) = merge(&[], &[named, extra].concat());
        let mut allowed = NETWORK_MANAGEMENT_PGNS.to_vec();
        allowed.extend(listed);
        Some(Self {
            label,
            allowed,
            refused: std::sync::Mutex::new(Vec::new()),
        })
    }

    /// Whether `pgn` may be sent, logging the first refusal of each PGN.
    pub(crate) fn allows(&self, pgn: u32) -> bool {
        if self.allowed.contains(&pgn) {
            return true;
        }
        if let Ok(mut refused) = self.refused.lock()
            && !refused.contains(&pgn)
        {
            refused.push(pgn);
            log::warn!(
                "{}: refusing to send PGN {pgn}: it is not in the transmit list; \
                 name it (--tx-pgn / pgn_lists.tx) before starting",
                self.label
            );
        }
        false
    }
}

/// A list as advertised: `builtin` first, then each PGN of `extra` not
/// already in it. Returns the list and the PGNs of `extra` left out — invalid
/// ones, and those that would take the list past [`MAX_PGN_LIST_LEN`].
pub fn merge(builtin: &[u32], extra: &[u32]) -> (Vec<u32>, Vec<u32>) {
    let mut list = builtin.to_vec();
    let mut dropped = Vec::new();
    for &pgn in extra {
        if list.contains(&pgn) {
            continue;
        }
        if pgn > MAX_PGN || list.len() >= MAX_PGN_LIST_LEN {
            if !dropped.contains(&pgn) {
                dropped.push(pgn);
            }
            continue;
        }
        list.push(pgn);
    }
    (list, dropped)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn the_rx_filter_passes_the_named_and_network_management() {
        assert_eq!(RxFilter::new(&[]), None);
        let f = RxFilter::new(&[129029, 127250, 129029]).unwrap();
        assert!(f.passes(127250));
        assert!(f.passes(129029));
        assert!(!f.passes(130306));
        assert!(f.passes(60928));
        assert!(f.passes(126996));
        assert!(f.passes(262400));
    }

    #[test]
    fn a_gate_allows_what_is_named_and_refuses_the_rest() {
        let gate = TxGate::new("test", &[127508], &[127258]).unwrap();
        for pgn in [127508, 127258, 59904, 126996] {
            assert!(gate.allows(pgn), "{pgn}");
        }
        assert!(!gate.allows(127506));
        assert!(TxGate::new("test", &[], &[127258]).is_none(), "extra alone");
    }

    /// What the gateway's list leaves out is refused too: a PGN beyond
    /// the list's limit (here a quirk's, appended last) or an invalid one.
    #[test]
    fn a_gate_refuses_what_the_list_leaves_out() {
        let named: Vec<u32> = (130_000..130_000 + MAX_PGN_LIST_LEN as u32).collect();
        let gate = TxGate::new("test", &named, &[127258]).unwrap();
        assert!(gate.allows(130_000));
        assert!(!gate.allows(127258), "past the limit");
        let gate = TxGate::new("test", &[127508, 0x2_0000], &[]).unwrap();
        assert!(!gate.allows(0x2_0000), "invalid");
    }

    #[test]
    fn extra_pgns_follow_the_builtin_ones_without_duplicates() {
        let (list, dropped) = merge(&[59392, 126464], &[127508, 126464, 127506, 127508]);
        assert_eq!(list, [59392, 126464, 127508, 127506]);
        assert!(dropped.is_empty());
    }

    #[test]
    fn pgns_that_never_go_on_the_wire_are_dropped() {
        let (list, dropped) = merge(&[59392], &[0x40000, 127508, 0x2_0000]);
        assert_eq!(list, [59392, 127508]);
        assert_eq!(dropped, [0x40000, 0x2_0000]);
    }

    #[test]
    fn the_list_stops_at_one_fast_packet() {
        let extra: Vec<u32> = (130_000..130_100).collect();
        let (list, dropped) = merge(&[59392], &extra);
        assert_eq!(list.len(), MAX_PGN_LIST_LEN);
        assert_eq!(dropped.len(), 100 - (MAX_PGN_LIST_LEN - 1));
        assert_eq!(dropped[0], 130_000 + (MAX_PGN_LIST_LEN as u32 - 1));
    }
}