Skip to main content

DSL Syntax Reference

File structure

A .pgn file contains comments, enum definitions, lookup table definitions, and PGN blocks in any order.

# This is a comment

enum WindReference {
0 = "true_north"
1 = "magnetic_north"
2 = "apparent"
}

lookup VictronRegister uint16 {
0x0100 = "Product ID"
0xED8D = "DC Channel 1 Voltage"
}

pgn 130306 "Wind Data" interval=100ms {
sid uint8 :8
wind_speed uint16 :16 scale=0.01 unit="m/s"
wind_angle uint16 :16 scale=0.0001 unit="rad"
wind_reference WindReference :3
_ :5
}

Comments

Lines starting with # are comments. Comments can appear anywhere.

# PGN 129025 — Position, Rapid Update

PGN blocks

PGN definitions come in two forms:

# Full definition with field layout
pgn <number> "<name>" [attributes...] {
<field definitions>
}

# Name-only definition (no field layout known)
pgn <number> "<name>" [attributes...]
  • number: PGN number (decimal)
  • name: human-readable name (becomes the Go struct name in PascalCase for full definitions)
  • attributes: optional PGN-level metadata (see below)

Name-only PGNs

Omitting the braces registers the PGN's name and metadata without defining a field layout. The generated Registry entry has Decode: nil.

pgn 129038 "AIS Class A Position Report" fast_packet
pgn 126983 "Alert" fast_packet
pgn 127493 "Transmission Parameters Dynamic"

Use this form when the PGN's field structure is unknown or not yet implemented. Name-only PGNs still participate in fast-packet identification, PGN name display, and interval metadata.

PGN-level attributes

Attributes between the description and opening { describe transport and timing metadata for the PGN:

AttributeDescription
fast_packetBare flag. PGN uses multi-frame fast-packet protocol (payloads > 8 bytes).
interval=<duration>Default transmission interval. Accepts ms and s suffixes (e.g. 100ms, 1s, 2500ms, 60000ms). Stored as time.Duration in PGNInfo.
on_demandBare flag. PGN is event-driven (sent on request, not periodically).
draftBare flag. Definition is incomplete or reverse-engineered. Propagated to PGNInfo.Draft.

Examples:

# Fast-packet PGN with 1-second interval
pgn 129029 "GNSS Position Data" fast_packet interval=1000ms {
...
}

# On-demand PGN (no periodic transmission)
pgn 59904 "ISO Request" on_demand {
...
}

# Periodic single-frame PGN
pgn 129025 "Position Rapid Update" interval=100ms {
...
}

# All three combined
pgn 126996 "Product Information" fast_packet on_demand interval=5000ms {
...
}

These attributes are code-generated into the PGNInfo struct in pgn.Registry:

type PGNInfo struct {
PGN uint32
Description string
FastPacket bool
Interval time.Duration
OnDemand bool
Draft bool
Tolerances map[string]float64 // field name -> change detection tolerance
Decode func([]byte) (any, error) // nil for name-only PGNs
}

The FastPacket field is used by IsFastPacket() to identify fast-packet PGNs at runtime. For dispatch groups (multiple PGN definitions sharing the same number), all variants must agree on PGN-level metadata.

Field definitions

<name>  <type>  :<bits>  [attributes...]
ComponentDescription
nameField name (snake_case, becomes PascalCase in Go)
typeData type (see below)
:<bits>Bit width of the field
attributesOptional key=value pairs

Padding and unknown fields

Use _ as the field name for reserved/padding bits defined by the spec:

_  :5    # 5 bits of spec-defined padding

Use ? for data of unknown meaning (observed non-0xFF values, but undocumented):

?  :32   # 32 bits of unknown data

Both _ and ? have no type and generate no Go struct field. The distinction is semantic: _ means the spec defines these bits as reserved, ? means "we see data here but don't know what it means."

Data types

Integer types

TypeGo typeDescription
uint8uint8Unsigned 8-bit (or less with :N)
uint16uint16Unsigned 16-bit
uint32uint32Unsigned 32-bit
uint64uint64Unsigned 64-bit
int8int8Signed 8-bit
int16int16Signed 16-bit
int32int32Signed 32-bit
int64int64Signed 64-bit

Integer fields can use fewer bits than their type's natural width. A uint8 :4 reads 4 bits and stores in a uint8.

String type

model_id  string  :256   # 32 bytes (256 bits)

Strings are fixed-width, measured in bits (always a multiple of 8). Trailing 0xFF padding and null bytes are stripped. Use trim="..." to also right-trim specific characters (e.g. trim="@ " for AIS names that use @ and space padding).

Enum types

Use a previously defined enum name as the type:

wind_reference  WindReference  :3

Lookup types

Use a previously defined lookup name as the type:

register_id  uint16  :16  lookup=VictronRegister

The lookup= attribute can also be used on integer fields to add a Name() method without changing the underlying type.

Field attributes

These are per-field attributes (placed after the :bits specifier). For PGN-level attributes, see PGN-level attributes above.

AttributeValueDescription
scale=NfloatMultiply raw integer by this factor. Changes Go field to float64.
offset=NfloatAdd to scaled value: decoded = raw * scale + offset.
unit="..."stringUnit annotation (informational, included in generated comments)
trim="..."stringRight-trim these characters from decoded string (e.g. "@ " for AIS padding). Only valid on string fields.
tolerance=NfloatChange detection threshold. Fields with changes smaller than this are suppressed by the ChangeTracker. See Tolerance.
value=NintegerFixed value for dispatch. Field must equal this value for the PGN to match.
lookup=NameidentifierAttach a lookup table for Name() method
repeat=NintegerGenerate a slice of N elements (see Repeated Fields)
group="map"stringWith repeat=, generate a map keyed by instance index
as="name"stringCustom name for the repeated field in the Go struct

Tolerance for change tracking

The tolerance= attribute sets a threshold for the ChangeTracker (used by lplexdump -changes). When a PGN has any fields with tolerances, only those fields are checked for significance. All other fields (SID counters, padding, etc.) are ignored. A field change that stays within its tolerance is suppressed; one that exceeds it triggers a delta event.

pgn 127257 "Attitude" interval=1000ms {
sid uint8 :8
yaw int16 :16 scale=0.0001 unit="rad" tolerance=0.01
pitch int16 :16 scale=0.0001 unit="rad" tolerance=0.005
roll int16 :16 scale=0.0001 unit="rad" tolerance=0.005
_ :8
}

In this example, pitch/roll changes under 0.005 rad (~0.3 degrees) are suppressed. The sid field increments every packet but has no tolerance, so it's ignored entirely.

Tolerances are code-generated into PGNInfo.Tolerances (a map[string]float64) and automatically wired into the ChangeTracker at construction time via FieldToleranceDiff.

Bit layout

Fields are packed in order from bit 0 (LSB of byte 0). NMEA 2000 uses little-endian byte order. The generator tracks the current bit offset and reads each field at the appropriate position.

Example for PGN 129025 (8 bytes):

Bit offset  0                              32
|------- latitude (32 bits) ---|------- longitude (32 bits) ---|
Byte 0 1 2 3 4 5 6 7

Null detection

NMEA 2000 uses all-bits-set as a null/unavailable sentinel. The generated decoder checks for this and uses Go zero values (or NaN for scaled floats) when the raw value indicates null.

TypeNull value
uint8 :80xFF
uint16 :160xFFFF
int16 :160x7FFF
Scaled floatAll-bits-set in raw value