n2k

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 20, 2026 License: MIT Imports: 35 Imported by: 0

README

n2k

CI Go Reference Release

n2k is a standalone Go library for NMEA 2000 (N2K) — the CAN-based network that connects marine instruments (GPS, depth, wind, engine, autopilot) on boats. It reads, writes, filters, and replays bus traffic from CAN hardware, USB-CAN adapters, WiFi gateways, and capture files, decoding messages into strongly typed Go structs while preserving byte-exact wire payloads for re-encoding.

It is the Go foundation for NMEA 2000 software — the bus toolkit itself, not a plugin or companion layer for another app. Use it directly in telemetry collectors, loggers, gateways, test rigs, monitoring services, automation, and any Go program that needs to understand or participate on an N2K bus.

Why n2k

  • ~600 typed PGN message types. A PGN (Parameter Group Number) is NMEA 2000's message-type identifier; n2k decodes ~600 of them into Go structs (348 distinct numbers, including manufacturer-proprietary variants), generated from the community-maintained canboat schema — the reference database for open NMEA 2000 decoding. Every numeric field with a physical interpretation gets a generated SI-unit accessor (heading.HeadingValue() → radians) over raw wire ticks.
  • Wire-faithful re-encode. An unchanged decoded message retains its exact original payload, including reserved and trailing bytes. Mutating a field switches encoding to the current struct values, so edits are never silently discarded.
  • A real bus node, not just a decoder. NewClient claims an address per ISO 11783, heartbeats, answers product/configuration info and ISO requests, accepts Commanded Address assignments, and handles NMEA group functions (transmit / retime / pause) — the protocol behavior NMEA 2000 requires of a transmitting device, handled for you.
  • Pure Go, CGO-free, cross-compiles to Linux, macOS, and Windows.

Quick Start — No Boat Required

The repo bundles a real six-second capture from a sailing vessel (testdata/sample.log), so this program runs as-is at a desk:

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)
			}
		}
	}
}

On a boat, only the source option changes:

n2k.CAN("can0")                            // SocketCAN (Linux)
n2k.USB("/dev/ttyUSB0")                    // USB-CAN serial adapter
n2k.TCP("192.168.4.1:1457", n2k.FormatYDRaw)  // Yacht Devices WiFi gateway
n2k.UDP(":1457", n2k.FormatYDRaw)          // same gateway, UDP broadcast
n2k.ActisenseTCP("10.0.0.5:2000")          // role-aware Actisense TCP
n2k.ActisenseSerial("/dev/cu.usbserial-1234") // direct NGT/NGX

The TCP/UDP sources mean you can develop on your laptop against your boat's WiFi gateway — no CAN interface, no Linux, no cross-compiling until you deploy. TCP works for the write path too: NewClient over a Yacht Devices gateway in RAW mode is a full bus node, address claiming included, over the WiFi gateway.

Add it to your project
go get github.com/open-ships/n2k

For command-line capture and diagnostics, install open-ships/n2k-cli.

VERSION declares the exact version published by the next successful main build. The project remains on the github.com/open-ships/n2k v1 module path while its public API is known to be in flux; minor releases may deliberately refine or replace contracts such as the Actisense gateway/Client boundary. Pin the minor version when stability is required. Fully green release automation publishes an annotated tag, deterministic source archive, checksums, SBOM, and separate build-provenance and SBOM attestations through the shared Open Ships release policy.

Using n2k

Everything the library does, mapped to its API:

Area Functionality API
Read decoded traffic Decode live or recorded NMEA 2000 frames into typed PGN structs. Receive, NewScanner
Observe raw traffic Retain owned frame/message/gateway/transport-error records with transport context. Observe, Client.Observations, ReplayObservations
Write PGNs Encode PGN structs back to byte-preserving CAN frames or gateway messages. NewClient, Client.Write
Act as a bus node Claim or accept a commanded address, heartbeat, answer product/configuration info and ISO requests, and handle group functions. NewClient, WithName, WithProductInfo, WithConfigInfo
Schedule transmissions Broadcast PGNs periodically and let other devices retime or pause them through group functions. Client.BroadcastPGN, Client.Broadcast
Request data Send typed ISO requests and await typed replies. Request[T]
Discover devices Track observed devices by stable 64-bit NAME and current source address. Client.Devices, Client.DeviceAt
Read many sources Use SocketCAN, USB-CAN adapters, direct Actisense serial, TCP/UDP gateways, candump/EBL logs, or in-memory frames. CAN, USB, Serial, TCP, UDP, File, EBL, Replay
Gateway formats Speak Yacht Devices RAW, Actisense binary/ASCII messages, and source-authoritative BST-95 or mode-6 CAN. FormatYDRaw, FormatActisense*
Control Actisense devices Use typed local BEM through an honest gateway session or the same commands remotely over PGN 126720. ActisenseGatewaySession, ActisenseRemoteDevice
Record Actisense evidence Write EBL files or attach an exact session wire trace with per-layer metrics. NewEBLWriter, NewActisenseEBLTrace, WithActisenseWireTrace
Physical values Keep raw wire ticks while exposing generated SI-unit accessors. <Field>Value, Set<Field>Value, pgn.PhysicalValue
Filter traffic Filter by PGN metadata or decoded fields with CEL; metadata-only filters avoid decode work. Filter
Preserve unknowns Drop undecoded PGNs by default or surface them for logging/research. IncludeUnknown, *pgn.UnknownPGN
Test without hardware Replay bundled or custom captures and inspect written frames in tests. File, OriginalTiming, Replay, WrittenFrames
Inspect PGN support Query complete runtime metadata, fields, ranges, and confidence. pgn.PgnInfoLookup
Observe health Export connection epochs, queue/counter metrics, address state, and structured logs. Client.Status, Client.Err, WithLogger
Extend transports Provide custom frame buses, observation buses, readiness, lifecycle, or assembled-message writers. Bus, ObservationBus, ReadyBus, ConnectionLifecycleBus, MessageWriter, WithBus
Reading and Writing

Client provides read and write access to NMEA 2000. Use it when you need to transmit messages in addition to receiving them.

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

client, err := n2k.NewClient(ctx,
    n2k.CAN("can0"), // auto-claims an available address
)
if err != nil {
    panic(err)
}
defer client.Close()

// Write a message. The struct knows its own PGN number.
// Priority defaults to 6, destination defaults to broadcast (255).
heading := &pgn.VesselHeading{}
heading.SetHeadingValue(1.5708) // radians; stored as raw wire ticks
result := client.Write(heading)
if err := result.WaitContext(ctx); err != nil {
    log.Printf("write failed: %v", err)
}

// Explicitly set priority. VesselHeading is a broadcast (PDU2) PGN, so it
// cannot carry a destination address.
heading2 := &pgn.VesselHeading{
    Info: pgn.MessageInfo{Priority: pgn.Priority(2)},
}
heading2.SetHeadingValue(1.5708)
if err := client.Write(heading2).WaitContext(ctx); err != nil { ... }

// Addressed PGNs use TargetId. ISO Request is a PDU1 PGN.
requested := uint64(126996)
request := &pgn.IsoRequest{
    Info: pgn.MessageInfo{TargetId: pgn.Target(42)},
    Pgn:  &requested,
}
if err := client.Write(request).WaitContext(ctx); err != nil { ... }

// Read messages (same as top-level API)
for msg, err := range client.Receive() {
    if err != nil {
        panic(err)
    }
    fmt.Printf("Msg: %v\n", msg)
}
Address Claiming

Every device that transmits on NMEA 2000 must claim a unique bus address (0–251) using the ISO 11783 address claim protocol (PGN 60928). NewClient handles this automatically — it broadcasts an address claim, waits for contention, and only returns once a valid address is secured.

How contention works: Each device has a 64-bit NAME. When two devices claim the same address, the lower NAME wins and keeps the address; the loser must yield. The client supports two modes:

// Auto mode (default) — starts at address 251 and negotiates downward on
// contention. If all addresses are exhausted, NewClient returns an error.
client, err := n2k.NewClient(ctx, n2k.CAN("can0"))

// Explicit mode — uses a fixed address. If another device with a lower NAME
// contests it, NewClient returns an error instead of retrying.
client, err := n2k.NewClient(ctx,
    n2k.CAN("can0"),
    n2k.WithSourceAddress(42),
)

Device NAME: The NAME determines who wins contention. Lower NAME = higher priority. Customize it to control your device's identity and arbitration priority on the bus:

client, err := n2k.NewClient(ctx,
    n2k.CAN("can0"),
    n2k.WithName(n2k.DeviceName{
        IndustryGroup:    4,     // 3 bits: 4 = Marine
        ManufacturerCode: 123,   // 11 bits: replace with your assigned code
        DeviceClass:      25,    // 7 bits: 25 = Internetwork Device
        DeviceFunction:   130,   // 8 bits: 130 = PC Gateway
        DeviceInstance:   0,     // 8 bits
        SystemInstance:   0,     // 4 bits
        IdentityNumber:   12345, // 21 bits: unique per physical device
    }),
)

