Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Tutorial: decode J1939 PGNs

Identifying a PGN tells you what a frame is; decoding it tells you what it says. machbus ships the J1939 wire codecs as plain functions on typed structs in [machbus::j1939]. Each message group — engine controllers, diagnostics, hours meters, language preferences, and more — has a struct with three uniform operations: decode bytes into the struct, from_message decode from an assembled Message, and encode the struct back to bytes. No protocol stack, no session, no IO.

This tutorial walks through examples/j1939_decode.rs. It builds two messages — EEC1 (engine speed) and DM1 (active diagnostic trouble codes) — encodes each one, then decodes it straight back. Because every block round-trips, the output is self-validating: if the codec were wrong, the numbers coming out would not match the numbers going in.

The imports

#![allow(unused)]
fn main() {
use machbus::j1939::{DiagnosticLamps, DmDtcList, Dtc, Eec1, Fmi};
use machbus::net::Message;
}
  • Eec1 is the Electronic Engine Controller 1 message (PGN 61444): engine speed, driver demand, actual torque, and so on.
  • DmDtcList is the diagnostic-message DTC list — the structure behind DM1 (active faults) and its siblings.
  • Dtc is a single Diagnostic Trouble Code: an SPN plus a failure mode plus an occurrence count.
  • Fmi is the Failure Mode Identifier enum — the kind of fault (too high, too low, intermittent, and so on).
  • DiagnosticLamps is the lamp-status block that rides along with a DM1 (malfunction lamp, warning lamp, etc.).
  • Message (from machbus::net) is the assembled-message type: a PGN, a data buffer, and a source address. It is what you get after the transport layer has reassembled multi-frame traffic for you.

The codec contract

Every J1939 codec in machbus follows the same shape, and learning it once means you know all of them:

  • Struct::decode(&[u8]) -> Option<Self> — parse raw payload bytes. It returns an Option because the bytes might be too short or malformed; None means “this is not a valid payload for this message”. Always handle the None case when you are reading off a real bus, where truncated and garbage frames happen.
  • Struct::from_message(&Message) -> Option<Self> — the same, but starting from an assembled Message. It reads the data out of the message and runs decode on it. Use this when you already have a Message in hand.
  • struct.encode() -> [u8; 8] (or -> Vec<u8> for variable-length messages) — serialize back to the wire format you would transmit.

Decoding EEC1

#![allow(unused)]
fn main() {
{{#include ../../../examples/j1939_decode.rs:eec1}}
}

Step through it:

  • The Eec1 struct is built with physical, human-readable values: engine_speed_rpm: 1500.0 is RPM, driver_demand_percent: 40.0 is a percentage, and so on. You do not deal with the scaling factors and offsets the wire format uses — the codec applies them for you. source_address is the address this frame claims to come from.
  • eec1.encode() returns a [u8; 8] — exactly one CAN frame’s worth of data, with each field scaled and packed into its bit positions. This is the buffer you would hand to your CAN driver to transmit.
  • Eec1::decode(&bytes) parses those eight bytes back into an Eec1. Note the .expect("valid EEC1 payload"): decode returns Option<Eec1>, and the example unwraps it because it just produced the bytes itself and knows they are valid. In real code you would match or if let on the Option instead, because off-bus bytes are not guaranteed to be well-formed.
  • The printout reads the decoded fields back: 1500 rpm, driver demand 40%, actual 38%. These match the input exactly, which is the round-trip proving the codec is symmetric.

Decoding from a Message

Often you do not have a bare byte slice — you have a Message that the network layer handed you. The second block decodes from one of those:

#![allow(unused)]
fn main() {
{{#include ../../../examples/j1939_decode.rs:from_message}}
}
  • Message::new(61444, bytes.to_vec(), 0x00) builds a message for PGN 61444 (EEC1), carrying the bytes we just encoded, from source address 0x00. In a real reader these three pieces come off the bus, not from a local encode.
  • Eec1::from_message(&msg) returns Option<Eec1>. The example uses if let Some(e) = … — this is the idiomatic way to handle the fallible decode. If the message’s PGN or payload did not match, you would simply skip it.
  • The output, from_message: 1500 rpm from source 0x00, shows you can pull the source address off the Message itself (msg.source) alongside the decoded engine speed. This is the pattern you will use in a real dispatch loop: match on the PGN, call the matching from_message, and use both the decoded fields and the message metadata.

Decoding DM1 diagnostics

Diagnostics are where J1939 gets interesting, because a single DM1 can carry a list of faults and can spill across multiple CAN frames. machbus models this with DmDtcList:

#![allow(unused)]
fn main() {
{{#include ../../../examples/j1939_decode.rs:dm1}}
}

Read it field by field:

  • lamps: DiagnosticLamps::default() sets all the diagnostic lamps to their default (off) state. On a real ECU these flags tell the operator whether the malfunction lamp, amber warning lamp, red stop lamp, or protect lamp is lit.
  • dtcs: vec![…] is the list of active faults. Each Dtc has:
    • spn — the Suspect Parameter Number, identifying what is faulty. 110 is engine coolant temperature; 190 is engine speed.
    • fmi: Fmi::AboveNormalModerate / Fmi::BelowNormal — the Failure Mode Identifier, an enum describing the nature of the fault. AboveNormalModerate means a value is moderately too high (coolant running hot); BelowNormal means a value is too low. Because Fmi is an enum, the example can print it with {:?} and get a readable name instead of a raw number.
    • occurrence_count — how many times this fault has been seen.
  • dm1.encode() returns a Vec<u8>, not a [u8; 8]. DM1 is variable-length: a few faults fit in a single frame, but more will exceed eight bytes and require the transport protocol (multi-frame). The codec produces the full logical payload; the transport layer handles the framing.
  • DmDtcList::decode(&payload) parses it back, again returning an Option. The example loops over decoded.dtcs and prints each one. The two faults come back exactly as they went in.

Run it

$ cargo run --example j1939_decode

Output:

EEC1: 1500 rpm, driver demand 40%, actual 38%
from_message: 1500 rpm from source 0x00

DM1: 2 active fault(s):
  SPN 110   FMI AboveNormalModerate (count 3)
  SPN 190   FMI BelowNormal (count 1)

Every value printed is the value that was fed in, which is the whole point of a round-trip example: it proves the encode and decode paths agree.

What to change for real bus data

The example encodes its own bytes so the demo is reproducible. On a live bus you skip the encode entirely. You read a frame, build a Message from the PGN, payload, and source (or, for multi-frame messages, let the transport layer reassemble the full payload first), then call the matching from_message. The crucial difference: handle the Option. Real frames can be truncated, corrupted, or simply a different PGN than you expected, so decode and from_message will legitimately return None — treat that as “skip this frame”, not as a panic. Replace the .expect(...) you see here with an if let Some(...) or a match.

More J1939 codecs

EEC1 and DM1 are two members of much larger families. machbus provides codecs across the J1939 message groups, including:

  • Engine and powertrain — engine controllers, fuel economy, hours and revolutions, temperatures and fluid levels.
  • Diagnostics — the full DM family (active, previously active, and the related diagnostic messages), built on the same DmDtcList / Dtc / Fmi types you used above.
  • Network management and heartbeat — address claim, keep-alive, and status messages.
  • Language and units — operator language and unit-system preferences.

They all follow the identical decode / from_message / encode contract, so the patterns in this tutorial carry over directly. See J1939 messages and PGNs for the catalogue.

See also