canboat 8.3.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.

//! PGN database — static, embedded at compile time.
//!
//! The database is generated by `build.rs` from the repository's own
//! `database/*.yaml`
//! from `database/*.yaml` (via keel) and lives in [`crate::engine::schema_data`].
//! Callers get a `&'static PgnDatabase` via [`PgnDatabase::embedded`];
//! there is no runtime JSON parser, no `load(path)`, no `LoadOptions`.

use crate::engine::schema_data;
use crate::engine::types::{
    BitLookupTable, FieldInfo, FieldRef, IndirectLookupTable, LookupFieldTypeTable,
    LookupFieldTypeValue, LookupTable, PgnInfo,
};

/// The canboat PGN database — a thin wrapper around the build-time
/// generated static schema tables.
///
/// One instance is exposed via [`Self::embedded`]; constructing your
/// own is not supported (and not useful — the data is hard-coded).
pub struct PgnDatabase {
    pub schema_version: &'static str,
    pub version: &'static str,
    /// FNV-1a/64 content hash of the schema source (the whole
    /// `database/**` YAML tree, `database/j1939/` included), emitted by
    /// keel. Two processes that exchange field indices must share this
    /// exact value — plus [`Self::units`] and [`Self::is_j1939`], the
    /// other two components of the database identity — before either
    /// can trust the other's per-field `order` numbers.
    pub schema_hash: u64,

    /// Which unit system this database presents (see [`Units`]). The
    /// schema hash is identical for both, so anything exchanging decoded
    /// *values* (not raw bits) across processes must also agree on this.
    units: Units,

    /// Which schema flavor this database's `pgns` array came from. The
    /// schema hash covers the whole database tree and is identical for
    /// both flavors, so anything exchanging field *indices* across
    /// processes must agree on `(schema_hash, units, flavor)` — the
    /// same index resolves to different PGNs in the two tables.
    j1939: bool,

    pgns: &'static [PgnInfo],
    /// `(pgn_number, indices_into_pgns)`, sorted by `pgn_number` for
    /// O(log n) lookup. The variant list per PGN preserves canboat.json
    /// declaration order.
    pgn_index: &'static [(u32, &'static [u32])],

    lookups: &'static [LookupTable],
    bit_lookups: &'static [BitLookupTable],
    indirect_lookups: &'static [IndirectLookupTable],
    field_type_lookups: &'static [LookupFieldTypeTable],

    /// Generated variant dispatch for this database's PGN table. Each
    /// schema flavor (NMEA 2000, J1939) emits its own — the returned
    /// index is only meaningful against `pgns`, so the two must never
    /// be mixed.
    pub(crate) dispatch: fn(u32, &[u8]) -> Option<usize>,
    /// Generated `Fallback: true` catch-all search for `pgns`.
    pub(crate) catchall: fn(u32) -> Option<usize>,
}

/// Unit system a [`PgnDatabase`] presents its numeric fields in.
///
/// canboat's schema is generated in two forms at build time (they share
/// everything but the ~150 fields whose unit differs, and all strings),
/// so this is a zero-cost choice of which static table you decode /
/// encode against — not a runtime conversion.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum Units {
    /// Strict SI base units: `rad`, `K`, `Pa`, `C` (coulomb). What a
    /// physics-facing consumer (Signal K, control code) wants — and
    /// therefore **the default**. Matches `--units si` (canboat C's
    /// `analyzer -si`).
    #[default]
    Si,
    /// canboat's practical humanized units: `deg`, `°C`, `bar`, `Ah`.
    /// What a human reads; matches `--units metric`, i.e. canboat C's
    /// `analyzer` without `-si`. Was the default before the Rust
    /// binaries standardised on SI.
    Metric,
}

