Documentation
¶
Overview ¶
Package n2k is a standalone Go toolkit for NMEA 2000 marine networks. It reads and writes messages from CAN hardware, USB-CAN adapters, network gateways, and capture/replay sources, decoding PGNs into strongly typed Go structs from package pgn.
Index ¶
- func Receive(ctx context.Context, opts ...Option) iter.Seq2[pgn.Message, error]
- func Request[T pgn.Message](ctx context.Context, c *Client, target uint8) (T, error)
- type Bus
- type Client
- func (c *Client) Broadcast(interval time.Duration, provide func() pgn.Message) (stop func())
- func (c *Client) Close() error
- func (c *Client) DeviceAt(source uint8) (Device, bool)
- func (c *Client) Devices() []Device
- func (c *Client) Receive() iter.Seq2[pgn.Message, error]
- func (c *Client) Scanner() *Scanner
- func (c *Client) Write(msg pgn.Message) *WriteResult
- func (c *Client) WrittenFrames() []can.Frame
- type ConfigInfo
- type Device
- type DeviceName
- type FileOption
- type MessageWriter
- type Option
- func CAN(iface string) Option
- func File(path string, opts ...FileOption) Option
- func Filter(expr string) Option
- func IncludeUnknown() Option
- func Replay(frames []can.Frame) Option
- func TCP(addr string, format StreamFormat) Option
- func UDP(listenAddr string, format StreamFormat) Option
- func USB(port string) Option
- func WithBus(bus Bus) Option
- func WithClaimTimeout(d time.Duration) Option
- func WithConfigInfo(ci ConfigInfo) Option
- func WithHeartbeatInterval(d time.Duration) Option
- func WithLogger(l *slog.Logger) Option
- func WithName(name DeviceName) Option
- func WithProductInfo(p ProductInfo) Option
- func WithReconnect(policy ReconnectPolicy) Option
- func WithSourceAddress(addr uint8) Option
- type ProductInfo
- type ReconnectPolicy
- type Scanner
- type StreamFormat
- type WriteResult
Examples ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func Receive ¶
Receive returns an iterator of decoded NMEA 2000 messages from the configured sources. Each yielded value is a pointer to a PGN struct (e.g., *pgn.VesselHeading) or *pgn.UnknownPGN if IncludeUnknown() is set.
Example ¶
Replay the bundled capture file -- no hardware required. Swap n2k.File(...) for n2k.CAN("can0"), n2k.TCP("192.168.4.1:1457", n2k.FormatYDRaw), or n2k.USB("/dev/ttyUSB0") to read a live bus.
package main
import (
"context"
"fmt"
"log"
"github.com/open-ships/n2k"
"github.com/open-ships/n2k/pgn"
)
func main() {
ctx := context.Background()
for msg, err := range n2k.Receive(ctx, n2k.File("testdata/sample.log")) {
if err != nil {
log.Fatal(err)
}
if heading, ok := msg.(*pgn.VesselHeading); ok {
if rad, present := heading.HeadingValue(); present {
fmt.Printf("heading: %.4f rad\n", rad)
return
}
}
}
}
Output: heading: 1.9624 rad
Example (Filter) ¶
Filter messages with a CEL expression; metadata-only expressions skip decoding entirely.
package main
import (
"context"
"fmt"
"log"
"github.com/open-ships/n2k"
"github.com/open-ships/n2k/pgn"
)
func main() {
ctx := context.Background()
for msg, err := range n2k.Receive(ctx,
n2k.File("testdata/sample.log"),
n2k.Filter("pgn == 128267"),
) {
if err != nil {
log.Fatal(err)
}
if depth, ok := msg.(*pgn.WaterDepth); ok {
if meters, present := depth.DepthValue(); present {
fmt.Printf("water depth: %.2f m\n", meters)
return
}
}
}
}
Output: water depth: 2.70 m
func Request ¶
Request sends an ISO Request (PGN 59904) for T's PGN to target and waits for the first matching reply. T names the expected response struct, e.g.
pi, err := n2k.Request[*pgn.ProductInformation](ctx, client, 0x23)
target 255 broadcasts the request and accepts a reply from any device. When ctx has no deadline, a default of 1250 ms (the ISO 11783 response time) is applied. Request works on bus clients only.
Types ¶
type Bus ¶
type Bus interface {
// Run opens the bus and delivers every incoming frame to handler until
// ctx is cancelled or an unrecoverable error occurs.
Run(ctx context.Context, handler func(can.Frame)) error
// WriteFrame sends one frame.
WriteFrame(frame can.Frame) error
// Close releases resources. Safe to call even if Run was never called.
Close() error
}
Bus is a physical or virtual CAN bus. External callers can implement Bus to inject fake hardware for testing, or to adapt transports this library does not ship. Pass an implementation via WithBus.
type Client ¶
type Client struct {
// contains filtered or unexported fields
}
Client is a read/write NMEA 2000 bus node. It composes address claiming, transport protocol, encoding, and framing into a single type that can both receive and transmit PGN messages.
func NewClient ¶
NewClient creates a Client that can read and write NMEA 2000 messages. Provide CAN, USB, TCP, Replay, or WithBus for writable clients; File and UDP are read-only sources for Receive/NewScanner.
Example ¶
NewClient claims a bus address and provides write access. Not runnable without CAN hardware, so this example is compile-only.
package main
import (
"context"
"log"
"github.com/open-ships/n2k"
"github.com/open-ships/n2k/pgn"
)
func main() {
ctx := context.Background()
client, err := n2k.NewClient(ctx, n2k.CAN("can0"))
if err != nil {
log.Fatal(err)
}
defer func() { _ = client.Close() }()
heading := &pgn.VesselHeading{}
heading.SetHeadingValue(1.5708) // radians in, raw wire ticks underneath
if err := client.Write(heading).Wait(); err != nil {
log.Fatal(err)
}
}
Output:
func (*Client) Broadcast ¶
Broadcast schedules periodic transmission of a PGN. provide is called on each tick to build the message; returning nil skips that tick (data not available yet). The first transmission happens as soon as the client's address claim completes.
provide is called once immediately to learn the PGN it produces; if it returns nil there, the PGN is learned from its first non-nil message instead. Scheduling the same PGN again replaces the earlier schedule. The returned stop function halts transmission and is safe to call repeatedly; Close also stops all broadcasters.
Another device can retime or pause a broadcast at runtime by sending a request group function (PGN 126208) naming the broadcast PGN.
func (*Client) Close ¶
Close shuts down the client, cancels the context, and releases resources. It is safe to call Close multiple times.
func (*Client) DeviceAt ¶
DeviceAt resolves a bus source address to the device currently holding it. Use it to correlate a message's SourceId with a stable device identity:
if dev, ok := client.DeviceAt(msg.MessageInfo().SourceId); ok { ... }
func (*Client) Devices ¶
Devices returns a snapshot of every device observed on the bus, sorted by address. The client builds this map passively from address claims, product information, and configuration information; it also requests those PGNs from newly seen devices and enumerates the bus once at startup. Replay clients always return an empty list.
func (*Client) Receive ¶
Receive returns an iterator of decoded NMEA 2000 messages. For bus clients it reads from the internal message channel; for replay clients it builds a fresh Scanner over the client's config so each call gets a full replay.
func (*Client) Scanner ¶
Scanner creates a new Scanner that reads from this client. For bus clients it reads from the internal message channel; for replay clients it builds a fresh Scanner over the client's config.
func (*Client) Write ¶
func (c *Client) Write(msg pgn.Message) *WriteResult
Write asynchronously encodes and transmits a PGN message. The message must be a pointer to a PGN struct (e.g. *pgn.VesselHeading). The returned WriteResult can be used to wait for completion and check for errors. Writes are serialized through a single goroutine to guarantee FIFO ordering.
func (*Client) WrittenFrames ¶
WrittenFrames returns a copy of all CAN frames written through this client. This is primarily useful in replay/testing mode to inspect what was sent.
type ConfigInfo ¶
type ConfigInfo struct {
InstallationDescription1 string
InstallationDescription2 string
ManufacturerInformation string
}
ConfigInfo describes this installation of the device (PGN 126998). All three fields are free-form strings.
type Device ¶
type Device struct {
// RawName is the packed 64-bit ISO 11783 NAME from the device's address
// claim.
RawName uint64
// Name is the decoded NAME.
Name DeviceName
// Address is the device's most recently claimed bus address.
Address uint8
// LastSeen is when the device last transmitted anything.
LastSeen time.Time
// ProductInfo is the device's PGN 126996 payload, nil until observed.
ProductInfo *pgn.ProductInformation
// ConfigInfo is the device's PGN 126998 payload, nil until observed.
ConfigInfo *pgn.ConfigurationInformation
}
Device is one node observed on the NMEA 2000 bus. Devices are identified by their 64-bit ISO 11783 NAME — bus addresses are dynamic and can change under address contention, so RawName is the stable key.
type DeviceName ¶
type DeviceName struct {
IdentityNumber uint32 // 21 bits (0-20)
ManufacturerCode uint16 // 11 bits (21-31)
DeviceInstance uint8 // 8 bits: lower 3 (32-34) + upper 5 (35-39)
DeviceFunction uint8 // 8 bits (40-47)
DeviceClass uint8 // 7 bits (49-55), bit 48 is reserved
SystemInstance uint8 // 4 bits (56-59)
IndustryGroup uint8 // 3 bits (60-62)
}
DeviceName represents the ISO 11783 / NMEA 2000 64-bit NAME that uniquely identifies a device on a CAN bus network. The NAME is used during address claiming (PGN 60928) to arbitrate bus addresses.
func DefaultDeviceName ¶
func DefaultDeviceName() DeviceName
DefaultDeviceName returns a DeviceName pre-configured for a PC-based software gateway operating on an NMEA 2000 marine network. The identity number is randomized so that multiple processes using the same binary can coexist on the same bus without NAME collisions during address claiming.
func UnpackDeviceName ¶
func UnpackDeviceName(name uint64) DeviceName
UnpackDeviceName extracts a DeviceName from the 64-bit NAME integer defined by ISO 11783. The arbitraryAddressCapable flag (bit 63) is not stored in the struct; callers can check it directly via (name >> 63) & 1.
func (DeviceName) Pack ¶
func (d DeviceName) Pack(arbitraryAddressCapable bool) uint64
Pack encodes the DeviceName into the 64-bit NAME integer defined by ISO 11783. The arbitraryAddressCapable flag sets bit 63, indicating the device can participate in dynamic address negotiation.
64-bit NAME layout (LSB-first):
Bits 0-20: Identity Number (21 bits) Bits 21-31: Manufacturer Code (11 bits) Bits 32-34: Device Instance Lower (3 bits) Bits 35-39: Device Instance Upper (5 bits) Bits 40-47: Device Function (8 bits) Bit 48: Reserved (0) Bits 49-55: Device Class (7 bits) Bits 56-59: System Instance (4 bits) Bits 60-62: Industry Group (3 bits) Bit 63: Arbitrary Address Capable (1 bit)
func (DeviceName) Validate ¶
func (d DeviceName) Validate() error
Validate checks that each field fits within its bit width as defined by the ISO 11783 NAME specification. DeviceInstance and DeviceFunction are uint8 and always fit their 8-bit fields.
type FileOption ¶
type FileOption interface {
// contains filtered or unexported methods
}
FileOption configures a File source.
func OriginalTiming ¶
func OriginalTiming() FileOption
OriginalTiming replays frames paced by the log's own timestamps instead of as fast as they can be read. Frames without timestamps are delivered immediately.
type MessageWriter ¶
type MessageWriter interface {
WriteMessage(pgnNum uint32, priority, source, destination uint8, payload []byte) error
}
MessageWriter is optionally implemented by Bus implementations that transmit whole assembled PGN messages rather than raw CAN frames — message-oriented gateways such as Actisense-format streams, where the gateway performs fast-packet fragmentation itself. When a client's bus implements MessageWriter, writes that fit in one message (payloads up to 223 bytes) bypass CAN framing and use WriteMessage; larger ISO-TP transfers and protocol frames (address claims, ISO requests) still go frame-by-frame through WriteFrame.
Implementations may not be able to honor the source address (an Actisense gateway stamps its own claimed address); it is provided so implementations that can control it (custom transports) have it.
type Option ¶
type Option interface {
// contains filtered or unexported methods
}
Option configures the behavior of Receive and NewScanner.
func File ¶
func File(path string, opts ...FileOption) Option
File adds a source that replays CAN frames from a candump -L / -l log file. By default frames are delivered as fast as they can be read; pass OriginalTiming() to pace them by the log's timestamps. File sources are read-only: they work with Receive and NewScanner but not NewClient.
func Filter ¶
Filter sets a CEL expression to filter messages. The expression is automatically partitioned into pre-decode (metadata) and post-decode (struct field) stages.
func IncludeUnknown ¶
func IncludeUnknown() Option
IncludeUnknown includes undecodable messages as *pgn.UnknownPGN in the output stream. By default, unknown PGNs are dropped and logged at debug level.
func TCP ¶
func TCP(addr string, format StreamFormat) Option
TCP adds a source that dials a network gateway (e.g. a Yacht Devices YDWG-02 in RAW server mode, or an Actisense gateway) at addr ("host:port"). TCP works with Receive/NewScanner and can also back NewClient for writes. RAW gateways provide frame-level read/write access; Actisense-format gateways send assembled messages and stamp their own source address.
func UDP ¶
func UDP(listenAddr string, format StreamFormat) Option
UDP adds a source that listens on listenAddr (e.g. ":1457" or "0.0.0.0:1457") for datagrams broadcast by a network gateway. UDP sources are read-only: they work with Receive and NewScanner but not NewClient.
func WithBus ¶
WithBus provides a pre-constructed Bus for the client to use — either a custom transport not shipped by this library, or a fake for testing. When set, the client uses this bus directly instead of constructing one from CAN/USB sources.
func WithClaimTimeout ¶
WithClaimTimeout sets how long NewClient blocks waiting for address claiming to complete on a real CAN bus. Default is 1500ms. This allows time for the initial 250ms claim window plus several rounds of contention renegotiation.
func WithConfigInfo ¶
func WithConfigInfo(ci ConfigInfo) Option
WithConfigInfo sets the installation description (PGN 126998) this client reports when another device requests it.
func WithHeartbeatInterval ¶
WithHeartbeatInterval sets the cadence of the client's automatic heartbeat (PGN 126993). The NMEA 2000 standard requires every device to heartbeat at least every 60 seconds, which is the default. Pass 0 to disable automatic heartbeats. Only bus clients heartbeat; replay clients never do.
func WithLogger ¶
WithLogger overrides the default slog.Default() logger.
func WithName ¶
func WithName(name DeviceName) Option
WithName sets the ISO 11783 device NAME used for address claiming. The NAME is a 64-bit identifier that uniquely identifies this device on the NMEA 2000 network. In address contention, the device with the lower NAME wins. When not set, a default NAME is used (see DefaultDeviceName).
func WithProductInfo ¶
func WithProductInfo(p ProductInfo) Option
WithProductInfo sets the product identity (PGN 126996) this client reports when another device requests it. Without it, a generic software-gateway identity is reported. String fields longer than 32 bytes are truncated on the wire.
func WithReconnect ¶ added in v0.2.0
func WithReconnect(policy ReconnectPolicy) Option
WithReconnect enables automatic reconnection for TCP gateway sources. After a connection drops, the source re-dials with exponential backoff (starting at InitialBackoff, capped at MaxBackoff) until it reconnects or the context is cancelled. Without this option, a dropped TCP connection ends the read loop and surfaces as an error (the historical behavior).
Reconnection covers connections that drop mid-session; the initial connection must still succeed (for NewClient, within the claim timeout). It applies only to TCP sources — CAN, USB, UDP, file, and replay sources are unaffected. On a bus client, a transparent transport reconnect does not re-run NMEA 2000 address claiming, so a gateway that itself rebooted may require a fresh client.
func WithSourceAddress ¶
WithSourceAddress sets an explicit NMEA 2000 source address for the client. When set, the client uses this address and treats contention as a fatal error. When not set (default), the client uses auto mode — starting at address 253 and working downward if contention occurs.
type ProductInfo ¶
type ProductInfo struct {
// N2KVersion is the supported NMEA 2000 standard version in thousandths,
// e.g. 2101 means version 2.101. Zero means the default (2101).
N2KVersion uint16
// ProductCode is the manufacturer-assigned product code.
ProductCode uint16
// ModelID names the product (at most 32 bytes on the wire).
ModelID string
// SoftwareVersion is the software version string (at most 32 bytes).
SoftwareVersion string
// ModelVersion is the model version string (at most 32 bytes).
ModelVersion string
// SerialNumber is the device serial code (at most 32 bytes).
SerialNumber string
// CertificationLevel is the NMEA 2000 certification level lookup value.
CertificationLevel uint8
// LoadEquivalency is the device's bus load in units of 50 mA.
LoadEquivalency uint8
}
ProductInfo describes this client as an NMEA 2000 product. Other devices (chartplotters, network analyzers) request it via PGN 126996 and display it in their device lists.
type ReconnectPolicy ¶ added in v0.2.0
type ReconnectPolicy struct {
// InitialBackoff is the delay before the first reconnect attempt after a
// drop, and the delay restored after any successful connection. Defaults
// to 500ms when zero.
InitialBackoff time.Duration
// MaxBackoff caps the exponentially growing delay between attempts while
// the gateway stays unreachable. Defaults to 30s when zero.
MaxBackoff time.Duration
}
ReconnectPolicy configures automatic reconnection for network gateway (TCP) sources after a dropped connection. The zero value is valid: both fields fall back to their defaults.
type Scanner ¶
type Scanner struct {
// contains filtered or unexported fields
}
Scanner reads decoded NMEA 2000 messages one at a time. Call Next() to advance, Message() to get the current message, and Err() for errors.
func NewScanner ¶
NewScanner creates a Scanner that reads from the configured sources.
Example ¶
The Scanner API is an alternative to the iterator.
package main
import (
"context"
"fmt"
"log"
"github.com/open-ships/n2k"
)
func main() {
ctx := context.Background()
scanner := n2k.NewScanner(ctx, n2k.File("testdata/sample.log"))
messages := 0
for scanner.Next() {
messages++
}
if err := scanner.Err(); err != nil {
log.Fatal(err)
}
fmt.Printf("decoded over 900 messages: %t\n", messages > 900)
}
Output: decoded over 900 messages: true
type StreamFormat ¶
type StreamFormat int
StreamFormat identifies the wire format spoken by a network gateway.
const ( // FormatYDRaw is the Yacht Devices RAW ASCII line protocol (YDWG-02 and // compatible gateways). FormatYDRaw StreamFormat = iota // FormatActisense is the Actisense binary stream protocol (NGT-1 and // compatible gateways). Messages arrive fully assembled and are re-framed // internally so they flow through the same decode pipeline as CAN frames. FormatActisense )
type WriteResult ¶
type WriteResult struct {
// contains filtered or unexported fields
}
WriteResult represents the outcome of an asynchronous write operation. The zero value is safe to use and behaves as a completed, successful write.
func (*WriteResult) Done ¶
func (r *WriteResult) Done() <-chan struct{}
Done returns a channel that is closed when the write is complete. On a zero-value WriteResult, Done returns an already-closed channel.
func (*WriteResult) Wait ¶
func (r *WriteResult) Wait() error
Wait blocks until the write is complete, then returns the error (or nil). On a zero-value WriteResult, Wait returns nil immediately.
Source Files
¶
Directories
¶
| Path | Synopsis |
|---|---|
|
cmd
|
|
|
n2k
command
Command n2k is the NMEA 2000 command-line tool: decode live or recorded bus traffic into JSON, no code required.
|
Command n2k is the NMEA 2000 command-line tool: decode live or recorded bus traffic into JSON, no code required. |
|
pgngen
command
|
|
|
roundtrip
command
Command roundtrip replays a candump log (candump -L / -l format) through the n2k decode pipeline and verifies that every decoded message re-encodes back to the bytes that were on the wire.
|
Command roundtrip replays a candump log (candump -L / -l format) through the n2k decode pipeline and verifies that every decoded message re-encodes back to the bytes that were on the wire. |
|
internal
|
|
|
adapter
Package adapter implements the adapter for raw CAN bus frame endpoints.
|
Package adapter implements the adapter for raw CAN bus frame endpoints. |
|
canbus
Package canbus is built around the Channel structure, which represents a single canbus channel for sending/receiving CAN frames.
|
Package canbus is built around the Channel structure, which represents a single canbus channel for sending/receiving CAN frames. |
|
candump
Package candump parses candump -L / -l log lines into CAN frames.
|
Package candump parses candump -L / -l log lines into CAN frames. |
|
claiming
Package claiming implements the NMEA 2000 / ISO 11783 address claiming protocol (PGN 60928).
|
Package claiming implements the NMEA 2000 / ISO 11783 address claiming protocol (PGN 60928). |
|
decoder
Package decoder converts input messages to an intermediate (Packet) form, and outputs equivalent golang structs.
|
Package decoder converts input messages to an intermediate (Packet) form, and outputs equivalent golang structs. |
|
framer
Package framer builds CAN frames from encoded PGN payloads.
|
Package framer builds CAN frames from encoded PGN payloads. |
|
gateway
Package gateway parses the wire formats spoken by NMEA 2000 network gateways: the Yacht Devices RAW ASCII line protocol and the Actisense binary stream protocol.
|
Package gateway parses the wire formats spoken by NMEA 2000 network gateways: the Yacht Devices RAW ASCII line protocol and the Actisense binary stream protocol. |
|
transport
Package transport implements ISO 11783 Transport Protocol for NMEA 2000 messages that exceed 8 bytes and cannot use fast-packet encoding.
|
Package transport implements ISO 11783 Transport Protocol for NMEA 2000 messages that exceed 8 bytes and cannot use fast-packet encoding. |
|
Package pgn converts NMEA 2000 messages to strongly typed Go data.
|
Package pgn converts NMEA 2000 messages to strongly typed Go data. |
|
Package units provides type-safe unit conversion for physical quantities commonly encountered in marine (NMEA 2000) and general-purpose sensor systems.
|
Package units provides type-safe unit conversion for physical quantities commonly encountered in marine (NMEA 2000) and general-purpose sensor systems. |