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;
}
Eec1is the Electronic Engine Controller 1 message (PGN 61444): engine speed, driver demand, actual torque, and so on.DmDtcListis the diagnostic-message DTC list — the structure behind DM1 (active faults) and its siblings.Dtcis a single Diagnostic Trouble Code: an SPN plus a failure mode plus an occurrence count.Fmiis the Failure Mode Identifier enum — the kind of fault (too high, too low, intermittent, and so on).DiagnosticLampsis the lamp-status block that rides along with a DM1 (malfunction lamp, warning lamp, etc.).Message(frommachbus::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 anOptionbecause the bytes might be too short or malformed;Nonemeans “this is not a valid payload for this message”. Always handle theNonecase 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 assembledMessage. It reads thedataout of the message and runsdecodeon it. Use this when you already have aMessagein 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
Eec1struct is built with physical, human-readable values:engine_speed_rpm: 1500.0is RPM,driver_demand_percent: 40.0is 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_addressis 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 anEec1. Note the.expect("valid EEC1 payload"):decodereturnsOption<Eec1>, and the example unwraps it because it just produced the bytes itself and knows they are valid. In real code you wouldmatchorif leton theOptioninstead, 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 address0x00. In a real reader these three pieces come off the bus, not from a localencode.Eec1::from_message(&msg)returnsOption<Eec1>. The example usesif 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 theMessageitself (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 matchingfrom_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. EachDtchas:spn— the Suspect Parameter Number, identifying what is faulty.110is engine coolant temperature;190is engine speed.fmi: Fmi::AboveNormalModerate/Fmi::BelowNormal— the Failure Mode Identifier, an enum describing the nature of the fault.AboveNormalModeratemeans a value is moderately too high (coolant running hot);BelowNormalmeans a value is too low. BecauseFmiis 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 aVec<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 anOption. The example loops overdecoded.dtcsand 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/Fmitypes 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
- J1939 messages and PGNs — the message catalogue and PGN meanings.
- Tutorial: inspect CAN identifiers — how to get the PGN in the first place, before you decode the payload.
- SAE J1939: the heritage — the standard these codecs implement.
- Powertrain — engine and drivetrain messages in a full application.
- Diagnostics — the DM family at application scale.