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, andcodec: 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, plusbus::Deviceto read+write a live bus through any gateway.raw—bus::Device::open_rawalone: 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
FrameSourcevia a device backend), claim an address, apply quirks, and expose an in-processDecodedPgnstream plus an optional TCP serving layer (the 2597–2606 ports thecanboat serverdaemon 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 fullbridgepipeline or its TCP serving layer. This is the middle rung between bring-your-ownread::FrameSourceand the fully-assembledbridge::Bridge. - codec
- The gateways’ wire protocols without the I/O: bytes in,
Frames out, and back. For a transport thebusopeners 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 behindcanboat convert --from json, the gateway bridges’ stdin TX, and the wasm bindings. - output
- Turn a
DecodedPgnback into bytes/text.write_nmea0183/write_aisland here when thenmea0183/aisfeatures 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.
- Decoded
Field - One decoded field.
- Decoded
Pgn - 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, wherepush(f, 0xffff)is a value in the field’s unit. SeeEncodeValue::Raw. - Reassembler
- Fast-packet + ISO Transport Protocol reassembler.
Enums§
- Decode
Error - Encode
Error - Why a message could not be encoded. Non-exhaustive: new ways to fail can be added without a breaking release.
- Encode
Value - A value to place into a PGN field when encoding. Owned and
caller-constructible (unlike the decode
crate::engine::FieldValue, which borrows&'staticschema references). - Field
Value - The decoded value of one field.
- Frame
Packet Type - 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
pushcall. - Reassembly
Error - Units
- Unit system a
PgnDatabasepresents 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
RawFramewill accept — the ISO 11783 Transport Protocol ceiling (255 * 7 = 1785). Fast-packet payloads stay atFASTPACKET_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