// Version constants always come from the main schema: SCHEMA_HASH
// already covers the whole database tree, `database/j1939/` included.
// The PGN table, its index, the generated dispatch/catchall *and* the
// lookup tables vary per flavor - the two trees draw on different
// manufacturer registries (MANUFACTURER_CODE vs J1939_MANUFACTURER_CODE),
// so each module carries only the enumerations its own PGNs reference.
macro_rules! embedded_db {
    ($flavor:ident, $pgns:ident, $units:expr, $j1939:expr) => {
        PgnDatabase {
            schema_version: schema_data::SCHEMA_VERSION,
            version: schema_data::VERSION,
            schema_hash: schema_data::SCHEMA_HASH,
            units: $units,
            j1939: $j1939,
            pgns: crate::engine::$flavor::$pgns,
            pgn_index: crate::engine::$flavor::PGN_INDEX,
            lookups: crate::engine::$flavor::LOOKUPS,
            bit_lookups: crate::engine::$flavor::BIT_LOOKUPS,
            indirect_lookups: crate::engine::$flavor::INDIRECT_LOOKUPS,
            field_type_lookups: crate::engine::$flavor::FIELD_TYPE_LOOKUPS,
            dispatch: crate::engine::$flavor::dispatch,
            catchall: crate::engine::$flavor::find_catchall,
        }
    };
}

static EMBEDDED_SI: PgnDatabase = embedded_db!(schema_data, PGNS_SI, Units::Si, false);
static EMBEDDED_METRIC: PgnDatabase = embedded_db!(schema_data, PGNS_METRIC, Units::Metric, false);
static EMBEDDED_J1939_SI: PgnDatabase = embedded_db!(schema_data_j1939, PGNS_SI, Units::Si, true);
static EMBEDDED_J1939_METRIC: PgnDatabase =
    embedded_db!(schema_data_j1939, PGNS_METRIC, Units::Metric, true);

