DeterministicESPAsyncWebServer v6.27.1
Zero-allocation, bounded-execution async HTTP server for ESP32
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 DETWS_ENABLE_J1939
12
13#include <string.h>
14
15bool 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 return false;
19 uint8_t edp = (uint8_t)((pgn >> 17) & 1u);
20 uint8_t dp = (uint8_t)((pgn >> 16) & 1u);
21 uint8_t pf = (uint8_t)((pgn >> 8) & 0xFFu);
22 // PDU1 (peer-to-peer) carries the destination address in PS; PDU2 (broadcast) carries
23 // the PGN's group-extension low octet there.
24 uint8_t ps = (pf < J1939_PDU2_THRESHOLD) ? da : (uint8_t)(pgn & 0xFFu);
25 *id = ((uint32_t)(priority & 7u) << 26) | ((uint32_t)edp << 25) | ((uint32_t)dp << 24) | ((uint32_t)pf << 16) |
26 ((uint32_t)ps << 8) | (uint32_t)sa;
27 return true;
28}
29
30bool j1939_decode_id(uint32_t id, J1939Id *out)
31{
32 if (!out)
33 return false;
35 out->priority = (uint8_t)((id >> 26) & 7u);
36 uint8_t edp = (uint8_t)((id >> 25) & 1u);
37 uint8_t dp = (uint8_t)((id >> 24) & 1u);
38 out->pf = (uint8_t)((id >> 16) & 0xFFu);
39 out->ps = (uint8_t)((id >> 8) & 0xFFu);
40 out->sa = (uint8_t)(id & 0xFFu);
41 out->pdu1 = out->pf < J1939_PDU2_THRESHOLD;
42 if (out->pdu1)
43 {
44 out->da = out->ps;
45 out->pgn = ((uint32_t)edp << 17) | ((uint32_t)dp << 16) | ((uint32_t)out->pf << 8);
46 }
47 else
48 {
49 out->da = J1939_ADDR_GLOBAL;
50 out->pgn = ((uint32_t)edp << 17) | ((uint32_t)dp << 16) | ((uint32_t)out->pf << 8) | out->ps;
51 }
52 return true;
53}
54
55// Fill a CanFrame as a 29-bit extended frame.
56static bool ext_frame(CanFrame *f, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da, uint8_t dlc)
57{
58 uint32_t id;
59 if (!j1939_encode_id(&id, priority, pgn, sa, da))
60 return false;
61 f->id = id;
62 f->extended = true;
63 f->rtr = false;
64 f->dlc = dlc;
65 memset(f->data, 0xFF, sizeof(f->data)); // J1939 pads unused octets with 0xFF (not available)
66 return true;
67}
68
69bool j1939_build_message(CanFrame *out, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da, const uint8_t *data,
70 uint8_t len)
71{
72 if (!out || len > DET_CAN_MAX_DLC || (len && !data))
73 return false;
74 if (!ext_frame(out, priority, pgn, sa, da, len))
75 return false;
76 if (len)
77 memcpy(out->data, data, len);
78 return true;
79}
80
81bool j1939_build_request(CanFrame *out, uint8_t sa, uint8_t da, uint32_t requested_pgn)
82{
83 if (!out || requested_pgn > 0x3FFFFu)
84 return false;
85 // Request PGN (priority 6): 3-octet little-endian requested PGN.
86 if (!ext_frame(out, 6, J1939_PGN_REQUEST, sa, da, 3))
87 return false; // GCOVR_EXCL_LINE unreachable: fixed priority 6 + J1939_PGN_REQUEST (<=0x3FFFF), encode can't
88 // fail
89 out->data[0] = (uint8_t)requested_pgn;
90 out->data[1] = (uint8_t)(requested_pgn >> 8);
91 out->data[2] = (uint8_t)(requested_pgn >> 16);
92 return true;
93}
94
95uint64_t j1939_build_name(bool arbitrary_address_capable, uint8_t industry_group, uint8_t vehicle_system_instance,
96 uint8_t vehicle_system, uint8_t function, uint8_t function_instance, uint8_t ecu_instance,
97 uint16_t manufacturer_code, uint32_t identity_number)
98{
99 // NAME bit layout (J1939-81), LSB first:
100 // [0..20] identity number, [21..31] manufacturer code, [32..34] ECU instance,
101 // [35..39] function instance, [40..47] function, [48] reserved, [49..55] vehicle system,
102 // [56..59] vehicle system instance, [60..62] industry group, [63] arbitrary-address-capable.
103 uint64_t n = 0;
104 n |= (uint64_t)(identity_number & 0x1FFFFFu);
105 n |= (uint64_t)(manufacturer_code & 0x7FFu) << 21;
106 n |= (uint64_t)(ecu_instance & 0x7u) << 32;
107 n |= (uint64_t)(function_instance & 0x1Fu) << 35;
108 n |= (uint64_t)(function & 0xFFu) << 40;
109 n |= (uint64_t)(vehicle_system & 0x7Fu) << 49;
110 n |= (uint64_t)(vehicle_system_instance & 0xFu) << 56;
111 n |= (uint64_t)(industry_group & 0x7u) << 60;
112 n |= (uint64_t)(arbitrary_address_capable ? 1u : 0u) << 63;
113 return n;
114}
115
116bool j1939_build_address_claim(CanFrame *out, uint8_t sa, uint64_t name)
117{
118 // Address Claimed (priority 6, broadcast): NAME as 8 octets, little-endian.
119 if (!ext_frame(out, 6, J1939_PGN_ADDRESS_CLAIM, sa, J1939_ADDR_GLOBAL, 8))
120 return false; // GCOVR_EXCL_LINE unreachable: fixed priority 6 + J1939_PGN_ADDRESS_CLAIM (<=0x3FFFF), encode
121 // can't fail
122 for (int i = 0; i < 8; i++)
123 out->data[i] = (uint8_t)(name >> (8 * i));
124 return true;
125}
126
127uint8_t j1939_tp_num_packets(uint16_t total_size)
128{
129 return (uint8_t)((total_size + (J1939_TP_DT_LEN - 1)) / J1939_TP_DT_LEN);
130}
131
132bool j1939_build_bam_cm(CanFrame *out, uint8_t sa, uint32_t pgn, uint16_t total_size)
133{
134 if (!out || total_size < 9 || total_size > DETWS_J1939_TP_MAX || pgn > 0x3FFFFu)
135 return false; // BAM is for 9..1785 octet messages
136 if (!ext_frame(out, 7, J1939_PGN_TP_CM, sa, J1939_ADDR_GLOBAL, 8))
137 return false; // GCOVR_EXCL_LINE unreachable: fixed priority 7 + J1939_PGN_TP_CM (<=0x3FFFF), encode can't fail
138 out->data[0] = J1939_TP_CM_BAM;
139 out->data[1] = (uint8_t)total_size; // message size, little-endian
140 out->data[2] = (uint8_t)(total_size >> 8);
141 out->data[3] = j1939_tp_num_packets(total_size); // total packets
142 out->data[4] = 0xFF; // reserved
143 out->data[5] = (uint8_t)pgn; // transported PGN, little-endian
144 out->data[6] = (uint8_t)(pgn >> 8);
145 out->data[7] = (uint8_t)(pgn >> 16);
146 return true;
147}
148
149bool j1939_build_tp_dt(CanFrame *out, uint8_t sa, uint8_t da, uint8_t seq, const uint8_t *chunk, uint8_t chunk_len)
150{
151 if (!out || seq == 0 || chunk_len == 0 || chunk_len > J1939_TP_DT_LEN || !chunk)
152 return false;
153 if (!ext_frame(out, 7, J1939_PGN_TP_DT, sa, da, 8))
154 return false; // GCOVR_EXCL_LINE unreachable: fixed priority 7 + J1939_PGN_TP_DT (<=0x3FFFF), encode can't fail
155 out->data[0] = seq; // sequence number, 1-based
156 memcpy(out->data + 1, chunk, chunk_len); // remaining octets stay 0xFF padding
157 return true;
158}
159
160void j1939_tp_reset(J1939TpRx *rx)
161{
162 if (rx)
163 memset(rx, 0, sizeof(*rx));
164}
165
166J1939TpResult j1939_tp_feed(J1939TpRx *rx, const CanFrame *f)
167{
168 if (!rx || !f || !f->extended)
169 return J1939TpResult::J1939_TP_IGNORED;
170 J1939Id id;
171 if (!j1939_decode_id(f->id, &id))
172 return J1939TpResult::J1939_TP_IGNORED; // GCOVR_EXCL_LINE unreachable: decode_id only fails on a null out, and
173 // &id is non-null
174
175 if (id.pgn == J1939_PGN_TP_CM && f->dlc >= 8)
176 {
177 uint8_t control = f->data[0];
178 if (control != J1939_TP_CM_BAM && control != J1939_TP_CM_RTS)
179 return J1939TpResult::J1939_TP_IGNORED; // CTS / EOM / Abort are not receiver-side session starts
180 uint16_t total = (uint16_t)(f->data[1] | (f->data[2] << 8));
181 uint8_t packets = f->data[3];
182 uint32_t pgn = (uint32_t)f->data[5] | ((uint32_t)f->data[6] << 8) | ((uint32_t)f->data[7] << 16);
183 if (total < 9 || total > DETWS_J1939_TP_MAX || packets != j1939_tp_num_packets(total))
184 return J1939TpResult::J1939_TP_ERROR;
185 rx->active = true;
186 rx->sa = id.sa;
187 rx->pgn = pgn;
188 rx->total_size = total;
189 rx->num_packets = packets;
190 rx->next_seq = 1;
191 rx->received = 0;
192 return J1939TpResult::J1939_TP_STARTED;
193 }
194
195 if (id.pgn == J1939_PGN_TP_DT && f->dlc >= 1)
196 {
197 if (!rx->active || id.sa != rx->sa)
198 return J1939TpResult::J1939_TP_IGNORED;
199 uint8_t seq = f->data[0];
200 if (seq != rx->next_seq)
201 {
202 j1939_tp_reset(rx);
203 return J1939TpResult::J1939_TP_ERROR; // out-of-sequence: abort the session
204 }
205 uint16_t remaining = (uint16_t)(rx->total_size - rx->received);
206 uint8_t take = remaining < J1939_TP_DT_LEN ? (uint8_t)remaining : (uint8_t)J1939_TP_DT_LEN;
207 memcpy(rx->buf + rx->received, f->data + 1, take);
208 rx->received = (uint16_t)(rx->received + take);
209 rx->next_seq++;
210 if (rx->received >= rx->total_size)
211 {
212 rx->active = false;
213 return J1939TpResult::J1939_TP_COMPLETE;
214 }
215 return J1939TpResult::J1939_TP_PROGRESS;
216 }
217
218 return J1939TpResult::J1939_TP_IGNORED;
219}
220
221#endif // DETWS_ENABLE_J1939
#define DET_CAN_MAX_DLC
classic CAN carries at most 8 data octets.
Definition can.h:30
#define DET_CAN_EXT_ID_MASK
29-bit extended identifier.
Definition can.h:32
SAE J1939 message codec (DETWS_ENABLE_J1939) - the heavy-duty-vehicle / agriculture / marine / genset...
Definition can.h:44
uint8_t data[DET_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