ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
j1939.cpp
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.cpp
6 * @brief SAE J1939 message codec (pure, host-tested).
7 */
8
10
11#if PC_NEED_J1939
12
13#include <string.h>
14
15bool pc_j1939_encode_id(uint32_t *id, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da)
16{
17 if (!id || priority > 7 || pgn > 0x3FFFFu)
18 {
19 return false;
20 }
21 uint8_t edp = (uint8_t)((pgn >> 17) & 1u);
22 uint8_t dp = (uint8_t)((pgn >> 16) & 1u);
23 uint8_t pf = (uint8_t)((pgn >> 8) & 0xFFu);
24 // PDU1 (peer-to-peer) carries the destination address in PS; PDU2 (broadcast) carries
25 // the PGN's group-extension low octet there.
26 uint8_t ps = (pf < J1939_PDU2_THRESHOLD) ? da : (uint8_t)(pgn & 0xFFu);
27 *id = ((uint32_t)(priority & 7u) << 26) | ((uint32_t)edp << 25) | ((uint32_t)dp << 24) | ((uint32_t)pf << 16) |
28 ((uint32_t)ps << 8) | (uint32_t)sa;
29 return true;
30}
31
32bool pc_j1939_decode_id(uint32_t id, J1939Id *out)
33{
34 if (!out)
35 {
36 return false;
37 }
39 out->priority = (uint8_t)((id >> 26) & 7u);
40 uint8_t edp = (uint8_t)((id >> 25) & 1u);
41 uint8_t dp = (uint8_t)((id >> 24) & 1u);
42 out->pf = (uint8_t)((id >> 16) & 0xFFu);
43 out->ps = (uint8_t)((id >> 8) & 0xFFu);
44 out->sa = (uint8_t)(id & 0xFFu);
45 out->pdu1 = out->pf < J1939_PDU2_THRESHOLD;
46 if (out->pdu1)
47 {
48 out->da = out->ps;
49 out->pgn = ((uint32_t)edp << 17) | ((uint32_t)dp << 16) | ((uint32_t)out->pf << 8);
50 }
51 else
52 {
53 out->da = J1939_ADDR_GLOBAL;
54 out->pgn = ((uint32_t)edp << 17) | ((uint32_t)dp << 16) | ((uint32_t)out->pf << 8) | out->ps;
55 }
56 return true;
57}
58
59// Fill a CanFrame as a 29-bit extended frame.
60static bool ext_frame(CanFrame *f, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da, uint8_t dlc)
61{
62 uint32_t id;
63 if (!pc_j1939_encode_id(&id, priority, pgn, sa, da))
64 {
65 return false;
66 }
67 f->id = id;
68 f->extended = true;
69 f->rtr = false;
70 f->dlc = dlc;
71 memset(f->data, 0xFF, sizeof(f->data)); // J1939 pads unused octets with 0xFF (not available)
72 return true;
73}
74
75bool pc_j1939_build_message(CanFrame *out, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da, const uint8_t *data,
76 uint8_t len)
77{
78 if (!out || len > PC_CAN_MAX_DLC || (len && !data))
79 {
80 return false;
81 }
82 if (!ext_frame(out, priority, pgn, sa, da, len))
83 {
84 return false;
85 }
86 if (len)
87 {
88 memcpy(out->data, data, len);
89 }
90 return true;
91}
92
93bool pc_j1939_build_request(CanFrame *out, uint8_t sa, uint8_t da, uint32_t requested_pgn)
94{
95 if (!out || requested_pgn > 0x3FFFFu)
96 {
97 return false;
98 }
99 // Request PGN (priority 6): 3-octet little-endian requested PGN.
100 if (!ext_frame(out, 6, J1939_PGN_REQUEST, sa, da, 3)) // GCOVR_EXCL_LINE unreachable: fixed priority 6 +
101 // J1939_PGN_REQUEST (<=0x3FFFF), encode can't fail
102 {
103 return false; // GCOVR_EXCL_LINE unreachable: fixed priority 6 + J1939_PGN_REQUEST (<=0x3FFFF), encode can't
104 // fail
105 }
106 out->data[0] = (uint8_t)requested_pgn;
107 out->data[1] = (uint8_t)(requested_pgn >> 8);
108 out->data[2] = (uint8_t)(requested_pgn >> 16);
109 return true;
110}
111
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 // NAME bit layout (J1939-81), LSB first:
117 // [0..20] identity number, [21..31] manufacturer code, [32..34] ECU instance,
118 // [35..39] function instance, [40..47] function, [48] reserved, [49..55] vehicle system,
119 // [56..59] vehicle system instance, [60..62] industry group, [63] arbitrary-address-capable.
120 uint64_t n = 0;
121 n |= (uint64_t)(identity_number & 0x1FFFFFu);
122 n |= (uint64_t)(manufacturer_code & 0x7FFu) << 21;
123 n |= (uint64_t)(ecu_instance & 0x7u) << 32;
124 n |= (uint64_t)(function_instance & 0x1Fu) << 35;
125 n |= (uint64_t)(function & 0xFFu) << 40;
126 n |= (uint64_t)(vehicle_system & 0x7Fu) << 49;
127 n |= (uint64_t)(vehicle_system_instance & 0xFu) << 56;
128 n |= (uint64_t)(industry_group & 0x7u) << 60;
129 n |= (uint64_t)(arbitrary_address_capable ? 1u : 0u) << 63;
130 return n;
131}
132
133bool pc_j1939_build_address_claim(CanFrame *out, uint8_t sa, uint64_t name)
134{
135 // Address Claimed (priority 6, broadcast): NAME as 8 octets, little-endian.
136 if (!ext_frame(out, 6, J1939_PGN_ADDRESS_CLAIM, sa, J1939_ADDR_GLOBAL, 8)) // GCOVR_EXCL_LINE unreachable: fixed
137 // priority 6 + PGN_ADDRESS_CLAIM
138 // (<=0x3FFFF), encode can't fail
139 {
140 return false; // GCOVR_EXCL_LINE unreachable: fixed priority 6 + J1939_PGN_ADDRESS_CLAIM (<=0x3FFFF), encode
141 // can't fail
142 }
143 for (int i = 0; i < 8; i++)
144 {
145 out->data[i] = (uint8_t)(name >> (8 * i));
146 }
147 return true;
148}
149
150uint8_t pc_j1939_tp_num_packets(uint16_t total_size)
151{
152 return (uint8_t)((total_size + (J1939_TP_DT_LEN - 1)) / J1939_TP_DT_LEN);
153}
154
155bool pc_j1939_build_bam_cm(CanFrame *out, uint8_t sa, uint32_t pgn, uint16_t total_size)
156{
157 if (!out || total_size < 9 || total_size > PC_J1939_TP_MAX || pgn > 0x3FFFFu)
158 {
159 return false; // BAM is for 9..1785 octet messages
160 }
161 if (!ext_frame(out, 7, J1939_PGN_TP_CM, sa, J1939_ADDR_GLOBAL, 8)) // GCOVR_EXCL_LINE unreachable: fixed priority
162 // 7 + J1939_PGN_TP_CM (<=0x3FFFF), encode
163 // can't fail
164 {
165 return false; // GCOVR_EXCL_LINE unreachable: fixed priority 7 + J1939_PGN_TP_CM (<=0x3FFFF), encode can't fail
166 }
167 out->data[0] = J1939_TP_CM_BAM;
168 out->data[1] = (uint8_t)total_size; // message size, little-endian
169 out->data[2] = (uint8_t)(total_size >> 8);
170 out->data[3] = pc_j1939_tp_num_packets(total_size); // total packets
171 out->data[4] = 0xFF; // reserved
172 out->data[5] = (uint8_t)pgn; // transported PGN, little-endian
173 out->data[6] = (uint8_t)(pgn >> 8);
174 out->data[7] = (uint8_t)(pgn >> 16);
175 return true;
176}
177
178bool pc_j1939_build_tp_dt(CanFrame *out, uint8_t sa, uint8_t da, uint8_t seq, const uint8_t *chunk, uint8_t chunk_len)
179{
180 if (!out || seq == 0 || chunk_len == 0 || chunk_len > J1939_TP_DT_LEN || !chunk)
181 {
182 return false;
183 }
184 if (!ext_frame(out, 7, J1939_PGN_TP_DT, sa, da, 8)) // GCOVR_EXCL_LINE unreachable: fixed priority 7 +
185 // J1939_PGN_TP_DT (<=0x3FFFF), encode can't fail
186 {
187 return false; // GCOVR_EXCL_LINE unreachable: fixed priority 7 + J1939_PGN_TP_DT (<=0x3FFFF), encode can't fail
188 }
189 out->data[0] = seq; // sequence number, 1-based
190 memcpy(out->data + 1, chunk, chunk_len); // remaining octets stay 0xFF padding
191 return true;
192}
193
194void pc_j1939_tp_reset(J1939TpRx *rx)
195{
196 if (rx)
197 {
198 memset(rx, 0, sizeof(*rx));
199 }
200}
201
202J1939TpResult pc_j1939_tp_feed(J1939TpRx *rx, const CanFrame *f)
203{
204 if (!rx || !f || !f->extended)
205 {
206 return J1939TpResult::J1939_TP_IGNORED;
207 }
208 J1939Id id;
209 if (!pc_j1939_decode_id(f->id, &id)) // GCOVR_EXCL_LINE unreachable: decode_id only fails on a null out, and &id
210 // is non-null
211 {
212 return J1939TpResult::J1939_TP_IGNORED; // GCOVR_EXCL_LINE unreachable: decode_id only fails on a null out, and
213 // &id is non-null
214 }
215
216 if (id.pgn == J1939_PGN_TP_CM && f->dlc >= 8)
217 {
218 uint8_t control = f->data[0];
219 if (control != J1939_TP_CM_BAM && control != J1939_TP_CM_RTS)
220 {
221 return J1939TpResult::J1939_TP_IGNORED; // CTS / EOM / Abort are not receiver-side session starts
222 }
223 uint16_t total = (uint16_t)(f->data[1] | (f->data[2] << 8));
224 uint8_t packets = f->data[3];
225 uint32_t pgn = (uint32_t)f->data[5] | ((uint32_t)f->data[6] << 8) | ((uint32_t)f->data[7] << 16);
226 if (total < 9 || total > PC_J1939_TP_MAX || packets != pc_j1939_tp_num_packets(total))
227 {
228 return J1939TpResult::J1939_TP_ERROR;
229 }
230 rx->active = true;
231 rx->sa = id.sa;
232 rx->pgn = pgn;
233 rx->total_size = total;
234 rx->num_packets = packets;
235 rx->next_seq = 1;
236 rx->received = 0;
237 return J1939TpResult::J1939_TP_STARTED;
238 }
239
240 if (id.pgn == J1939_PGN_TP_DT && f->dlc >= 1)
241 {
242 if (!rx->active || id.sa != rx->sa)
243 {
244 return J1939TpResult::J1939_TP_IGNORED;
245 }
246 uint8_t seq = f->data[0];
247 if (seq != rx->next_seq)
248 {
249 pc_j1939_tp_reset(rx);
250 return J1939TpResult::J1939_TP_ERROR; // out-of-sequence: abort the session
251 }
252 uint16_t remaining = (uint16_t)(rx->total_size - rx->received);
253 uint8_t take = remaining < J1939_TP_DT_LEN ? (uint8_t)remaining : (uint8_t)J1939_TP_DT_LEN;
254 memcpy(rx->buf + rx->received, f->data + 1, take);
255 rx->received = (uint16_t)(rx->received + take);
256 rx->next_seq++;
257 if (rx->received >= rx->total_size)
258 {
259 rx->active = false;
260 return J1939TpResult::J1939_TP_COMPLETE;
261 }
262 return J1939TpResult::J1939_TP_PROGRESS;
263 }
264
265 return J1939TpResult::J1939_TP_IGNORED;
266}
267
268// --- typed decoders (SAE J1939-71) ---
269
270bool pc_j1939_decode_eec1(const CanFrame *f, J1939Eec1 *out)
271{
272 if (!f || !out || f->dlc < 8)
273 {
274 return false;
275 }
276 J1939Id id;
277 if (!pc_j1939_decode_id(f->id, &id) || id.pgn != J1939_PGN_EEC1)
278 {
279 return false;
280 }
281 out->torque_mode = (uint8_t)(f->data[0] & 0x0Fu);
282 // percent torque: raw 0..250 maps to -125..+125 %; 0xFB..0xFF is error / not-available.
283 out->drivers_demand_torque_pct = (f->data[1] <= 0xFAu) ? (int16_t)((int)f->data[1] - 125) : J1939_TORQUE_NA;
284 out->actual_engine_torque_pct = (f->data[2] <= 0xFAu) ? (int16_t)((int)f->data[2] - 125) : J1939_TORQUE_NA;
285 uint16_t raw = (uint16_t)(f->data[3] | ((uint16_t)f->data[4] << 8)); // little-endian
286 out->engine_speed_valid = (raw <= 0xFAFFu); // >= 0xFB00 is error / not-available
287 out->engine_speed_rpm = (float)raw * 0.125f;
288 return true;
289}
290
291bool pc_j1939_decode_et1(const CanFrame *f, J1939Et1 *out)
292{
293 if (!f || !out || f->dlc < 8)
294 {
295 return false;
296 }
297 J1939Id id;
298 if (!pc_j1939_decode_id(f->id, &id) || id.pgn != J1939_PGN_ET1)
299 {
300 return false;
301 }
302 out->coolant_valid = (f->data[0] <= 0xFAu);
303 out->coolant_temp_c = (float)((int)f->data[0] - 40); // 1 degC/bit, -40 offset
304 out->fuel_valid = (f->data[1] <= 0xFAu);
305 out->fuel_temp_c = (float)((int)f->data[1] - 40);
306 uint16_t oilraw = (uint16_t)(f->data[2] | ((uint16_t)f->data[3] << 8));
307 out->oil_valid = (oilraw <= 0xFAFFu);
308 out->oil_temp_c = (float)oilraw * 0.03125f - 273.0f; // 0.03125 degC/bit, -273 offset
309 return true;
310}
311
312bool pc_j1939_decode_lfe(const CanFrame *f, J1939Lfe *out)
313{
314 if (!f || !out || f->dlc < 8)
315 {
316 return false;
317 }
318 J1939Id id;
319 if (!pc_j1939_decode_id(f->id, &id) || id.pgn != J1939_PGN_LFE)
320 {
321 return false;
322 }
323 uint16_t fr = (uint16_t)(f->data[0] | ((uint16_t)f->data[1] << 8)); // SPN 183, 0.05 L/h/bit
324 out->fuel_rate_valid = (fr <= 0xFAFFu);
325 out->fuel_rate_lph = (float)fr * 0.05f;
326 uint16_t ie = (uint16_t)(f->data[2] | ((uint16_t)f->data[3] << 8)); // SPN 184, 1/512 km/L per bit
327 out->instant_econ_valid = (ie <= 0xFAFFu);
328 out->instant_econ_kmpl = (float)ie * (1.0f / 512.0f);
329 uint16_t ae = (uint16_t)(f->data[4] | ((uint16_t)f->data[5] << 8)); // SPN 185, 1/512 km/L per bit
330 out->avg_econ_valid = (ae <= 0xFAFFu);
331 out->avg_econ_kmpl = (float)ae * (1.0f / 512.0f);
332 out->throttle_valid = (f->data[6] <= 0xFAu); // SPN 51, 0.4 %/bit
333 out->throttle_pct = (float)f->data[6] * 0.4f;
334 return true;
335}
336
337bool pc_j1939_decode_amb(const CanFrame *f, J1939Amb *out)
338{
339 if (!f || !out || f->dlc < 8)
340 {
341 return false;
342 }
343 J1939Id id;
344 if (!pc_j1939_decode_id(f->id, &id) || id.pgn != J1939_PGN_AMB)
345 {
346 return false;
347 }
348 out->baro_valid = (f->data[0] <= 0xFAu); // SPN 108, 0.5 kPa/bit
349 out->baro_kpa = (float)f->data[0] * 0.5f;
350 uint16_t cab = (uint16_t)(f->data[1] | ((uint16_t)f->data[2] << 8)); // SPN 170
351 out->cab_temp_valid = (cab <= 0xFAFFu);
352 out->cab_temp_c = (float)cab * 0.03125f - 273.0f; // 0.03125 degC/bit, -273 offset
353 uint16_t amb = (uint16_t)(f->data[3] | ((uint16_t)f->data[4] << 8)); // SPN 171
354 out->ambient_temp_valid = (amb <= 0xFAFFu);
355 out->ambient_temp_c = (float)amb * 0.03125f - 273.0f;
356 out->inlet_temp_valid = (f->data[5] <= 0xFAu); // SPN 172, 1 degC/bit, -40 offset
357 out->inlet_temp_c = (float)((int)f->data[5] - 40);
358 uint16_t road = (uint16_t)(f->data[6] | ((uint16_t)f->data[7] << 8)); // SPN 79
359 out->road_temp_valid = (road <= 0xFAFFu);
360 out->road_temp_c = (float)road * 0.03125f - 273.0f;
361 return true;
362}
363
364bool pc_j1939_decode_ic1(const CanFrame *f, J1939Ic1 *out)
365{
366 if (!f || !out || f->dlc < 8)
367 {
368 return false;
369 }
370 J1939Id id;
371 if (!pc_j1939_decode_id(f->id, &id) || id.pgn != J1939_PGN_IC1)
372 {
373 return false;
374 }
375 out->trap_inlet_valid = (f->data[0] <= 0xFAu); // SPN 81, 0.5 kPa/bit
376 out->trap_inlet_kpa = (float)f->data[0] * 0.5f;
377 out->boost_valid = (f->data[1] <= 0xFAu); // SPN 102, 2 kPa/bit
378 out->boost_kpa = (float)f->data[1] * 2.0f;
379 out->intake_temp_valid = (f->data[2] <= 0xFAu); // SPN 105, 1 degC/bit, -40 offset
380 out->intake_temp_c = (float)((int)f->data[2] - 40);
381 out->air_inlet_valid = (f->data[3] <= 0xFAu); // SPN 106, 2 kPa/bit
382 out->air_inlet_kpa = (float)f->data[3] * 2.0f;
383 out->air_filter_valid = (f->data[4] <= 0xFAu); // SPN 107, 0.05 kPa/bit
384 out->air_filter_kpa = (float)f->data[4] * 0.05f;
385 uint16_t egt = (uint16_t)(f->data[5] | ((uint16_t)f->data[6] << 8)); // SPN 173
386 out->exhaust_temp_valid = (egt <= 0xFAFFu);
387 out->exhaust_temp_c = (float)egt * 0.03125f - 273.0f; // 0.03125 degC/bit, -273 offset
388 out->coolant_filter_valid = (f->data[7] <= 0xFAu); // SPN 112, 0.5 kPa/bit
389 out->coolant_filter_kpa = (float)f->data[7] * 0.5f;
390 return true;
391}
392
393bool pc_j1939_decode_vd(const CanFrame *f, J1939Vd *out)
394{
395 if (!f || !out || f->dlc < 8)
396 {
397 return false;
398 }
399 J1939Id id;
400 if (!pc_j1939_decode_id(f->id, &id) || id.pgn != J1939_PGN_VD)
401 {
402 return false;
403 }
404 // SPN 244 trip distance + SPN 245 total distance: 4-octet little-endian, 0.125 km/bit.
405 uint32_t trip = (uint32_t)f->data[0] | ((uint32_t)f->data[1] << 8) | ((uint32_t)f->data[2] << 16) |
406 ((uint32_t)f->data[3] << 24);
407 out->trip_valid = (trip <= 0xFAFFFFFFu); // >= 0xFB000000 is error / not-available
408 out->trip_km = (double)trip * 0.125;
409 uint32_t total = (uint32_t)f->data[4] | ((uint32_t)f->data[5] << 8) | ((uint32_t)f->data[6] << 16) |
410 ((uint32_t)f->data[7] << 24);
411 out->total_valid = (total <= 0xFAFFFFFFu);
412 out->total_km = (double)total * 0.125;
413 return true;
414}
415
416bool pc_j1939_decode_ccvs(const CanFrame *f, J1939Ccvs *out)
417{
418 if (!f || !out || f->dlc < 8)
419 {
420 return false;
421 }
422 J1939Id id;
423 if (!pc_j1939_decode_id(f->id, &id) || id.pgn != J1939_PGN_CCVS)
424 {
425 return false;
426 }
427 // SPN 84 Wheel-Based Vehicle Speed: bytes 2-3, little-endian, 1/256 km/h per bit, 0 offset.
428 uint16_t ws = (uint16_t)(f->data[1] | ((uint16_t)f->data[2] << 8));
429 out->speed_valid = (ws <= 0xFAFFu); // >= 0xFB00 is error / not-available
430 out->wheel_speed_kmh = (float)ws * (1.0f / 256.0f);
431 // SPN 595 Cruise Control Active: byte 4, bits 1-2 (the low 2 bits) - a 2-bit state.
432 out->cruise_active = (uint8_t)(f->data[3] & 0x03u);
433 return true;
434}
435
436bool pc_j1939_decode_dm1(const uint8_t *body, size_t len, J1939Dm1 *out, J1939Dtc *out_dtcs, size_t max)
437{
438 if (!body || !out || len < 2) // the lamp-status + flash-status octets
439 {
440 return false;
441 }
442 // Lamp status (J1939-73): 2 bits each, protect (0-1) / amber (2-3) / red-stop (4-5) / MIL (6-7).
443 out->protect = (uint8_t)(body[0] & 0x03u);
444 out->amber_warning = (uint8_t)((body[0] >> 2) & 0x03u);
445 out->red_stop = (uint8_t)((body[0] >> 4) & 0x03u);
446 out->mil = (uint8_t)((body[0] >> 6) & 0x03u);
447 // body[1] is the flash status (same 2-bit layout); not decoded here.
448 size_t ndtc = (len - 2) / 4; // whole 4-octet DTC blocks after the two status octets
449 uint8_t stored = 0;
450 for (size_t i = 0; i < ndtc; i++)
451 {
452 if (stored >= max) // the output buffer is full; stop scanning further DTC blocks
453 {
454 break;
455 }
456 const uint8_t *d = body + 2 + i * 4;
457 uint32_t spn = (uint32_t)d[0] | ((uint32_t)d[1] << 8) | (((uint32_t)d[2] >> 5) << 16);
458 uint8_t fmi = (uint8_t)(d[2] & 0x1Fu);
459 if (spn == 0 && fmi == 0) // the "no active DTC" placeholder
460 {
461 continue;
462 }
463 if (out_dtcs)
464 {
465 out_dtcs[stored].spn = spn;
466 out_dtcs[stored].fmi = fmi;
467 out_dtcs[stored].cm = (uint8_t)((d[3] >> 7) & 0x01u);
468 out_dtcs[stored].oc = (uint8_t)(d[3] & 0x7Fu);
469 }
470 stored++;
471 }
472 out->dtc_count = stored;
473 return true;
474}
475
476#endif // PC_NEED_J1939
#define PC_CAN_MAX_DLC
classic CAN carries at most 8 data octets.
Definition can.h:30
#define PC_CAN_EXT_ID_MASK
29-bit extended identifier.
Definition can.h:32
SAE J1939 message codec (PC_ENABLE_J1939) - the heavy-duty-vehicle / agriculture / marine / genset CA...
Definition can.h:44
uint8_t data[PC_CAN_MAX_DLC]
Definition can.h:49
bool rtr
Definition can.h:47
uint32_t id
Definition can.h:45
uint8_t dlc
Definition can.h:48
bool extended
Definition can.h:46