impl PgnDatabase {
    /// The build-time embedded database in the requested [`Units`].
    #[inline]
    pub fn embedded(units: Units) -> &'static Self {
        match units {
            Units::Si => &EMBEDDED_SI,
            Units::Metric => &EMBEDDED_METRIC,
        }
    }

    /// The build-time embedded **J1939** database — the Rust mirror of
    /// the C `analyzer-j1939` tables, generated from
    /// `database/j1939/pgns/`. Decoding against it is exclusive:
    /// J1939-only PGNs (EEC1, DTCs, …) resolve here and nowhere else,
    /// and NMEA-2000-only PGNs deliberately do not. The ISO PGNs both
    /// buses share (address claim, requests, TP) are present in both
    /// flavors.
    #[inline]
    pub fn embedded_j1939(units: Units) -> &'static Self {
        match units {
            Units::Si => &EMBEDDED_J1939_SI,
            Units::Metric => &EMBEDDED_J1939_METRIC,
        }
    }

    /// The unit system this database decodes into.
    #[inline]
    pub fn units(&self) -> Units {
        self.units
    }

    /// True when this database carries the J1939 PGN tables. Part of
    /// the database identity alongside [`Self::units`] and
    /// `schema_hash` — the flavors share the hash but not the index
    /// space.
    #[inline]
    pub fn is_j1939(&self) -> bool {
        self.j1939
    }

    /// Total number of PGN definitions (including manufacturer variants).
    pub fn pgn_count(&self) -> usize {
        self.pgns.len()
    }

    /// Iterate over every PGN definition in load order.
    pub fn pgns(&self) -> std::slice::Iter<'static, PgnInfo> {
        let pgns: &'static [PgnInfo] = self.pgns;
        pgns.iter()
    }

    /// This db's `&'static` PGN slice — the SI or Metric array. Internal:
    /// the schema-index dispatch resolves against it (see
    /// [`crate::engine::decode::PgnDatabase::pick_variant`]).
    pub(crate) fn pgn_slice(&self) -> &'static [PgnInfo] {
        self.pgns
    }

    /// Iterator over every definition for a given PGN number.
    pub fn pgn_variants(&self, pgn: u32) -> PgnVariants {
        let index: &'static [(u32, &'static [u32])] = self.pgn_index;
        let pgns: &'static [PgnInfo] = self.pgns;
        let slice: &'static [u32] = match index.binary_search_by_key(&pgn, |&(p, _)| p) {
            Ok(i) => index[i].1,
            Err(_) => &[],
        };
        PgnVariants {
            inner: slice.iter(),
            pgns,
        }
    }

    /// First definition for a PGN number, or `None`.
    pub fn first_pgn(&self, pgn: u32) -> Option<&'static PgnInfo> {
        match self.pgn_index.binary_search_by_key(&pgn, |&(p, _)| p) {
            Ok(i) => self.pgn_index[i].1.first().map(|&j| &self.pgns[j as usize]),
            Err(_) => None,
        }
    }

    /// First definition whose `Id` field matches `id` (e.g.
    /// `"cogSogRapidUpdate"`). O(n) linear scan — call once at startup
    /// and cache the resulting `&'static PgnInfo`.
    pub fn pgn_by_id(&self, id: &str) -> Option<&'static PgnInfo> {
        self.pgns.iter().find(|p| p.id == id)
    }

    /// Start encoding a message for the PGN with this schema id (e.g.
    /// `"isoRequest"`, `"windData"`). See [`crate::engine::encode`].
    pub fn encode(
        &'static self,
        pgn_id: &str,
    ) -> Result<crate::engine::encode::PgnBuilder, crate::engine::encode::EncodeError> {
        let pgn = self
            .pgn_by_id(pgn_id)
            .ok_or_else(|| crate::engine::encode::EncodeError::NoSuchPgnId(pgn_id.to_string()))?;
        Ok(crate::engine::encode::PgnBuilder::for_pgn(self, pgn))
    }

    /// Start encoding a message for a generated PGN constant
    /// (`crate::engine::pgn::WIND_DATA`) — the compile-checked counterpart
    /// to [`Self::encode`]. The constant points at the SI schema array,
    /// so this re-resolves it by `id` against this database's own arrays,
    /// meaning field scaling happens in this db's [`Units`].
    pub fn encode_for(&'static self, pgn: &'static PgnInfo) -> crate::engine::encode::PgnBuilder {
        let resolved = self.pgn_by_id(pgn.id).unwrap_or(pgn);
        crate::engine::encode::PgnBuilder::for_pgn(self, resolved)
    }

    /// Start encoding a message by PGN number. Returns
    /// [`crate::engine::encode::EncodeError::AmbiguousPgn`] when the number has
    /// more than one schema variant — use [`Self::encode`] with the id.
    pub fn encode_by_pgn(
        &'static self,
        pgn: u32,
    ) -> Result<crate::engine::encode::PgnBuilder, crate::engine::encode::EncodeError> {
        let mut variants = self.pgn_variants(pgn);
        let first = variants
            .next()
            .ok_or(crate::engine::encode::EncodeError::NoSuchPgn(pgn))?;
        let extra = variants.count();
        if extra > 0 {
            return Err(crate::engine::encode::EncodeError::AmbiguousPgn {
                pgn,
                variants: extra + 1,
            });
        }
        Ok(crate::engine::encode::PgnBuilder::for_pgn(self, first))
    }

    /// Resolve a (PGN id, field id) pair into a [`FieldRef`] that later
    /// indexes into [`crate::engine::DecodedPgn`] at `O(1)` via
    /// [`crate::engine::DecodedPgn::field`]. Both inputs are canboat-json `Id`
    /// values (camelCase machine identifiers, e.g.
    /// `("isoAddressClaim", "uniqueNumber")`). The infallible,
    /// compile-checked counterpart is the generated `field::…` constant,
    /// which is itself a `FieldRef`.
    pub fn field(&self, pgn_id: &str, field_id: &str) -> Option<FieldRef> {
        let info = self.pgn_by_id(pgn_id)?;
        let f = info.fields.iter().find(|f| f.id == field_id)?;
        Some(FieldRef {
            pgn: info,
            field: f,
        })
    }

    /// Find a catch-all "fallback" PGN definition for an unknown `pgn`.
    /// Mirrors canboat's `searchForUnknownPgn`: the largest entry with
    /// `Fallback: true` whose pgn number is `<= pgn`. O(log n) via the
    /// build-time sparse fallback table.
    pub fn fallback_pgn(&self, pgn: u32) -> Option<&'static PgnInfo> {
        // The catch-all search returns a schema index shared by both
        // unit arrays of this db's flavor; resolve it against its own
        // `pgns`.
        Some(&self.pgns[(self.catchall)(pgn)?])
    }

    /// Look up an enum table by name (e.g. `"MANUFACTURER_CODE"`).
    pub fn lookup(&self, name: &str) -> Option<&'static LookupTable> {
        match self.lookups.binary_search_by(|t| t.name.cmp(name)) {
            Ok(i) => Some(&self.lookups[i]),
            Err(_) => None,
        }
    }

    /// Look up a bit-flag table by name.
    pub fn bit_lookup(&self, name: &str) -> Option<&'static BitLookupTable> {
        match self.bit_lookups.binary_search_by(|t| t.name.cmp(name)) {
            Ok(i) => Some(&self.bit_lookups[i]),
            Err(_) => None,
        }
    }

    /// Resolve `(value1, value2)` through an INDIRECT_LOOKUP table.
    pub fn indirect_lookup(&self, name: &str, value1: u64, value2: u64) -> Option<&'static str> {
        let table = match self.indirect_lookups.binary_search_by(|t| t.name.cmp(name)) {
            Ok(i) => &self.indirect_lookups[i],
            Err(_) => return None,
        };
        table.get(value1, value2).map(|v| v.name)
    }

    /// Resolve a DYNAMIC_FIELD_KEY value through a
    /// LookupFieldTypeEnumeration.
    pub fn field_type_lookup(&self, name: &str, key: u64) -> Option<&'static LookupFieldTypeValue> {
        let table = match self
            .field_type_lookups
            .binary_search_by(|t| t.name.cmp(name))
        {
            Ok(i) => &self.field_type_lookups[i],
            Err(_) => return None,
        };
        table.get(key)
    }

    /// Reverse of [`Self::field_type_lookup`]: resolve a DYNAMIC_FIELD_KEY
    /// *label* (e.g. "Polar Performance") to its entry. Lets a config layer
    /// pre-resolve a human key name to its stable numeric `value` once, then
    /// match records by that number at runtime — robust against decode/wire
    /// name-resolution gaps that a name match would silently miss. Linear
    /// over the (small) table; intended for load-time use, not the hot path.
    pub fn field_type_lookup_by_name(
        &self,
        name: &str,
        value_name: &str,
    ) -> Option<&'static LookupFieldTypeValue> {
        let table = match self
            .field_type_lookups
            .binary_search_by(|t| t.name.cmp(name))
        {
            Ok(i) => &self.field_type_lookups[i],
            Err(_) => return None,
        };
        table.values.iter().find(|v| v.name == value_name)
    }
}

/// Iterator returned by [`PgnDatabase::pgn_variants`]. Walks the
/// index slice for a single PGN number, yielding `&'static PgnInfo`
/// for each variant.
pub struct PgnVariants {
    inner: std::slice::Iter<'static, u32>,
    pgns: &'static [PgnInfo],
}

impl Iterator for PgnVariants {
    type Item = &'static PgnInfo;
    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        self.inner.next().map(|&i| &self.pgns[i as usize])
    }
    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        self.inner.size_hint()
    }
}

impl ExactSizeIterator for PgnVariants {}

// Silence unused-import warnings when downstream callers don't touch
// these accessor types directly. They're part of the public API but
// only appear in return types.
#[allow(dead_code)]
fn _api_anchor(_: &FieldInfo) {}