ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
j1939.h
Go to the documentation of this file.
1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4/**
5 * @file j1939.h
6 * @brief SAE J1939 message codec (PC_ENABLE_J1939) - the heavy-duty-vehicle / agriculture /
7 * marine / genset CAN higher-layer protocol, over 29-bit extended CAN frames.
8 *
9 * J1939 packs a 29-bit extended identifier as:
10 * @code
11 * bits 28-26 Priority | 25 EDP | 24 DP | 23-16 PF | 15-8 PS | 7-0 SA
12 * @endcode
13 * The 18-bit Parameter Group Number (PGN) is EDP|DP|PF|PS, where PS is part of the PGN only
14 * for PDU2 (broadcast, PF >= 240); for PDU1 (PF < 240) PS is the destination address (DA) and
15 * the PGN's low octet is 0. This codec encodes / decodes that id, builds single-frame
16 * messages, runs the Transport Protocol (BAM broadcast + RTS/CTS connection mode) with a
17 * reassembler for messages up to `PC_J1939_TP_MAX` octets, and builds the Address Claimed
18 * (with a 64-bit NAME) and Request PGN messages.
19 *
20 * Pure and host-tested. Drive it from the ESP32 TWAI peripheral (or an MCP2515 over SPI) to
21 * bridge a J1939 bus onto Wi-Fi - decode engine / transmission / genset PGNs and publish them.
22 *
23 * @author Douglas Quigg (dstroy0)
24 * @date 2026
25 */
26
27#ifndef PROTOCORE_J1939_H
28#define PROTOCORE_J1939_H
29
30#include "protocore_config.h"
31
32#if PC_NEED_J1939
33
35#include <stddef.h>
36#include <stdint.h>
37
38// Well-known PGNs and addresses.
39#define J1939_PGN_TP_CM 0x00EC00u ///< Transport Protocol - Connection Management (60416)
40#define J1939_PGN_TP_DT 0x00EB00u ///< Transport Protocol - Data Transfer (60160)
41#define J1939_PGN_ADDRESS_CLAIM 0x00EE00u ///< Address Claimed / Cannot Claim (60928)
42#define J1939_PGN_REQUEST 0x00EA00u ///< Request PGN (59904)
43#define J1939_ADDR_GLOBAL 0xFFu ///< broadcast destination address
44#define J1939_ADDR_NULL 0xFEu ///< null / unclaimed source address
45#define J1939_PDU2_THRESHOLD 240u ///< PF >= 240 is PDU2 (broadcast); < 240 is PDU1 (peer)
46
47// TP.CM control bytes (data[0] of a TP.CM frame).
48#define J1939_TP_CM_RTS 0x10u ///< Request To Send (connection mode)
49#define J1939_TP_CM_CTS 0x11u ///< Clear To Send
50#define J1939_TP_CM_EOM_ACK 0x13u ///< End Of Message Acknowledge
51#define J1939_TP_CM_BAM 0x20u ///< Broadcast Announce Message
52#define J1939_TP_CM_ABORT 0xFFu ///< Connection Abort
53
54#define J1939_TP_DT_LEN 7u ///< data octets carried per TP.DT packet (1 seq byte + 7 data)
55
56/** @brief A decoded J1939 identifier. */
57struct J1939Id
58{
59 uint8_t priority; ///< 0 (highest) .. 7
60 uint32_t pgn; ///< 18-bit Parameter Group Number
61 uint8_t sa; ///< source address
62 uint8_t da; ///< destination address (PDU1), or J1939_ADDR_GLOBAL (PDU2)
63 uint8_t pf; ///< PDU format
64 uint8_t ps; ///< PDU specific (DA for PDU1, group extension for PDU2)
65 bool pdu1; ///< true => peer-to-peer (PF < 240); false => broadcast
66};
67
68/** @brief Result of feeding a frame to the TP reassembler. */
69enum class J1939TpResult : uint8_t
70{
71 J1939_TP_IGNORED = 0, ///< not a TP frame for the active session
72 J1939_TP_STARTED, ///< a BAM / RTS opened a session
73 J1939_TP_PROGRESS, ///< a data packet was accepted, more to come
74 J1939_TP_COMPLETE, ///< the message is fully reassembled (see fields below)
75 J1939_TP_ERROR, ///< malformed / out-of-sequence / too large
76};
77
78/** @brief Transport-Protocol reassembly context (one in-flight message). */
79struct J1939TpRx
80{
81 bool active;
82 uint8_t sa; ///< source of the session
83 uint32_t pgn; ///< the transported PGN
84 uint16_t total_size; ///< announced message size
85 uint8_t num_packets; ///< announced packet count
86 uint8_t next_seq; ///< next expected sequence number (1-based)
87 uint16_t received; ///< octets stored so far
88 uint8_t buf[PC_J1939_TP_MAX];
89};
90
91// --- identifier ---
92
93/** @brief Encode a 29-bit J1939 id. @p da is used only for a PDU1 (PF < 240) PGN. */
94bool pc_j1939_encode_id(uint32_t *id, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da);
95
96/** @brief Decode a 29-bit J1939 id into its fields. */
97bool pc_j1939_decode_id(uint32_t id, J1939Id *out);
98
99// --- single-frame messages ---
100
101/** @brief Build a single-frame (<= 8 octet) J1939 message. */
102bool pc_j1939_build_message(CanFrame *out, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da, const uint8_t *data,
103 uint8_t len);
104
105/** @brief Build a Request-PGN frame asking @p da for @p requested_pgn. */
106bool pc_j1939_build_request(CanFrame *out, uint8_t sa, uint8_t da, uint32_t requested_pgn);
107
108/** @brief Build an Address-Claimed frame announcing @p sa with the 64-bit @p name. */
109bool pc_j1939_build_address_claim(CanFrame *out, uint8_t sa, uint64_t name);
110
111/** @brief Compose a 64-bit J1939 NAME from its fields (see J1939-81). */
112uint64_t pc_j1939_build_name(bool arbitrary_address_capable, uint8_t industry_group, uint8_t vehicle_system_instance,
113 uint8_t vehicle_system, uint8_t function, uint8_t function_instance, uint8_t ecu_instance,
114 uint16_t manufacturer_code, uint32_t identity_number);
115
116// --- transport protocol (multi-packet) ---
117
118/** @brief Octet count -> TP packet count (ceil(size / 7)). */
119uint8_t pc_j1939_tp_num_packets(uint16_t total_size);
120
121/** @brief Build the BAM (broadcast) TP.CM announce frame for @p pgn / @p total_size. */
122bool pc_j1939_build_bam_cm(CanFrame *out, uint8_t sa, uint32_t pgn, uint16_t total_size);
123
124/** @brief Build TP.DT data packet @p seq (1-based) carrying @p chunk_len (1..7) octets. */
125bool pc_j1939_build_tp_dt(CanFrame *out, uint8_t sa, uint8_t da, uint8_t seq, const uint8_t *chunk, uint8_t chunk_len);
126
127/** @brief Reset a reassembly context to idle. */
128void pc_j1939_tp_reset(J1939TpRx *rx);
129
130/** @brief Feed a received frame to the reassembler; see @ref J1939TpResult. */
131J1939TpResult pc_j1939_tp_feed(J1939TpRx *rx, const CanFrame *f);
132
133// --- typed decoders for common engine PGNs (SAE J1939-71) ---
134//
135// These lift a raw single-frame CAN message into engineering units, applying the J1939 scale + offset and
136// the "not available" ranges (a 1-octet SPN is valid 0x00..0xFA, a 2-octet SPN 0x0000..0xFAFF; the rest
137// is error / not-available and clears the corresponding valid flag).
138
139#define J1939_PGN_EEC1 0x00F004u ///< Electronic Engine Controller 1 (61444): engine speed + torque
140#define J1939_PGN_ET1 0x00FEEEu ///< Engine Temperature 1 (65262): coolant / fuel / oil temperature
141#define J1939_PGN_LFE 0x00FEF2u ///< Fuel Economy (65266): fuel rate + instantaneous / average economy
142#define J1939_PGN_AMB 0x00FEF5u ///< Ambient Conditions (65269): barometric pressure + air / road temperatures
143#define J1939_PGN_IC1 0x00FEF6u ///< Inlet/Exhaust Conditions 1 (65270): boost + intake / exhaust + filter pressures
144#define J1939_PGN_VD 0x00FEE0u ///< Vehicle Distance (65248): trip + total vehicle distance
145#define J1939_PGN_CCVS 0x00FEF1u ///< Cruise Control/Vehicle Speed (65265): wheel-based vehicle speed + cruise state
146#define J1939_PGN_DM1 0x00FECAu ///< Active Diagnostic Trouble Codes (65226): lamp status + DTC list
147
148/** @brief Decoded EEC1 (PGN 61444). Percent-torque fields are @ref J1939_TORQUE_NA when not available. */
149struct J1939Eec1
150{
151 uint8_t torque_mode; ///< engine torque mode (data[0] low nibble)
152 int16_t drivers_demand_torque_pct; ///< driver's demand percent torque (-125..125), or J1939_TORQUE_NA
153 int16_t actual_engine_torque_pct; ///< actual engine percent torque (-125..125), or J1939_TORQUE_NA
154 bool engine_speed_valid; ///< false when the raw speed is in the not-available range
155 float engine_speed_rpm; ///< engine speed (rpm, 0.125 rpm/bit)
156};
157
158/** @brief Sentinel percent-torque value meaning "not available". */
159#define J1939_TORQUE_NA ((int16_t)0x7FFF)
160
161/** @brief Decoded ET1 (PGN 65262). Each temperature has its own validity flag. */
162struct J1939Et1
163{
164 bool coolant_valid;
165 float coolant_temp_c; ///< engine coolant temperature (degC, 1 degC/bit, -40 offset)
166 bool fuel_valid;
167 float fuel_temp_c; ///< fuel temperature (degC, 1 degC/bit, -40 offset)
168 bool oil_valid;
169 float oil_temp_c; ///< engine oil temperature (degC, 0.03125 degC/bit, -273 offset)
170};
171
172/**
173 * @brief Decode an EEC1 (PGN 61444) single frame into @p out.
174 * @return true iff @p f decodes to PGN 61444 and carries 8 data octets; false otherwise.
175 */
176bool pc_j1939_decode_eec1(const CanFrame *f, J1939Eec1 *out);
177
178/**
179 * @brief Decode an ET1 (PGN 65262) single frame into @p out.
180 * @return true iff @p f decodes to PGN 65262 and carries 8 data octets; false otherwise.
181 */
182bool pc_j1939_decode_et1(const CanFrame *f, J1939Et1 *out);
183
184/** @brief Decoded LFE (PGN 65266). Each value has its own validity flag (cleared for a not-available raw). */
185struct J1939Lfe
186{
187 bool fuel_rate_valid;
188 float fuel_rate_lph; ///< engine fuel rate (L/h, 0.05 L/h per bit)
189 bool instant_econ_valid;
190 float instant_econ_kmpl; ///< instantaneous fuel economy (km/L, 1/512 km/L per bit)
191 bool avg_econ_valid;
192 float avg_econ_kmpl; ///< average fuel economy (km/L, 1/512 km/L per bit)
193 bool throttle_valid;
194 float throttle_pct; ///< throttle valve 1 position (percent, 0.4 %/bit)
195};
196
197/**
198 * @brief Decode an LFE (PGN 65266) single frame into @p out.
199 * @return true iff @p f decodes to PGN 65266 and carries 8 data octets; false otherwise.
200 */
201bool pc_j1939_decode_lfe(const CanFrame *f, J1939Lfe *out);
202
203/** @brief Decoded AMB (PGN 65269). Each measurement has its own validity flag (cleared for a
204 * not-available raw). Barometric pressure is a 1-octet SPN; the temperatures are 2-octet except the
205 * air inlet, which is a 1-octet SPN. */
206struct J1939Amb
207{
208 bool baro_valid;
209 float baro_kpa; ///< barometric pressure (kPa, 0.5 kPa/bit) - SPN 108
210 bool cab_temp_valid;
211 float cab_temp_c; ///< cab interior temperature (degC, 0.03125 degC/bit, -273 offset) - SPN 170
212 bool ambient_temp_valid;
213 float ambient_temp_c; ///< ambient air temperature (degC, 0.03125 degC/bit, -273 offset) - SPN 171
214 bool inlet_temp_valid;
215 float inlet_temp_c; ///< engine air inlet temperature (degC, 1 degC/bit, -40 offset) - SPN 172
216 bool road_temp_valid;
217 float road_temp_c; ///< road surface temperature (degC, 0.03125 degC/bit, -273 offset) - SPN 79
218};
219
220/**
221 * @brief Decode an AMB (PGN 65269) single frame into @p out.
222 * @return true iff @p f decodes to PGN 65269 and carries 8 data octets; false otherwise.
223 */
224bool pc_j1939_decode_amb(const CanFrame *f, J1939Amb *out);
225
226/** @brief Decoded IC1 (PGN 65270). Each measurement has its own validity flag (cleared for a
227 * not-available raw). Exhaust gas temperature is a 2-octet SPN; the rest are 1-octet. */
228struct J1939Ic1
229{
230 bool trap_inlet_valid;
231 float trap_inlet_kpa; ///< particulate trap inlet pressure (kPa, 0.5 kPa/bit) - SPN 81
232 bool boost_valid;
233 float boost_kpa; ///< boost pressure (kPa, 2 kPa/bit) - SPN 102
234 bool intake_temp_valid;
235 float intake_temp_c; ///< intake manifold 1 temperature (degC, 1 degC/bit, -40 offset) - SPN 105
236 bool air_inlet_valid;
237 float air_inlet_kpa; ///< air inlet pressure (kPa, 2 kPa/bit) - SPN 106
238 bool air_filter_valid;
239 float air_filter_kpa; ///< air filter 1 differential pressure (kPa, 0.05 kPa/bit) - SPN 107
240 bool exhaust_temp_valid;
241 float exhaust_temp_c; ///< exhaust gas temperature (degC, 0.03125 degC/bit, -273 offset) - SPN 173
242 bool coolant_filter_valid;
243 float coolant_filter_kpa; ///< coolant filter differential pressure (kPa, 0.5 kPa/bit) - SPN 112
244};
245
246/**
247 * @brief Decode an IC1 (PGN 65270) single frame into @p out.
248 * @return true iff @p f decodes to PGN 65270 and carries 8 data octets; false otherwise.
249 */
250bool pc_j1939_decode_ic1(const CanFrame *f, J1939Ic1 *out);
251
252/** @brief Decoded VD (PGN 65248). The distances are held as double: at 0.125 km/bit a 32-bit odometer
253 * spans hundreds of millions of km, beyond float's ~7-digit precision. */
254struct J1939Vd
255{
256 bool trip_valid;
257 double trip_km; ///< trip distance (km, 0.125 km/bit) - SPN 244
258 bool total_valid;
259 double total_km; ///< total vehicle distance (km, 0.125 km/bit) - SPN 245
260};
261
262/**
263 * @brief Decode a VD (PGN 65248) single frame into @p out.
264 * @return true iff @p f decodes to PGN 65248 and carries 8 data octets; false otherwise.
265 */
266bool pc_j1939_decode_vd(const CanFrame *f, J1939Vd *out);
267
268/** @brief Decoded CCVS (PGN 65265): the wheel-based vehicle speed plus the cruise-control-active state.
269 * Only the two signals with cross-source-verified positions are decoded; the many discrete switches in
270 * this PGN are left to the caller (their bit positions vary between vendor definitions). */
271struct J1939Ccvs
272{
273 bool speed_valid; ///< false when the raw wheel-based speed is in the not-available range
274 float wheel_speed_kmh; ///< wheel-based vehicle speed (km/h, 1/256 km/h per bit) - SPN 84
275 uint8_t
276 cruise_active; ///< cruise control active state, a 2-bit value (0 off / 1 active / 2 error / 3 n/a) - SPN 595
277};
278
279/**
280 * @brief Decode a CCVS (PGN 65265) single frame into @p out.
281 * @return true iff @p f decodes to PGN 65265 and carries 8 data octets; false otherwise.
282 */
283bool pc_j1939_decode_ccvs(const CanFrame *f, J1939Ccvs *out);
284
285/** @brief One decoded Diagnostic Trouble Code (J1939-73 SPN conversion method 4). */
286struct J1939Dtc
287{
288 uint32_t spn; ///< suspect parameter number (19-bit)
289 uint8_t fmi; ///< failure mode identifier (5-bit)
290 uint8_t cm; ///< SPN conversion method (1-bit)
291 uint8_t oc; ///< occurrence count (7-bit)
292};
293
294/** @brief Decoded DM1 lamp status (each field 0 = off, 1 = on; 2/3 reserved / not available). */
295struct J1939Dm1
296{
297 uint8_t mil; ///< malfunction indicator lamp
298 uint8_t red_stop; ///< red stop lamp
299 uint8_t amber_warning; ///< amber warning lamp
300 uint8_t protect; ///< protect lamp
301 uint8_t dtc_count; ///< number of active DTCs decoded into the caller's array
302};
303
304/**
305 * @brief Decode a DM1 (PGN 65226) body: the lamp-status octet, the flash-status octet, then 4-octet DTCs.
306 * DM1 may arrive single-frame or reassembled over the Transport Protocol, so this takes the raw body
307 * (a frame's data[] or a TP buffer). An all-zero DTC (the "no active fault" placeholder) is skipped.
308 * @param out_dtcs caller array receiving up to @p max decoded DTCs (may be null to only read the lamps).
309 * @return true iff @p len is at least 2 octets (the two status octets); false otherwise.
310 */
311bool pc_j1939_decode_dm1(const uint8_t *body, size_t len, J1939Dm1 *out, J1939Dtc *out_dtcs, size_t max);
312
313#endif // PC_NEED_J1939
314#endif // PROTOCORE_J1939_H
Shared CAN 2.0 frame type for the CAN-based industrial codecs (one source of truth).
User-facing configuration for ProtoCore.
Definition can.h:44