When WithName is not set, DefaultDeviceName() is used. That development default randomizes the identity number so multiple local processes can coexist on one bus. Production devices must provide their assigned manufacturer code and a stable, persisted identity with WithName; a random identity is not a provisioned product identity.

Persist the last claimed address and offer it on the next start; the client will try it first and move if it is occupied:

client, err := n2k.NewClient(ctx,
    n2k.CAN("can0"),
    n2k.WithPreferredAddress(loadLastAddress()),
)
// Save client.Status().Address after claiming or during orderly shutdown.

The client also handles Commanded Address (PGN 65240) without application code. It accepts only an exact nine-byte broadcast ISO transport (BAM) transfer whose full 64-bit NAME matches the client and whose requested address is 0–251. A valid change immediately emits a new Address Claim and places application writes behind a fresh contention window. Mismatched NAMEs, fast-packet or addressed lookalikes, malformed lengths, special addresses, and commands for the current address have no effect. Observe moves through client.Status().Address; user filters and unread subscriptions cannot disable this protocol behavior.

Claim timeout: NewClient blocks for up to 1500ms (the default) to allow the network to respond to the initial claim. On heavily contested buses, increase it:

client, err := n2k.NewClient(ctx,
    n2k.CAN("can0"),
    n2k.WithClaimTimeout(3 * time.Second),
)
Read-only
Iterator API
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

for msg, err := range n2k.Receive(ctx, n2k.CAN("can0")) {
    if err != nil {
        panic(err)
    }
    fmt.Printf("Msg: %v\n", msg)
}
Scanner API
s := n2k.NewScanner(ctx, n2k.CAN("can0"))
defer s.Close()
for s.Next() {
    fmt.Printf("Msg: %v\n", s.Message())
}
if err := s.Err(); err != nil {
    ...
}
Raw observations

Use Observe when typed decoding would discard context needed by a recorder, bridge, latency monitor, or multi-network diagnostic:

for observation, err := range n2k.Observe(ctx, n2k.File("capture.log")) {
    if err != nil {
        return err
    }
    fmt.Println(observation.AdapterID, observation.NetworkID,
        observation.Timestamp, observation.Direction, observation.Frame)
}

client.Observations() additionally publishes assembled-message, decode-error, gateway, and transport-error events. Every record owns its frame and payload bytes. A slow subscriber ends with ErrObservationOverflow without delaying address claims, ISO responses, decoding, or other subscribers. pgn.MessageInfo carries the same timing, Adapter, network, and direction context into typed messages.

Multiple Networks

Read from multiple CAN interfaces simultaneously:

for msg, err := range n2k.Receive(ctx,
    n2k.CAN("can0"),
    n2k.CAN("can1"),
    n2k.USB("/dev/ttyUSB0"),
) {
    // messages from all sources, interleaved by arrival
}
Log Files and Network Gateways

Frames don't have to come from local CAN hardware:

// Replay a candump -L / -l capture, as fast as possible...
for msg, err := range n2k.Receive(ctx, n2k.File("testdata/sample.log")) { ... }

// ...or paced by the log's own timestamps.
for msg, err := range n2k.Receive(ctx, n2k.File("capture.log", n2k.OriginalTiming())) { ... }

// Replay an Actisense EBL capture (raw BDTP or BSTRawFrame records).
for msg, err := range n2k.Receive(ctx, n2k.EBL("capture.ebl", n2k.OriginalTiming())) { ... }

// Yacht Devices YDWG-02 (RAW server mode) over TCP or UDP.
for msg, err := range n2k.Receive(ctx, n2k.TCP("192.168.4.1:1457", n2k.FormatYDRaw)) { ... }
for msg, err := range n2k.Receive(ctx, n2k.UDP(":1457", n2k.FormatYDRaw)) { ... }

// Actisense-format streams (W2K-1 gateways, or an NGT-1 behind a TCP
// bridge). Read-only use passively accepts all supported BST records.
for msg, err := range n2k.Receive(ctx, n2k.ActisenseTCP("10.0.0.5:2000")) { ... }

// Direct NGT/NGX serial connection; defaults to 115200 8N1.
for msg, err := range n2k.Receive(ctx, n2k.ActisenseSerial("/dev/cu.usbserial-1234")) { ... }

File, EBL, and UDP are read-only (use them with Receive/NewScanner). TCP and Serial also work with NewClient:

// A complete bus device over the boat's WiFi gateway. RAW mode is
// frame-level in both directions, so address claiming, heartbeats, and
// group functions behave exactly as on CAN hardware. The gateway echoes
// transmitted frames back, so the client also observes its own traffic.
client, err := n2k.NewClient(ctx, n2k.TCP("192.168.4.1:1457", n2k.FormatYDRaw))

// Preferred Actisense Client path. The dedicated constructors query the prior
// mode, set and acknowledge mode 5, then exchange source-authoritative BST-95
// frames. A gateway that rejects or strips BEM setup fails explicitly; there
// is no automatic fallback to gateway-owned writes.
client, err := n2k.NewClient(ctx, n2k.ActisenseTCP("10.0.0.5:2000"))
client, err := n2k.NewClient(ctx, n2k.ActisenseSerial("/dev/cu.usbserial-1234"))

// Gateway-owned BST-94 transmission is a separate session because the wire
// format has no source-address field. It never runs a virtual address claim.
session, err := n2k.NewActisenseTCPSession(ctx, "10.0.0.5:2000")
err = session.SendRawPGN(ctx, 126992, 3, 255, payload)

// Explicit raw selection also remains available.
client, err := n2k.NewClient(ctx,
    n2k.TCP("10.0.0.5:2000", n2k.FormatActisenseRaw))
Source support by platform
Source Linux macOS Windows Write access
CAN (SocketCAN)
USB (serial CAN adapter)
TCP (Yacht Devices RAW) ✅ full frame-level control
ActisenseTCP ✅ requires BST-95 raw mode for NewClient
ActisenseSerial ✅ requires BST-95 raw mode for NewClient
Explicit Actisense message/N2K ASCII format ❌ read-only; use ActisenseGatewaySession to send
Explicit Actisense raw format ✅ full BST-95 frame-level control
Explicit Actisense CAN ASCII format ✅ source-authoritative mode-6 control
UDP (gateway formats) ❌ read-only
File (candump -L/-l) ❌ read-only
EBL (Actisense capture) ❌ read-only
Replay (in-memory frames) ✅ writes captured via WrittenFrames
Actisense sessions and device control

BST-94 does not carry a source address, so the library never exposes it as a Client Bus. Use a gateway session for assembled sends and local control:

session, err := n2k.NewActisenseTCPSession(ctx, "10.0.0.5:2000")
if err != nil { ... }
defer session.Close()

product, err := session.GetProductInfo(ctx)
diagnostics := session.Diagnostics()
status := session.Status()

The session exposes the complete compiled NMEA 2000 BEM command set, including product/CAN/port information, Rx/Tx PGN controls, multi-reply lists, explicit commit/reinitialize methods, EBL wire trace, and cumulative metrics. Sends do not mutate Tx lists. ConfigureTransmitPGNs provides an explicit batched, rollback-capable volatile transaction.

With a source-authoritative raw Client, the same typed command Interface can target another Actisense device through addressed PGN 126720:

remote, err := client.ActisenseRemoteDevice(35)
product, err := remote.GetProductInfo(ctx)

See Actisense NMEA 2000 support for the command ledger, serial settings, persistence rules, formats, model caveats, and parity evidence.

Filter Messages using Common Expression Language

Filter messages using CEL expressions.

n2k automatically optimizes filters — metadata-only expressions skip decoding entirely.

// Only vessel heading messages
for msg, err := range n2k.Receive(ctx,
    n2k.CAN("can0"),
    n2k.Filter("pgn == 127250"),
) { ... }

// Filter on decoded fields -- decoded numeric fields hold raw wire ticks,
// not physical units (see "Physical Values" below); Heading is in
// 0.0001-radian ticks, so 31416 here is > pi rad.
for msg, err := range n2k.Receive(ctx,
    n2k.CAN("can0"),
    n2k.Filter("pgn == 127250 && msg.Heading > 31416"),
) { ... }

// Filter by source address
for msg, err := range n2k.Receive(ctx,
    n2k.CAN("can0"),
    n2k.Filter("source == 3"),
) { ... }

Filter variables:

Variable Type Description
pgn int Parameter Group Number
source int Source address (0-251)
priority int Message priority (0-7)
destination int Destination address (255 = broadcast)
msg.<field> varies Decoded struct field (case-insensitive), in raw wire ticks

Repeating-group slice fields (Repeating1/Repeating2) are not addressable in filter expressions.

