Skip to main content

Crate canboat

Crate canboat 

Source
Expand description

canboat — NMEA 2000 (canboat) decoding, encoding, and live-bus access.

This is the single crate an external consumer depends on. Its public surface is deliberately small and feature-gated; think in terms of Frame ⇆ DecodedPgn (decode) and PgnBuilder → Frame (encode). See doc/library-api-plan.md in the repository for the full design.

canboat is the fruit of reverse-engineering NMEA 2000 — a closed, pay-to-read standard — out on the water since 2009. This crate embeds the whole distilled result: nearly 600 PGN message definitions across some 350 PGNs — over 220 standardized, plus 370-odd company-specific variants spanning ~25 manufacturers (Simrad, Furuno, Garmin, Raymarine, B&G, …) — and it both decodes and encodes every one of them.

§Quick start

Everything below is in the baseline decode feature — no I/O, no threads. The pivot is Frame (a raw PGN on the wire) and Database (the compiled canboat schema). Fields are addressed by a generated FieldId constant (ids::field) — the recommended path on both sides: PgnBuilder::push to encode, DecodedPgn::field to read back. It is O(1) (a single array index by the field’s schema position) and compile-checked: a typo, or a field from the wrong PGN, fails the build instead of erroring at runtime.

use canboat::{Database, EncodeValue, FieldValue, Units};
use canboat::ids::field::wind_data as wd;

let db = Database::embedded(Units::Metric);

// Encode PGN 130306 "Wind Data": 5.23 m/s at 1.5 rad, apparent wind.
// Unset fields (and proprietary manufacturer/industry selectors) fall
// back to their schema defaults.
let frame: canboat::Frame = db
    .encode("windData")?
    .push(wd::WIND_SPEED, 5.23)?
    .push(wd::WIND_ANGLE, 1.5)?
    .push(wd::REFERENCE, EncodeValue::Lookup("Apparent".into()))?
    .build()?;
assert_eq!(frame.pgn, 130306);

// Decode it back — the read side mirrors the write side.
let decoded = db.decode(&frame).expect("valid frame decodes");
assert_eq!(decoded.id, "windData");
match &decoded.field(wd::REFERENCE).unwrap().value {
    FieldValue::Lookup { name, .. } => assert_eq!(*name, Some("Apparent")),
    other => panic!("unexpected: {other:?}"),
}

§Numbers and raw bits

A number is a value in the field’s unit, whether it is written as an integer or not: push(wd::WIND_SPEED, 5) is 5 m/s, exactly like 5.0. To write a field’s wire bits verbatim — a sentinel such as “not available”, say — wrap them in Raw, the one spelling that skips the scaling and the range check:

let five = db.encode("windData")?.push(wd::WIND_SPEED, 5)?.build()?;
let also_five = db.encode("windData")?.push(wd::WIND_SPEED, 5.0)?.build()?;
assert_eq!(five.data, also_five.data);
// 500 is the wire count of 0.01 m/s steps: the same 5 m/s.
let raw = db.encode("windData")?.push(wd::WIND_SPEED, Raw(500))?.build()?;
assert_eq!(raw.data, five.data);

No constant to hand (the field is chosen at runtime — a config-driven encoder, a CLI arg)? Use the *_by_name twins, PgnBuilder::push_by_name and DecodedPgn::field_by_name — an O(n) scan by schema name, resolved at runtime rather than compile time:

let frame = db
    .encode("windData")?
    .push_by_name("Wind Speed", 5.23)?
    .push_by_name("Reference", EncodeValue::Lookup("Apparent".into()))?
    .build()?;
assert_eq!(db.decode(&frame).unwrap().field_by_name("Wind Speed").is_some(), true);

§Lookup (enum) fields

A LOOKUP field carries an integer code that maps to a label. Encode it by label (EncodeValue::Lookup, shown above) or by code (an integer); on decode, FieldValue::Lookup gives you both the number and its resolved name, so you can match on whichever you have:

// Reference = 2 is "Apparent" — set the enum's code directly.
let frame = db.encode("windData")?.push(wd::REFERENCE, 2)?.build()?;

let decoded = db.decode(&frame).unwrap();
match &decoded.field(wd::REFERENCE).unwrap().value {
    FieldValue::Lookup { value, name } => {
        assert_eq!(*value, 2);                 // the raw enum value
        assert_eq!(*name, Some("Apparent"));   // its schema label, if known
    }
    _ => unreachable!(),
}

For a live bus, add the io feature: bus::Device::new(kind, address) opens any gateway canboat drives (bus::Kind::ALL) and hands back a bidirectional bus::DeviceHandle (a read::FrameSource plus a bus::FrameSender). To own the bus with the address-claim, quirks, and the 2597–2606 TCP serving layer already assembled, add the bridge feature and drive a bridge::Bridge instead.