Options
Option Description
n2k.CAN(iface) SocketCAN source (e.g., "can0")
n2k.USB(port) USB-CAN serial source (e.g., "/dev/ttyUSB0")
n2k.ActisenseSerial(port, ...opts) Role-aware Actisense serial source; passive reads, source-authoritative raw NewClient; configurable baud/data/parity/stop bits
n2k.Serial(port, format, ...opts) Direct Actisense serial source; binary message, BST-95 raw, CAN ASCII, or N2K ASCII
n2k.SerialDevices() Enumerate host serial ports with available USB identity fields
n2k.File(path, ...opts) candump -L/-l log file source (read-only); n2k.OriginalTiming() paces frames by log timestamps
n2k.EBL(path, ...opts) Actisense Enhanced Binary Log source (read-only); supports raw BDTP and BSTRawFrame records
n2k.NewEBLWriter(w, ...opts) SDK-compatible EBL writer used directly or by an ActisenseEBLTrace
n2k.ActisenseTCP(addr) Role-aware Actisense TCP source; passive reads, source-authoritative raw NewClient
n2k.NewActisenseTCPSession(ctx, addr, ...opts) Gateway-owned BST-93/94 sends, typed local BEM, diagnostics, metrics, and optional reconnect/trace
n2k.TCP(addr, format) Network gateway over TCP; supports Yacht Devices RAW and Actisense binary/raw/CAN-ASCII/N2K-ASCII formats
n2k.UDP(listenAddr, format) Network gateway datagrams (read-only), same formats; raw Actisense requires an upstream BST-95 configuration
n2k.Replay(frames) Replay source for testing
n2k.Filter(expr) CEL filter expression
n2k.IncludeUnknown() Include undecodable messages as *pgn.UnknownPGN
n2k.WithLogger(l) Override default slog.Logger
n2k.WithSourceAddress(addr) Explicit source address for writes (contention is fatal)
n2k.WithPreferredAddress(addr) Starting address for automatic claiming; use a persisted prior address
n2k.WithClaimTimeout(d) Initial address-claim deadline; default 1500ms
n2k.WithName(name) ISO 11783 device NAME for address claiming
n2k.WithProductInfo(p) Product identity reported via PGN 126996
n2k.WithConfigInfo(ci) Installation description reported via PGN 126998
n2k.WithHeartbeatInterval(d) Heartbeat (PGN 126993) cadence; default 60s, 0 disables
n2k.WithReceiveBuffer(n) Per-subscription live receive buffer; default 64
n2k.WithWriteQueue(n) Pending asynchronous write capacity; default 64
n2k.WithReconnect(policy) Auto-reconnect dropped TCP gateway connections with exponential backoff (ReconnectPolicy{InitialBackoff, MaxBackoff}; zero values default to 500ms/30s)
n2k.WithBus(bus) Inject a pre-constructed n2k.Bus (custom transport or test fake) instead of CAN/USB sources
Errors, backpressure, and health

Runtime failure is surfaced through the API. A bus disconnect ends live iterators/scanners with that error, causes later writes to fail, and appears in Client.Err() and Client.Status().

An Actisense operating-mode failure is available to errors.As without losing the underlying timeout or device error. This lets an application deliberately offer a legacy configuration instead of receiving a silent downgrade:

var modeErr *n2k.ActisenseModeError
if errors.As(err, &modeErr) && modeErr.RequestedMode == 5 {
    // Raw Client semantics are unavailable; choose a gateway session only if
    // the application can accept the physical gateway's source identity.
}

Queues are deliberately bounded:

  • Write never blocks on admission. When its queue is full, the returned result is already complete with ErrWriteQueueFull.
  • Each live Receive or Scanner call has an independent subscription. A slow subscription ends with ErrReceiveOverflow; it cannot stall address claiming, transport handling, or another reader.
  • Each raw observation subscription is independently bounded and ends with ErrObservationOverflow when slow.
  • Required automatic protocol traffic uses a dedicated priority queue. Queue, encoding, and transport failures become terminal state instead of logs.

Use WaitContext when a caller has a deadline, tune bounds with WithWriteQueue / WithReceiveBuffer, and expose Status() from health or metrics endpoints:

status := client.Status()
fmt.Println(status.Address, status.AddressClaimed, status.Connected,
    status.ConnectionEpoch, status.WriteQueueDepth,
    status.ProtocolRequiredQueueDepth, status.ReceiveSubscribers,
    status.ObservationSubscribers, status.TerminalError)
Custom transports

WithBus is the extension seam for hardware and gateways not included here. Implement Bus for frame-level read/write access. Optionally implement ObservationBus to retain transport context, ReadyBus when opening is asynchronous, ConnectionLifecycleBus for reconnect epochs, and MessageWriter when the gateway accepts assembled PGN payloads instead of CAN frames. The client still owns claiming, protocol routing, scheduling, and shutdown.

A Complete Bus Node

Beyond claiming an address, a bus client implements the protocol behavior NMEA 2000 requires of a transmitting device:

  • Heartbeat (PGN 126993) — sent every 60 seconds automatically (tune or disable with WithHeartbeatInterval).
  • Product & configuration info (PGNs 126996/126998) — requests from other devices (chartplotters, analyzers) are answered automatically. Set your identity with WithProductInfo / WithConfigInfo; without them a generic software-gateway identity is reported so the device never shows up blank.
  • ISO requests (PGN 59904) — requests for supported PGNs are answered; requests addressed to us for anything else are refused with an ISO acknowledgement NAK, per ISO 11783-3.
  • Commanded Address (PGN 65240) — a matching nine-byte BAM assignment moves the node to a claimable address, immediately reclaims it, and opens a fresh contention window. Malformed and non-target commands are ignored.
  • Group functions (PGN 126208) — request group functions can transmit, retime, pause (interval 0), or restore (interval 0xFFFFFFFE) any PGN the client transmits, including scheduled broadcasts. Unsupported group functions (commands, read/write fields) are acknowledged with the proper error codes.

All of this runs on a dedicated decode path, so a Filter(...) expression never breaks protocol behavior.

Request/response — ask another device for a PGN and await its typed reply (default timeout 1250ms, the ISO 11783 response time):

pi, err := n2k.Request[*pgn.ProductInformation](ctx, client, 0x23)
if err == nil {
    fmt.Println(pi.ModelId, pi.SoftwareVersionCode)
}

Periodic broadcasts — transmit a PGN on a schedule; the provider runs on every tick (return nil to skip a tick). Prefer BroadcastPGN, which registers the PGN immediately for deterministic replacement and group-function retiming:

stop := client.BroadcastPGN(127250, time.Second, func() pgn.Message {
    heading := &pgn.VesselHeading{}
    heading.SetHeadingValue(currentHeadingRadians())
    return heading
})
defer stop()

Other devices can retime or pause the broadcast with a request group function naming its PGN. Providers run outside the scheduler goroutine; panic is contained and Close is not held hostage by a blocked provider. Providers should still return promptly because Go cannot forcibly terminate a callback.

Device Registry

Bus clients passively map the network: every address claim, product info, and configuration info message updates a registry keyed by 64-bit NAME (addresses are dynamic — the NAME is the stable identity). On first sight of a device the client requests its product and configuration info, and at startup it broadcasts an address-claim request so the whole bus announces itself.

for _, d := range client.Devices() {
    model := "(no product info yet)"
    if d.ProductInfo != nil {
        model = d.ProductInfo.ModelId
    }
    fmt.Printf("addr %d: %s (manufacturer %d, last seen %s)\n",
        d.Address, model, d.Name.ManufacturerCode, d.LastSeen)
}

// Correlate a decoded message with the device that sent it.
if vh, ok := msg.(*pgn.VesselHeading); ok {
    if dev, found := client.DeviceAt(vh.Info.SourceId); found {
        fmt.Printf("heading from %016X\n", dev.RawName)
    }
}
Testing with Replay
frames := []can.Frame{
    {ID: 0x09F10D01, Length: 8, Data: [8]uint8{1, 2, 3, 4, 5, 6, 7, 8}},
}

for msg, err := range n2k.Receive(ctx, n2k.Replay(frames)) {
    // test your message handling
}

PGN Types

All decoded messages implement the pgn.Message interface and are pointers to typed structs in the pgn package. Use a type switch to handle specific message types. PGN structs are organized across category files — system.go, navigation.go, engine.go, etc.

type Message interface {
    PGNNumber() uint32
}

Every struct carries a pgn.MessageInfo field with wire metadata:

type MessageInfo struct {
    Timestamp time.Time
    Priority  *uint8
    PGN       uint32
    SourceId  uint8
    TargetId  *uint8
}

When writing, Priority and TargetId default to 6 and 255 respectively when nil. When reading, they are populated from the wire. Use the helpers pgn.Priority(v) and pgn.Target(v) for concise literal construction:

info := pgn.MessageInfo{
    PGN:      126996,
    SourceId:  3,
    Priority: pgn.Priority(2),
    TargetId: pgn.Target(42),
}

Physical Values