§Features

  • decode (baseline) — sans-I/O core: schema, decode, encode, formatters, and codec: the NGT-1 / iKonvert / Maretron wire protocols without a port. No threads, no async, no sockets.
  • io — byte-source readers (read::*Reader) for files / text, plus bus::Device to read+write a live bus through any gateway.
  • raw — bus::Device::open_raw alone: single CAN frames to and from the gateways that pass them, with no NMEA 2000 in between.
  • node — device: be a compliant N2K node (address claim + responder).
  • bridge — bridge: the live CAN bus, fully assembled.
  • nmea0183, ais — output-formatter sub-features.

The cli feature (the package default) builds the canboat binary and is not part of this library’s public contract. Library consumers select default-features = false plus the features they need.

Modules§

bridge
The live CAN bus, fully assembled: own a socketcan interface (or another FrameSource via a device backend), claim an address, apply quirks, and expose an in-process DecodedPgn stream plus an optional TCP serving layer (the 2597–2606 ports the canboat server daemon opens).
bus
Talk to a live bus through a gateway: an Actisense NGT-1 or W2K-1, a Yacht Devices gateway, an iKonvert, a MiniPlex, a SocketCAN interface, and the rest of Kind::ALL. Reading a stream of frames while writing frames back, without the full bridge pipeline or its TCP serving layer. This is the middle rung between bring-your-own read::FrameSource and the fully-assembled bridge::Bridge.
codec
The gateways’ wire protocols without the I/O: bytes in, Frames out, and back. For a transport the bus openers cannot reach — WebSerial or a WebSocket in a browser, an async runtime, an embedded UART.
device
Be a compliant N2K node without owning a transport, all Frame-in / Frame-out — the caller owns the bus.
ids
Generated compile-time identity constants: ids::pgn::WIND_DATA, ids::field::wind_data::WIND_ANGLE. Resolve a field once, at build time.
json_input
Analyzer/canboatjs JSON records → wire frames (convert --from json, the gateway bridges’ stdin TX, and the wasm bindings). Behind its own feature because it is the one place a real JSON parser (serde_json) is warranted. Analyzer JSON records → wire frames: the driver behind canboat convert --from json, the gateway bridges’ stdin TX, and the wasm bindings.
output
Turn a DecodedPgn back into bytes/text. write_nmea0183 / write_ais land here when the nmea0183 / ais features are wired.
prelude
The 90% path in one glob import: use canboat::prelude::*;.
read
Inbound: turn a byte source into a stream of DecodedPgn.
schema
Schema introspection: the read-facing shape of the PGN/field definitions, for consumers that treat the database as metadata (codegen, UI forms).

Structs§

Database
The canboat PGN database — a thin wrapper around the build-time generated static schema tables.
DecodedField
One decoded field.
DecodedPgn
A fully decoded PGN event.
FieldId
A compile-time reference to one field within a specific PGN definition: the field descriptor plus the PGN it lives in.
Frame
A CAN/N2K frame.
PgnBuilder
Fluent builder for one PGN message. Created via PgnDatabase::encode / PgnDatabase::encode_by_pgn.
Raw
The wire bits of a field, for PgnBuilder::push: push(f, Raw(0xffff)) writes them verbatim, where push(f, 0xffff) is a value in the field’s unit. See EncodeValue::Raw.
Reassembler
Fast-packet + ISO Transport Protocol reassembler.

Enums§

DecodeError
EncodeError
Why a message could not be encoded. Non-exhaustive: new ways to fail can be added without a breaking release.
EncodeValue
A value to place into a PGN field when encoding. Owned and caller-constructible (unlike the decode crate::engine::FieldValue, which borrows &'static schema references).
FieldValue
The decoded value of one field.
FramePacketType
Bare minimum slice of PGN-database state the reassembler needs. Kept narrow so the reassembler doesn’t link to the full database and the caller can mock it freely in tests.
Reassembled
What the reassembler emitted in response to a push call.
ReassemblyError
Units
Unit system a PgnDatabase presents its numeric fields in.

Constants§

ADDR_GLOBAL
The NMEA 2000 / J1939 global (broadcast) address, 255. Valid as a destination for broadcast PGNs, never as a source on the bus — so it also serves as the “unset: fill in my own claimed address” sentinel on outbound sends (the device / pipeline substitutes the real address).
ADDR_NULL
The null address, 254 — a node that has not (yet) claimed an address. The SocketCAN adapter reports this until its ISO claim wins.
CANBOAT_VERSION
FASTPACKET_MAX_SIZE
Maximum payload size of a coalesced fast-packet PGN.
FRAME_MAX_SIZE
Maximum payload size a RawFrame will accept — the ISO 11783 Transport Protocol ceiling (255 * 7 = 1785). Fast-packet payloads stay at FASTPACKET_MAX_SIZE; TP-carried payloads (and any input format that ships an upstream-coalesced TP record) can go up to this limit before we truncate. Named so the constant announces its scope (any coalesced payload, not just fast-packet).
SCHEMA_HASH