Numeric struct fields hold raw wire ticks (*uint64/*int64). Exact round trips are preserved by an owned original-payload snapshot; raw ticks make field edits precise and predictable. Every numeric field with a physical interpretation also gets generated typed accessors that do the unit math — SI units in, SI units out, raw ticks underneath:

heading := &pgn.VesselHeading{}
heading.SetHeadingValue(1.5708)      // radians -> stored as 15708 raw ticks

rad, ok := heading.HeadingValue()    // 1.5708, true
_ = heading.Heading                  // *uint64 raw ticks, still there

depth := decoded.(*pgn.WaterDepth)
meters, ok := depth.DepthValue()     // e.g. 2.70 m from raw 270

The accessor's bool is false when the field is nil — the wire sent the field's null/out-of-range sentinel, or the payload ended before reaching it. Each accessor documents its unit and conversion (value = raw * resolution + offset); the units are the schema's SI units (rad, m/s, K, V, ...).

For dynamic, metadata-driven access — when the field is only known at runtime — pgn.PhysicalValue(msg, fieldOrder) performs the same conversion by field source order and also returns the unit label:

v, unit, ok, err := pgn.PhysicalValue(heading, 2) // field order 2 = Heading
// v = 1.5708, unit = "rad"

PhysicalValue returns an error for unknown fields, non-numeric fields (strings, binary, FLOAT, match selectors), and fields inside repeating groups — for those, decode the group slice and use the element structs' accessors instead (they're generated too).

Unit Types

The units package provides type-safe quantity wrappers (Distance, Velocity, Pressure, ...) with built-in unit conversion. It is independent of the PGN decode path: decoded structs keep raw wire ticks, and the generated accessors above return plain float64 values in each field's native schema unit. Wrapping those values into units types is left to the caller; wiring units directly into decoding would require changing generated PGN struct fields from raw-tick *uint64/*int64 values to quantity types, which is deliberately deferred (see the units package doc comment).

Conformance Status

The public, reproducible protocol evidence is run with just conformance-local and in Linux CI. Its controlled baseline is ISO 11783-3:2026, edition 5 for transport, ISO 11783-5:2019, edition 3 for address management, and NMEA 2000 Version 3.000 with amendments for the marine application and certification process. The test-to-behavior matrix is in the conformance guide.

This evidence is not formal NMEA certification. The official licensed tool has not been run against this repository alone: certification requires the actual product hardware and firmware, assigned manufacturer and product codes, the licensed tool, its encrypted result, and NMEA validation. The tracked evidence template reports those external steps as not-run until real records are supplied; the project does not infer or simulate a pass.

Comparison

Reviewed 2026-07-18, these are relevant projects that document NMEA 2000 support. Go projects come first because n2k is for Go applications; non-Go projects follow to show the broader ecosystem. NMEA 0183-only parsers are excluded.

Legend: ✅ first-class documented support · 🟡 partial, lower-level, or workflow-specific · ❓ capability may exist but isn't documented · ❌ not present or not applicable.

Project Lang Go app API Typed PGNs Byte-preserving encode Bus-node behavior Group functions Best fit
open-ships/n2k Go ✅ direct top-level API ✅ ~600 generated structs ✅ tested round trips ✅ claim, heartbeat, info, requests ✅ transmit / retime / pause Go production apps, gateways, loggers, automation
boatkit-io/n2k Go ✅ endpoint/service/node packages ✅ generated structs 🟡 documented encode/write ✅ node claim + heartbeat ❓ not documented Go apps built around its service/node architecture
aldas/go-nmea-client Go 🟡 lower-level reader APIs ❌ no typed structs 🟡 raw/message oriented 🟡 basic node mapping ❓ not documented Raw ingestion and format conversion
canboat/canboat C ❌ not Go ❌ analyzer output only 🟡 tooling-oriented ❌ analysis suite ❌ not a bus-node library Shell analysis, reverse engineering, schema reference
ttlappalainen/NMEA2000 C++ ❌ not Go 🟡 hand-authored helpers 🟡 message helpers ✅ mandatory device behavior 🟡 device-oriented support Embedded C++ NMEA 2000 devices
canboat/canboatjs TypeScript ❌ not Go 🟡 TypeScript definitions ✅ documented encode/transmit 🟡 device integration 🟡 plugin/provider-specific Node.js, Signal K, TypeScript integrations
tomer-w/nmea2000 Python ❌ not Go 🟡 generated Python fields ✅ generated encode/decode ❓ not documented ❓ not documented Python automation and Home Assistant
fard-draf/korri-n2k Rust ❌ not Go ✅ generated structs 🟡 generated serialization ✅ address claiming + fast packet ❌ roadmap gap Embedded Rust with selectable PGN sets

For a Go application, open-ships/n2k is designed to cover the full path in one API: typed decoding, byte-preserving writes, bus-node behavior, group functions, and gateway/file sources. Evaluate the details that matter to your deployment rather than relying on the summary table alone.

Known Limitations

  • The codec implements the schema's current PGNIsProprietary field condition; a future schema condition expression must be added explicitly and otherwise fails closed during codec-plan compilation.
  • Formal NMEA certification has not been run. Products that transmit on a real vessel network must follow the hardware/tool workflow in Protocol conformance before making a certification claim.
  • One physical bus per client.
  • Address claiming uses a 1500ms default timeout; on heavily contested buses, increase via WithClaimTimeout.
  • File, EBL, and UDP sources are read-only; writing requires CAN, USB, Serial, TCP, or Replay.
  • FormatActisense and FormatActisenseN2KASCII are gateway-owned read sources and are rejected by NewClient. Use ActisenseGatewaySession for sends under the physical gateway identity, or BST-95/CAN ASCII for a true source-authoritative Client.
  • Mode 5 is not available on NGT-class hardware. Raw mode does not silently fall back; an unsupported or unacknowledged BEM setup fails NewClient startup. Such hardware can still be read passively or used through an ActisenseGatewaySession, without virtual-node claims.
  • Gateway TCP connections do not auto-reconnect by default; a dropped connection ends the read loop. Pass WithReconnect to re-dial dropped connections with backoff. Each new connection epoch reclaims the address, clears stale registry topology, waits through contention, re-enumerates, and resumes scheduled traffic.
  • Live delivery is bounded by design. A reader that cannot keep up receives ErrReceiveOverflow or ErrObservationOverflow; increase WithReceiveBuffer or consume faster.

License

MIT — see LICENSE.

Development and security

See CONTEXT.md for architecture vocabulary and invariants, the architecture guide for runtime ownership, the conformance guide for standards evidence, CONTRIBUTING.md for required checks, and SECURITY.md for private vulnerability reporting.

Acknowledgments

  • canboat — maintains the open PGN schema this library's decoders are generated from. The decoded-message breadth here is theirs.
  • boatkit-io/n2k — the Go project that inspired this one.

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

Examples

Constants

View Source
const (
	ActisenseModeUndefined          = actisense.ModeUndefined
	ActisenseModeTransferNormal     = actisense.ModeTransferNormal
	ActisenseModeTransferReceiveAll = actisense.ModeTransferReceiveAll
	ActisenseModeTransferLegacyRaw  = actisense.ModeTransferLegacyRaw
	ActisenseModeConvertNormal      = actisense.ModeConvertNormal
	ActisenseModeCANPacket          = actisense.ModeCANPacket
	ActisenseModeCANPacketASCII     = actisense.ModeCANPacketASCII
	ActisenseModeBuffer1            = actisense.ModeBuffer1
	ActisenseModeBuffer2            = actisense.ModeBuffer2
	ActisenseModeBuffer3            = actisense.ModeBuffer3
	ActisenseModeAutoswitchDirect   = actisense.ModeAutoswitchDirect
	ActisenseModeAutoswitchSmart    = actisense.ModeAutoswitchSmart
	ActisenseModeCombineSlow        = actisense.ModeCombineSlow
	ActisenseModeCombineFast        = actisense.ModeCombineFast
	ActisenseModeTest1              = actisense.ModeTest1
	ActisenseModeNSI1               = actisense.ModeNSI1
	ActisenseModeNormal             = actisense.ModeNormal
	ActisenseModePredefined1        = actisense.ModePredefined1
	ActisenseModePredefined2        = actisense.ModePredefined2
	ActisenseModeUser1              = actisense.ModeUser1
	ActisenseModeUser2              = actisense.ModeUser2
	ActisenseModeUser3              = actisense.ModeUser3
	ActisenseModeUser4              = actisense.ModeUser4
	ActisenseModeUser5              = actisense.ModeUser5
	ActisenseModeNull               = actisense.ModeNull
)
View Source
const (
	ActisenseBEMLocal  = actisense.BEMPathLocal
	ActisenseBEMRemote = actisense.BEMPathRemote
)
View Source
const (
	ActisenseModelUnknown = actisense.ModelUnknown
	ActisenseModelNGT1    = actisense.ModelNGT1
	ActisenseModelNGT1USB = actisense.ModelNGT1USB
	ActisenseModelNGW1    = actisense.ModelNGW1
	ActisenseModelEMU1    = actisense.ModelEMU1
	ActisenseModelPRONDC1 = actisense.ModelPRONDC1
	ActisenseModelWGX1    = actisense.ModelWGX1
	ActisenseModelNGX1    = actisense.ModelNGX1
)
View Source
const (
	ActisenseHardwareSerialNMEA0183   = actisense.HardwareSerialNMEA0183
	ActisenseHardwareSerialBST        = actisense.HardwareSerialBST
	ActisenseHardwareCANNMEA2000      = actisense.HardwareCANNMEA2000
	ActisenseHardwareCANJ1939         = actisense.HardwareCANJ1939
	ActisenseHardwareEthernetBST      = actisense.HardwareEthernetBST
	ActisenseHardwareEthernetNMEA0183 = actisense.HardwareEthernetNMEA0183
	ActisenseHardwareEthernetOneNet   = actisense.HardwareEthernetOneNet

	ActisensePortMediaCAN      = actisense.PortMediaCAN
	ActisensePortMediaUART     = actisense.PortMediaUART
	ActisensePortMediaUSB      = actisense.PortMediaUSB
	ActisensePortMediaBLE      = actisense.PortMediaBLE
	ActisensePortMediaWiFi     = actisense.PortMediaWiFi
	ActisensePortMediaEthernet = actisense.PortMediaEthernet
	ActisensePortMediaIPStream = actisense.PortMediaIPStream
	ActisensePortMediaUnknown  = actisense.PortMediaUnknown
	ActisensePortIndexNone     = actisense.PortIndexNone

	ActisenseBaudRateNoChange       = actisense.BaudRateNoChange
	ActisenseBaudRateDefault        = actisense.BaudRateDefault
	ActisenseBaudRateAdoptAlternate = actisense.BaudRateAdoptAlternate

	ActisensePortPCodeOff      = actisense.PortPCodeOff
	ActisensePortPCodeOn       = actisense.PortPCodeOn
	ActisensePortPCodeNoChange = actisense.PortPCodeNoChange

	ActisenseCANInfoInstallationDescription1 = actisense.CANInfoInstallationDescription1
	ActisenseCANInfoInstallationDescription2 = actisense.CANInfoInstallationDescription2
	ActisenseCANInfoManufacturerInformation  = actisense.CANInfoManufacturerInformation
)
View Source
const (
	ActisensePGNDisabled        = actisense.PGNDisabled
	ActisensePGNEnabled         = actisense.PGNEnabled
	ActisensePGNRespondMode     = actisense.PGNRespondMode
	ActisenseRxPGNMaskAcceptAll = actisense.RxPGNMaskAcceptAll
	ActisenseTxPGNRateDefault   = actisense.TxPGNRateDefault
	ActisenseTxPGNRateEvent     = actisense.TxPGNRateEvent
	ActisensePGNListRx          = actisense.PGNListRx
	ActisensePGNListTx          = actisense.PGNListTx
	ActisensePGNListBoth        = actisense.PGNListBoth
)
View Source
const (
	ActisenseDiagnosticStartup     = actisense.DiagnosticStartup
	ActisenseDiagnosticError       = actisense.DiagnosticError
	ActisenseDiagnosticSystem      = actisense.DiagnosticSystem
	ActisenseDiagnosticNegativeAck = actisense.DiagnosticNegativeAck
)
View Source
const (
	ObservationFrame          = raw.KindFrame
	ObservationMessage        = raw.KindMessage
	ObservationDecodeError    = raw.KindDecodeError
	ObservationGateway        = raw.KindGateway
	ObservationTransportError = raw.KindTransportError

	DirectionUnknown     = raw.DirectionUnknown
	DirectionReceived    = raw.DirectionReceived
	DirectionTransmitted = raw.DirectionTransmitted
)

Variables

View Source
var (
	// ErrWriteQueueFull reports that an asynchronous write could not be admitted
	// without blocking the caller. Callers may retry or apply their own policy.
	ErrWriteQueueFull = errors.New("n2k: write queue full")
	// ErrClientClosed reports an operation attempted after Client.Close.
	ErrClientClosed = errors.New("n2k: client closed")
)
View Source
var ErrActisenseGatewaySessionRequired = errors.New("n2k: gateway-owned Actisense message formats cannot back Client; use NewActisenseTCPSession/NewActisenseSerialSession or a source-authoritative raw CAN format")

ErrActisenseGatewaySessionRequired reports an attempt to run a source- authoritative Client over gateway-owned BST-93/94 traffic. Use a public ActisenseGatewaySession for legacy message sends, or BST-95 raw mode for a Client with its own NMEA 2000 identity.

View Source
var ErrActisenseRemoteEpochChanged = errors.New("n2k: Actisense remote request canceled because the local address or connection epoch changed")
View Source
var ErrObservationOverflow = errors.New("n2k: observation buffer overflow")

ErrObservationOverflow reports that an observation subscriber could not keep up. Only that subscriber is closed; protocol processing continues.

View Source
var ErrProtocolQueueFull = errors.New("n2k: protocol write queue full")

ErrProtocolQueueFull reports exhaustion of a bounded automatic-protocol transmission lane.

View Source
var ErrReceiveOverflow = errors.New("n2k: receive buffer overflow")

ErrReceiveOverflow reports that a live Client subscriber could not keep up with bus traffic. The subscriber is closed rather than allowing application backpressure to stall address claiming and other protocol processing.

Functions

func EncodeActisenseCANASCII added in v1.2.0

func EncodeActisenseCANASCII(frame can.Frame, direction Direction, transportTimestamp time.Duration) ([]byte, error)

EncodeActisenseCANASCII renders one CAN frame in Actisense's documented RAW ASCII representation.

func EncodeActisenseN2KASCII added in v1.2.0

func EncodeActisenseN2KASCII(pgn uint32, priority, source, destination uint8, payload []byte, transportTimestamp time.Duration) ([]byte, error)

EncodeActisenseN2KASCII renders one assembled NMEA 2000 message in the documented Type-A representation.

func Observe added in v1.0.0

func Observe(ctx context.Context, opts ...Option) iter.Seq2[Observation, error]

Observe returns a bounded iterator of owned transport observations from the configured read-only sources. Its bounded channel applies backpressure to the source, which makes file capture and replay lossless without unbounded memory growth. Use Client.Observations for a writable Client; that live protocol path fails only the slow subscriber with ErrObservationOverflow.

func Receive

func Receive(ctx context.Context, opts ...Option) iter.Seq2[pgn.Message, error]

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

func Request[T pgn.Message](ctx context.Context, c *Client, target uint8) (T, error)

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 ActisenseBEMOrigin added in v1.2.0

type ActisenseBEMOrigin = actisense.BEMOrigin

type ActisenseBEMPath added in v1.2.0

type ActisenseBEMPath = actisense.BEMPath

type ActisenseBEMResponse added in v1.2.0

type ActisenseBEMResponse = actisense.BEMResponse

type ActisenseByteStream added in v1.2.0

type ActisenseByteStream interface {
	io.Reader
	io.Writer
	io.Closer
}

ActisenseByteStream is a full-duplex byte transport for one Actisense connection epoch. Close must unblock Read and Write.

type ActisenseCANConfig added in v1.2.0

type ActisenseCANConfig = actisense.CANConfig

type ActisenseCANInfoField added in v1.2.0

type ActisenseCANInfoField = actisense.CANInfoField

type ActisenseDevice added in v1.2.0

type ActisenseDevice struct {
	*actisense.CommandSet
}

ActisenseDevice exposes the same typed command Interface for a locally attached gateway and a remote Actisense device.

type ActisenseDeviceCapabilities added in v1.2.0

type ActisenseDeviceCapabilities = actisense.DeviceCapabilities

type ActisenseDeviceError added in v1.2.0

type ActisenseDeviceError = actisense.DeviceError

type ActisenseDiagnostic added in v1.2.0

type ActisenseDiagnostic = actisense.Diagnostic

type ActisenseDiagnosticKind added in v1.2.0

type ActisenseDiagnosticKind = actisense.DiagnosticKind

type ActisenseEBLTrace added in v1.2.0

type ActisenseEBLTrace struct {
	// contains filtered or unexported fields
}

ActisenseEBLTrace converts raw transport reads/writes into the SDK's EBL wire-trace representation. Valid received frames become checksum-stripped BSTRawFrame records; invalid or unframed bytes remain exact raw evidence.

func NewActisenseEBLTrace added in v1.2.0

func NewActisenseEBLTrace(writer *EBLWriter) (*ActisenseEBLTrace, error)

func (*ActisenseEBLTrace) Err added in v1.2.0

func (t *ActisenseEBLTrace) Err() error

func (*ActisenseEBLTrace) Flush added in v1.2.0

func (t *ActisenseEBLTrace) Flush() error

type ActisenseErrorReport added in v1.2.0

type ActisenseErrorReport = actisense.ErrorReport

type ActisenseGatewaySession added in v1.2.0

type ActisenseGatewaySession struct {
	*ActisenseDevice
	// contains filtered or unexported fields
}

ActisenseGatewaySession owns a gateway identity, a sole-reader BEM session, observations, typed diagnostics, and explicit PGN-list transactions. It is deliberately not a Bus and does not run address claiming.

func NewActisenseGatewaySession added in v1.2.0

func NewActisenseGatewaySession(ctx context.Context, endpoint string, open ActisenseOpenFunc, options ...ActisenseSessionOption) (*ActisenseGatewaySession, error)

NewActisenseGatewaySession opens a gateway-owned session over a custom full-duplex byte-stream Adapter.

func NewActisenseSerialSession added in v1.2.0

func NewActisenseSerialSession(ctx context.Context, port string, serialConfig ActisenseSerialConfig, options ...ActisenseSessionOption) (*ActisenseGatewaySession, error)

NewActisenseSerialSession opens a gateway-owned session with configurable serial settings. The zero serial config defaults to 115200 8N1.

func NewActisenseTCPSession added in v1.2.0

func NewActisenseTCPSession(ctx context.Context, address string, options ...ActisenseSessionOption) (*ActisenseGatewaySession, error)

NewActisenseTCPSession opens an honest gateway-owned BST-93/94 and BEM session over TCP.

func (*ActisenseGatewaySession) Close added in v1.2.0

func (s *ActisenseGatewaySession) Close() error

func (*ActisenseGatewaySession) ConfigureTransmitPGNs added in v1.2.0

func (s *ActisenseGatewaySession) ConfigureTransmitPGNs(ctx context.Context, configurations []ActisenseTxPGNConfiguration) error

ConfigureTransmitPGNs snapshots every affected session entry, stages all changes, then activates once. Any failure restores the staged entries. The first snapshot for an epoch is best-effort restored by Close.

func (*ActisenseGatewaySession) Diagnostics added in v1.2.0

func (*ActisenseGatewaySession) Err added in v1.2.0

func (s *ActisenseGatewaySession) Err() error

func (*ActisenseGatewaySession) Observations added in v1.2.0

func (s *ActisenseGatewaySession) Observations() iter.Seq2[Observation, error]

func (*ActisenseGatewaySession) SendPGN added in v1.2.0

func (s *ActisenseGatewaySession) SendPGN(ctx context.Context, message pgn.Message) error

SendPGN transmits one assembled PGN under the gateway's own claimed source address. It never enables or activates a Tx list implicitly.

func (*ActisenseGatewaySession) SendRawPGN added in v1.2.0

func (s *ActisenseGatewaySession) SendRawPGN(ctx context.Context, pgnNumber uint32, priority, destination uint8, payload []byte) error

func (*ActisenseGatewaySession) Status added in v1.2.0

type ActisenseHardwareProtocol added in v1.2.0

type ActisenseHardwareProtocol = actisense.HardwareProtocol

type ActisenseModeError added in v1.1.0

type ActisenseModeError struct {
	RequestedMode uint16
	Err           error
}

ActisenseModeError reports that an Actisense gateway did not acknowledge a requested operating mode. RequestedMode is the Actisense wire value; raw CAN mode is 5. The wrapped error retains timeout, disconnect, negative acknowledgement, or device-error details.

func (*ActisenseModeError) Error added in v1.1.0

func (e *ActisenseModeError) Error() string

func (*ActisenseModeError) Unwrap added in v1.1.0

func (e *ActisenseModeError) Unwrap() error

Unwrap returns the underlying gateway setup error.

type ActisenseModelID added in v1.2.0

type ActisenseModelID = actisense.ModelID

type ActisenseNegativeAck added in v1.2.0

type ActisenseNegativeAck = actisense.NegativeAck

type ActisenseNegativeAckError added in v1.2.0

type ActisenseNegativeAckError = actisense.NegativeAckError

type ActisenseOpenFunc added in v1.2.0

type ActisenseOpenFunc func(context.Context) (ActisenseByteStream, error)

ActisenseOpenFunc opens one custom Actisense byte-stream connection epoch. It may be called again when reconnection is configured.

type ActisenseOperatingMode added in v1.2.0

type ActisenseOperatingMode = actisense.OperatingMode

type ActisensePGNEnableFlag added in v1.2.0

type ActisensePGNEnableFlag = actisense.PGNEnableFlag

type ActisensePGNListParameters added in v1.2.0

type ActisensePGNListParameters = actisense.PGNListParameters

type ActisensePGNListSelector added in v1.2.0

type ActisensePGNListSelector = actisense.PGNListSelector

type ActisenseParity added in v1.2.0

type ActisenseParity uint8

ActisenseParity configures parity on a directly connected Actisense serial Adapter without exposing the implementation's serial library.

const (
	ActisenseParityNone ActisenseParity = iota
	ActisenseParityOdd
	ActisenseParityEven
	ActisenseParityMark
	ActisenseParitySpace
)

type ActisensePortBaudrate added in v1.2.0

type ActisensePortBaudrate = actisense.PortBaudrate

type ActisensePortInventory added in v1.2.0

type ActisensePortInventory = actisense.PortInventory

type ActisensePortInventoryEntry added in v1.2.0

type ActisensePortInventoryEntry = actisense.PortInventoryEntry

type ActisensePortMedia added in v1.2.0

type ActisensePortMedia = actisense.PortMedia

type ActisensePortPCode added in v1.2.0

type ActisensePortPCode = actisense.PortPCode

type ActisenseProductInfo added in v1.2.0

type ActisenseProductInfo = actisense.ProductInfo

type ActisenseProprietaryPGNList added in v1.2.0

type ActisenseProprietaryPGNList = actisense.ProprietaryPGNList

type ActisenseProtocolMetrics added in v1.2.0

type ActisenseProtocolMetrics = actisense.SessionMetrics

type ActisenseRemoteDevice added in v1.2.0

type ActisenseRemoteDevice struct {
	*ActisenseDevice
	SourceAddress uint8
	// contains filtered or unexported fields
}

ActisenseRemoteDevice issues the same typed commands as a local gateway through Actisense's addressed PGN-126720 envelope.

func (*ActisenseRemoteDevice) Diagnostics added in v1.2.0

func (*ActisenseRemoteDevice) Metrics added in v1.2.0

Metrics returns cumulative BEM correlation and latency counters shared by all remote-device handles on this Client.

type ActisenseRemoteOption added in v1.2.0

type ActisenseRemoteOption interface {
	// contains filtered or unexported methods
}

ActisenseRemoteOption configures a remote-device handle.

func WithActisenseRemoteMultiReplyInactivity added in v1.2.0

func WithActisenseRemoteMultiReplyInactivity(timeout time.Duration) ActisenseRemoteOption

func WithActisenseRemoteTimeout added in v1.2.0

func WithActisenseRemoteTimeout(timeout time.Duration) ActisenseRemoteOption

type ActisenseRxPGNEnableList added in v1.2.0

type ActisenseRxPGNEnableList = actisense.RxPGNEnableList

type ActisenseRxPGNListEntry added in v1.2.0

type ActisenseRxPGNListEntry = actisense.RxPGNListEntry

type ActisenseRxPGNState added in v1.2.0

type ActisenseRxPGNState = actisense.RxPGNState

type ActisenseSerialConfig added in v1.2.0

type ActisenseSerialConfig struct {
	BaudRate int
	DataBits int
	Parity   ActisenseParity
	StopBits ActisenseStopBits
}

ActisenseSerialConfig is the complete host-side serial configuration. Its zero value selects the Actisense binary default of 115200 8N1.

type ActisenseSerialOption added in v1.2.0

type ActisenseSerialOption interface {
	// contains filtered or unexported methods
}

ActisenseSerialOption configures one direct serial Adapter.

func WithActisenseBaudRate added in v1.2.0

func WithActisenseBaudRate(baudRate int) ActisenseSerialOption

WithActisenseBaudRate changes only the host-side baud rate.

func WithActisenseSerialConfig added in v1.2.0

func WithActisenseSerialConfig(config ActisenseSerialConfig) ActisenseSerialOption

WithActisenseSerialConfig replaces the host-side serial configuration. Zero BaudRate or DataBits fields retain the 115200 8N1 defaults.

type ActisenseSessionMetrics added in v1.2.0

type ActisenseSessionMetrics struct {
	ConnectionEpochs uint64
	Reconnects       uint64
	GatewayResets    uint64
	Protocol         ActisenseProtocolMetrics
}

ActisenseSessionMetrics remains cumulative across reconnect epochs.

type ActisenseSessionOption added in v1.2.0

type ActisenseSessionOption interface {
	// contains filtered or unexported methods
}

ActisenseSessionOption configures a public gateway-owned session.

func WithActisenseCommandTimeout added in v1.2.0

func WithActisenseCommandTimeout(timeout time.Duration) ActisenseSessionOption

func WithActisenseMultiReplyInactivity added in v1.2.0

func WithActisenseMultiReplyInactivity(timeout time.Duration) ActisenseSessionOption

func WithActisenseSessionBuffer added in v1.2.0

func WithActisenseSessionBuffer(size int) ActisenseSessionOption

func WithActisenseSessionLogger added in v1.2.0

func WithActisenseSessionLogger(logger *slog.Logger) ActisenseSessionOption

func WithActisenseSessionMode added in v1.2.0

func WithActisenseSessionMode(mode ActisenseOperatingMode) ActisenseSessionOption

WithActisenseSessionMode selects gateway-owned BST message mode 1 or 2. Source-authoritative mode 5 belongs to NewClient via ActisenseTCP or ActisenseSerial and is intentionally rejected here.

func WithActisenseSessionReadyTimeout added in v1.2.0

func WithActisenseSessionReadyTimeout(timeout time.Duration) ActisenseSessionOption

func WithActisenseSessionReconnect added in v1.2.0

func WithActisenseSessionReconnect(policy ReconnectPolicy) ActisenseSessionOption

func WithActisenseWireTrace added in v1.2.0

func WithActisenseWireTrace(trace *ActisenseEBLTrace) ActisenseSessionOption

WithActisenseWireTrace records exact transmitted bytes and received wire evidence to an EBL trace. Valid received BDTP frames are stored in the checksum-stripped BSTRaw representation used by Actisense tooling.

type ActisenseSessionStatus added in v1.2.0

type ActisenseSessionStatus struct {
	Connected              bool
	ConnectionEpoch        uint64
	Closed                 bool
	TerminalError          error
	OperatingMode          ActisenseOperatingMode
	SourceAuthoritative    bool
	ReceiveAll             bool
	ISOControlPGNsVisible  bool
	ObservationSubscribers int
	DiagnosticSubscribers  int
	DeviceCapabilities     ActisenseDeviceCapabilities
	WireTraceError         error
	Metrics                ActisenseSessionMetrics
}

ActisenseSessionStatus honestly describes the gateway-owned connection. A session never claims an independent NMEA 2000 source identity.

type ActisenseStartupStatus added in v1.2.0

type ActisenseStartupStatus = actisense.StartupStatus

type ActisenseStopBits added in v1.2.0

type ActisenseStopBits uint8

ActisenseStopBits configures stop bits on a directly connected Actisense serial Adapter.

const (
	ActisenseStopBitsOne ActisenseStopBits = iota
	ActisenseStopBitsOnePointFive
	ActisenseStopBitsTwo
)

type ActisenseSupportedPGN added in v1.2.0

type ActisenseSupportedPGN = actisense.SupportedPGN

type ActisenseSupportedPGNList added in v1.2.0

type ActisenseSupportedPGNList = actisense.SupportedPGNList

type ActisenseSystemStatus added in v1.2.0

type ActisenseSystemStatus = actisense.SystemStatus

type ActisenseTxPGNConfiguration added in v1.2.0

type ActisenseTxPGNConfiguration struct {
	PGN  uint32
	Flag ActisensePGNEnableFlag
	Rate *uint32
}

type ActisenseTxPGNEnableList added in v1.2.0

type ActisenseTxPGNEnableList = actisense.TxPGNEnableList

type ActisenseTxPGNListEntry added in v1.2.0

type ActisenseTxPGNListEntry = actisense.TxPGNListEntry

type ActisenseTxPGNState added in v1.2.0

type ActisenseTxPGNState = actisense.TxPGNState

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. Client serializes
	// concurrent handler calls, though Bus implementations should normally
	// preserve wire order.
	Run(ctx context.Context, handler func(can.Frame)) error
	// WriteFrame sends one frame.
	WriteFrame(frame can.Frame) error
	// Close releases resources. It must be safe if Run was never called and
	// safe concurrently with Run or WriteFrame so cancellation can release
	// blocked device I/O.
	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

func NewClient(ctx context.Context, opts ...Option) (*Client, error)

NewClient creates a Client that can read and write NMEA 2000 messages. Provide CAN, USB, Serial, TCP, Replay, or WithBus for writable clients; File, EBL, 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)
	}
}

func (*Client) ActisenseRemoteDevice added in v1.2.0

func (c *Client) ActisenseRemoteDevice(source uint8, options ...ActisenseRemoteOption) (*ActisenseRemoteDevice, error)

ActisenseRemoteDevice returns a handle for one addressed Actisense device. Incoming replies must target this Client's source snapshot and are canceled across connection or address-claim epochs.

func (*Client) Broadcast

func (c *Client) Broadcast(interval time.Duration, provide func() pgn.Message) (stop func())

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.

The PGN is learned asynchronously from the first non-nil message. Until then, group-function lookup cannot find this schedule; BroadcastPGN avoids that delay. Scheduling the same PGN again replaces the earlier schedule. The returned stop function 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) BroadcastPGN added in v0.3.0

func (c *Client) BroadcastPGN(pgnNum uint32, interval time.Duration, provide func() pgn.Message) (stop func())

BroadcastPGN is Broadcast with an explicit PGN identity. Prefer it when a group-function peer must be able to retime the schedule immediately, before provide returns its first non-nil message. Declaring the PGN also makes replacement of an existing schedule deterministic without probing provide.

func (*Client) Close

func (c *Client) Close() error

Close shuts down the client, cancels the context, and releases resources. It is safe to call Close multiple times.

func (*Client) DeviceAt

func (c *Client) DeviceAt(source uint8) (Device, bool)

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

func (c *Client) Devices() []Device

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) Err added in v0.3.0

func (c *Client) Err() error

Err returns the terminal runtime error, if the bus or address-claiming lifecycle failed. A normal Close does not set an error.

func (*Client) Observations added in v1.0.0

func (c *Client) Observations() iter.Seq2[Observation, error]

Observations returns a bounded iterator of raw observations for a live client. Frame events are published before protocol handling; assembled message and decode-error events follow as the user pipeline processes them.

func (*Client) Receive

func (c *Client) Receive() iter.Seq2[pgn.Message, error]

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

func (c *Client) Scanner() *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) Status added in v0.3.0

func (c *Client) Status() ClientStatus

Status returns a concurrency-safe snapshot of the client's lifecycle and bounded queues. It does not perform I/O.

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

func (c *Client) WrittenFrames() []can.Frame

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 ClientStatus added in v0.3.0

type ClientStatus struct {
	Address                       uint8
	AddressClaimed                bool
	Connected                     bool
	ConnectionEpoch               uint64
	Rejoining                     bool
	Closed                        bool
	TerminalError                 error
	WriteQueueDepth               int
	WriteQueueCapacity            int
	ReceiveSubscribers            int
	ObservationSubscribers        int
	ProtocolRequiredQueueDepth    int
	ProtocolRequiredQueueCapacity int
	ProtocolAdvisoryQueueDepth    int
	ProtocolAdvisoryQueueCapacity int
	ApplicationWritesAccepted     uint64
	ApplicationWritesCompleted    uint64
	ApplicationWritesFailed       uint64
	ApplicationWritesRejected     uint64
	ProtocolWritesAccepted        uint64
	ProtocolWritesCompleted       uint64
	ProtocolWritesFailed          uint64
	ProtocolWritesRejected        uint64
	FramesReceived                uint64
	FramesTransmitted             uint64
	MessagesObserved              uint64
	DecodeErrorsObserved          uint64
	GatewayEventsObserved         uint64
	TransportErrorsObserved       uint64
	// Actisense is populated for source-authoritative Actisense binary or CAN
	// ASCII buses. Gateway-owned message sessions expose the same counters on
	// ActisenseGatewaySession.Status instead.
	Actisense *ActisenseSessionMetrics
}

ClientStatus is a point-in-time operational snapshot suitable for health endpoints and metrics collectors.

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 ConnectionLifecycleBus added in v1.0.0

type ConnectionLifecycleBus interface {
	SetConnectionObserver(observer func(connected bool, epoch uint64))
}

ConnectionLifecycleBus is optionally implemented by reconnecting buses. The observer is invoked synchronously whenever the underlying connection changes. On connect, it runs before the new connection becomes visible to writers, allowing Client to install a fresh transmission gate. Epoch starts at 1 and increases for every successful connection.

Implementations must not call the observer while holding locks that an ordinary WriteFrame needs. The observer must return promptly and must not perform bus I/O itself.

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 development-oriented DeviceName for a PC-based software gateway. Production devices should use WithName with the vendor's assigned manufacturer code and a stable, persisted identity number. The default identity is randomized so multiple development processes can coexist on one bus, but it is not a substitute for a provisioned product identity.

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 Direction added in v1.0.0

type Direction = raw.Direction

type EBLWriter added in v1.2.0

type EBLWriter struct {
	// contains filtered or unexported fields
}

EBLWriter writes interoperable Actisense Enhanced Binary Log records. It is safe for concurrent use and retains the first output error.

func NewEBLWriter added in v1.2.0

func NewEBLWriter(output io.Writer, options ...EBLWriterOption) (*EBLWriter, error)

func (*EBLWriter) Err added in v1.2.0

func (w *EBLWriter) Err() error

func (*EBLWriter) Metrics added in v1.2.0

func (w *EBLWriter) Metrics() EBLWriterMetrics

func (*EBLWriter) WriteDescription added in v1.2.0

func (w *EBLWriter) WriteDescription(description string) error

func (*EBLWriter) WriteRawBST added in v1.2.0

func (w *EBLWriter) WriteRawBST(timestamp time.Time, direction Direction, rawBST []byte) error

func (*EBLWriter) WriteRawBytes added in v1.2.0

func (w *EBLWriter) WriteRawBytes(timestamp time.Time, direction Direction, data []byte) error

type EBLWriterMetrics added in v1.2.0

type EBLWriterMetrics = ebl.WriterMetrics

type EBLWriterOption added in v1.2.0

type EBLWriterOption interface {
	// contains filtered or unexported methods
}

EBLWriterOption configures a new Enhanced Binary Log writer.

func WithEBLDescription added in v1.2.0

func WithEBLDescription(description string) EBLWriterOption

func WithEBLStartTime added in v1.2.0

func WithEBLStartTime(timestamp time.Time) EBLWriterOption

type FileOption

type FileOption interface {
	// contains filtered or unexported methods
}

FileOption configures a candump File or EBL capture source.

func OriginalTiming

func OriginalTiming() FileOption

OriginalTiming replays frames or messages paced by the capture's own timestamps instead of as fast as they can be read. Records 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 Observation added in v1.0.0

type Observation = raw.Observation

Public aliases keep the common observation surface in package n2k while allowing transport Adapter implementations to share the cycle-free raw package.

func ParseActisenseCANASCII added in v1.2.0

func ParseActisenseCANASCII(line string) (Observation, error)

ParseActisenseCANASCII parses one source-authoritative mode-6 CAN frame.

func ParseActisenseN2KASCII added in v1.2.0

func ParseActisenseN2KASCII(line string) (Observation, error)

ParseActisenseN2KASCII parses one gateway-assembled Type-A NMEA 2000 message.

type ObservationBus added in v1.0.0

type ObservationBus interface {
	RunObservations(ctx context.Context, handler func(raw.Observation)) error
}

ObservationBus is optionally implemented by buses that can preserve transport timestamps, direction, and source identity. Client prefers this Interface over Run and still accepts ordinary Bus implementations.

type ObservationKind added in v1.0.0

type ObservationKind = raw.Kind

type Option

type Option interface {
	// contains filtered or unexported methods
}

Option configures Receive, Observe, NewScanner, or NewClient.

func ActisenseSerial added in v1.1.0

func ActisenseSerial(port string, options ...ActisenseSerialOption) Option

ActisenseSerial adds a directly connected Actisense gateway at 115200 8N1. Receive, Observe, and NewScanner use the compatible gateway-owned message session. NewClient requires acknowledged mode 5 and uses source-authoritative BST-95 raw CAN; it fails rather than falling back when raw mode is unavailable. Use Serial with an explicit Actisense format to override this role-based policy.

func ActisenseTCP added in v1.1.0

func ActisenseTCP(addr string) Option

ActisenseTCP adds an Actisense gateway over TCP. Receive, Observe, and NewScanner passively decode all supported BST records without changing the gateway's operating mode. NewClient requires acknowledged mode 5 and uses source-authoritative BST-95 raw CAN; it fails rather than falling back when raw mode is unavailable. Use TCP with an explicit Actisense format to override this role-based policy.

func CAN

func CAN(iface string) Option

CAN adds a SocketCAN source for the given Linux CAN interface (e.g., "can0").

func EBL added in v1.1.0

func EBL(path string, opts ...FileOption) Option

EBL adds an Actisense Enhanced Binary Log capture source. Both classic raw BDTP streams and BSTRawFrame records are decoded. OriginalTiming paces replay using FILETIME markers.

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

func Filter(expr string) Option

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 Replay

func Replay(frames []can.Frame) Option

Replay adds a source that replays the given CAN frames. Useful for testing.

func ReplayObservations added in v1.0.0

func ReplayObservations(observations []Observation) Option

ReplayObservations adds owned source-aware observations for deterministic tests and capture replay. Each observation is copied before it is retained.

func Serial added in v1.1.0

func Serial(port string, format StreamFormat, options ...ActisenseSerialOption) Option

Serial adds a directly connected Actisense-format gateway at 115200 8N1. FormatActisense and FormatActisenseN2KASCII are passive, gateway-owned message sources. FormatActisenseRaw and FormatActisenseCANASCII perform an acknowledged mode setup and provide source-authoritative CAN frames.

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. FormatYDRaw, FormatActisenseRaw, and FormatActisenseCANASCII provide frame-level access. The two Actisense message formats are read-only here; use NewActisenseTCPSession for a writable gateway-owned binary session.

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. Raw/ASCII formats require the upstream gateway to already emit the selected representation because UDP has no return channel for BEM mode setup.

func USB

func USB(port string) Option

USB adds a USB-CAN Analyzer source for the given serial port (e.g., "/dev/ttyUSB0").

func WithBus

func WithBus(bus Bus) Option

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

func WithClaimTimeout(d time.Duration) Option

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

func WithHeartbeatInterval(d time.Duration) Option

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

func WithLogger(l *slog.Logger) Option

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 WithPreferredAddress added in v0.3.0

func WithPreferredAddress(addr uint8) Option

WithPreferredAddress sets the starting address for automatic address claiming while retaining arbitrary-address capability. Persist the last Client.Status().Address and pass it here on the next start to reclaim the device's prior address when available. Valid addresses are 0 through 251. Unlike WithSourceAddress, contention moves the client to another address.

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 rejected when the client is created.

func WithReadyTimeout added in v1.2.0

func WithReadyTimeout(d time.Duration) Option

WithReadyTimeout sets how long NewClient waits for an asynchronous Bus to open and finish transport-specific readiness work before address claiming starts. The default is five seconds. This deadline is intentionally separate from WithClaimTimeout so a gateway handshake can return its typed failure instead of being hidden by the address-claim deadline.

func WithReceiveBuffer added in v0.3.0

func WithReceiveBuffer(size int) Option

WithReceiveBuffer sets the number of decoded messages retained per live Client subscription. The default is 64. A subscriber that falls behind this bound is closed with ErrReceiveOverflow so it cannot stall protocol processing or other subscribers.

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. A bus client starts a new network epoch after reconnect: it reclaims its address, waits through contention, refreshes device discovery, and then resumes scheduled transmissions.

func WithSourceAddress

func WithSourceAddress(addr uint8) Option

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 251 and working downward if contention occurs.

func WithWriteQueue added in v0.3.0

func WithWriteQueue(size int) Option

WithWriteQueue sets the number of asynchronous writes that can wait behind the active write. The default is 64. Once full, Write completes immediately with ErrWriteQueueFull rather than blocking an application goroutine.

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 ReadyBus added in v0.3.0

type ReadyBus interface {
	Ready() <-chan struct{}
}

ReadyBus is optionally implemented by buses that are not writable until Run has opened their underlying device. Client waits for Ready before starting address claiming, removing scheduler-dependent startup races.

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

func NewScanner(ctx context.Context, opts ...Option) *Scanner

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

func (*Scanner) Close added in v0.3.0

func (s *Scanner) Close() error

Close stops the scanner and releases its source or live-client subscription. It is safe to call more than once.

func (*Scanner) Err

func (s *Scanner) Err() error

Err returns the first error encountered by the scanner.

func (*Scanner) Message

func (s *Scanner) Message() pgn.Message

Message returns the most recently scanned message.

func (*Scanner) Next

func (s *Scanner) Next() bool

Next advances the scanner to the next message. Returns false when no more messages are available (source exhausted or error occurred). Check Err() after Next returns false.

type SerialDevice added in v1.1.0

type SerialDevice struct {
	Name         string
	IsUSB        bool
	VID          string
	PID          string
	SerialNumber string
	Product      string
}

SerialDevice describes one OS serial port. USB identity fields are populated when the platform exposes them.

func SerialDevices added in v1.1.0

func SerialDevices() ([]SerialDevice, error)

SerialDevices enumerates serial ports using the host operating system.

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). BST-93 messages arrive fully assembled and retain
	// the gateway's source identity. This format is a gateway-owned message
	// session; prefer FormatActisenseRaw for a source-authoritative Client.
	FormatActisense
	// FormatActisenseRaw exchanges source-authoritative CAN frames using
	// BST-95. TCP and Serial sessions select and acknowledge operating mode 5;
	// UDP expects an upstream gateway already configured for BST-95. A writable
	// session that rejects or strips BEM setup fails rather than falling back.
	FormatActisenseRaw
	// FormatActisenseCANASCII selects acknowledged operating mode 6 and carries
	// source-authoritative CAN frames as human-readable lines. Binary BEM
	// control replies may be interleaved with the ASCII stream.
	FormatActisenseCANASCII
	// FormatActisenseN2KASCII reads gateway-assembled Type-A NMEA 2000
	// messages. Like FormatActisense, it is not a source-authoritative Bus.
	FormatActisenseN2KASCII
)

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.

func (*WriteResult) WaitContext added in v0.3.0

func (r *WriteResult) WaitContext(ctx context.Context) error

WaitContext waits for completion or returns when ctx is done. Cancelling ctx does not cancel a write that has already reached the bus.

Directories

Path Synopsis
cmd
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
actisense
Package actisense implements the bounded BDTP, BST, BEM, and EBL wire protocols used by Actisense-format gateways.
Package actisense implements the bounded BDTP, BST, BEM, and EBL wire protocols used by Actisense-format gateways.
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.
ebl
Package ebl reads bounded Enhanced Binary Log capture streams.
Package ebl reads bounded Enhanced Binary Log capture streams.
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 raw defines owned observations from NMEA 2000 transport adapters.
Package raw defines owned observations from NMEA 2000 transport adapters.
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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL