ProtoCore v0.0.2
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
nmea2000.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 nmea2000.cpp
6 * @brief NMEA 2000 codec (Fast Packet over J1939; pure, host-tested).
7 */
8
10
11#if PC_ENABLE_NMEA2000
12
13#include <string.h>
14
15uint8_t pc_n2k_fastpacket_num_frames(uint16_t total_len)
16{
17 if (total_len <= N2K_FP_F0_DATA)
18 {
19 return 1;
20 }
21 return (uint8_t)(1u + (total_len - N2K_FP_F0_DATA + (N2K_FP_FN_DATA - 1)) / N2K_FP_FN_DATA);
22}
23
24bool pc_n2k_fastpacket_build_frame(CanFrame *out, uint8_t seq, uint8_t frame_idx, uint8_t priority, uint32_t pgn,
25 uint8_t sa, uint8_t da, const uint8_t *data, uint16_t total_len)
26{
27 if (!out || !data || seq > 7 || total_len == 0 || total_len > PC_N2K_FP_MAX)
28 {
29 return false;
30 }
31 if (frame_idx >= pc_n2k_fastpacket_num_frames(total_len))
32 {
33 return false;
34 }
35 uint32_t id;
36 if (!pc_j1939_encode_id(&id, priority, pgn, sa, da))
37 {
38 return false;
39 }
40 out->id = id;
41 out->extended = true;
42 out->rtr = false;
43 out->dlc = PC_CAN_MAX_DLC; // Fast Packet frames are full 8-octet frames
44 memset(out->data, 0xFF, sizeof(out->data)); // pad unused octets with 0xFF
45
46 out->data[0] = (uint8_t)((seq << N2K_FP_SEQ_SHIFT) | (frame_idx & N2K_FP_FRAME_MASK));
47 if (frame_idx == 0)
48 {
49 out->data[1] = (uint8_t)total_len;
50 uint8_t n = total_len < N2K_FP_F0_DATA ? (uint8_t)total_len : (uint8_t)N2K_FP_F0_DATA;
51 memcpy(out->data + 2, data, n);
52 }
53 else
54 {
55 uint16_t off = (uint16_t)(N2K_FP_F0_DATA + (frame_idx - 1) * N2K_FP_FN_DATA);
56 uint16_t remaining = (uint16_t)(total_len - off);
57 uint8_t n = remaining < N2K_FP_FN_DATA ? (uint8_t)remaining : (uint8_t)N2K_FP_FN_DATA;
58 memcpy(out->data + 1, data + off, n);
59 }
60 return true;
61}
62
63void pc_n2k_fastpacket_reset(N2kFastPacketRx *rx)
64{
65 if (rx)
66 {
67 memset(rx, 0, sizeof(*rx));
68 }
69}
70
71N2kFpResult pc_n2k_fastpacket_feed(N2kFastPacketRx *rx, const CanFrame *f)
72{
73 if (!rx || !f || !f->extended || f->dlc < 2)
74 {
75 return N2kFpResult::N2K_FP_IGNORED;
76 }
77 J1939Id id;
78 if (!pc_j1939_decode_id(f->id, &id)) // GCOVR_EXCL_LINE unreachable: pc_j1939_decode_id only fails on a null
79 // out, and &id is non-null
80 {
81 return N2kFpResult::N2K_FP_IGNORED; // GCOVR_EXCL_LINE unreachable: pc_j1939_decode_id only fails on a null
82 // out, and &id is non-null
83 }
84
85 uint8_t seq = (uint8_t)(f->data[0] >> N2K_FP_SEQ_SHIFT);
86 uint8_t frame_idx = (uint8_t)(f->data[0] & N2K_FP_FRAME_MASK);
87
88 if (frame_idx == 0) // first frame: total length + first 6 data octets
89 {
90 uint16_t total = f->data[1];
91 if (total == 0 || total > PC_N2K_FP_MAX)
92 {
93 return N2kFpResult::N2K_FP_ERR;
94 }
95 pc_n2k_fastpacket_reset(rx);
96 rx->active = true;
97 rx->seq = seq;
98 rx->sa = id.sa;
99 rx->pgn = id.pgn;
100 rx->total_len = total;
101 uint8_t n = total < N2K_FP_F0_DATA ? (uint8_t)total : (uint8_t)N2K_FP_F0_DATA;
102 memcpy(rx->buf, f->data + 2, n);
103 rx->received = n;
104 rx->next_frame = 1;
105 if (rx->received >= total)
106 {
107 rx->active = false;
108 return N2kFpResult::N2K_FP_COMPLETE;
109 }
110 return N2kFpResult::N2K_FP_STARTED;
111 }
112
113 // continuation frame: must match the active sequence / source / PGN and be in order.
114 if (!rx->active || seq != rx->seq || id.sa != rx->sa || id.pgn != rx->pgn)
115 {
116 return N2kFpResult::N2K_FP_IGNORED;
117 }
118 if (frame_idx != rx->next_frame)
119 {
120 pc_n2k_fastpacket_reset(rx);
121 return N2kFpResult::N2K_FP_ERR;
122 }
123 uint16_t remaining = (uint16_t)(rx->total_len - rx->received);
124 uint8_t n = remaining < N2K_FP_FN_DATA ? (uint8_t)remaining : (uint8_t)N2K_FP_FN_DATA;
125 memcpy(rx->buf + rx->received, f->data + 1, n);
126 rx->received = (uint16_t)(rx->received + n);
127 rx->next_frame++;
128 if (rx->received >= rx->total_len)
129 {
130 rx->active = false;
131 return N2kFpResult::N2K_FP_COMPLETE;
132 }
133 return N2kFpResult::N2K_FP_PROGRESS;
134}
135
136bool pc_n2k_build_single(CanFrame *out, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da, const uint8_t *data,
137 uint8_t len)
138{
139 return pc_j1939_build_message(out, priority, pgn, sa, da, data, len);
140}
141
142// --- typed PGN decoders ---
143
144namespace
145{
146uint16_t rd_u16le(const uint8_t *p)
147{
148 return (uint16_t)(p[0] | ((uint16_t)p[1] << 8));
149}
150int32_t rd_i32le(const uint8_t *p)
151{
152 return (int32_t)((uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24));
153}
154uint32_t rd_u32le(const uint8_t *p)
155{
156 return (uint32_t)p[0] | ((uint32_t)p[1] << 8) | ((uint32_t)p[2] << 16) | ((uint32_t)p[3] << 24);
157}
158int16_t rd_i16le(const uint8_t *p)
159{
160 return (int16_t)rd_u16le(p);
161}
162} // namespace
163
164bool pc_n2k_decode_position_rapid(const uint8_t *payload, size_t len, N2kPositionRapid *out)
165{
166 if (!payload || !out || len < 8)
167 {
168 return false;
169 }
170 int32_t lat = rd_i32le(payload); // 1e-7 deg/bit
171 int32_t lon = rd_i32le(payload + 4);
172 out->valid = (lat != (int32_t)0x7FFFFFFF && lon != (int32_t)0x7FFFFFFF); // 0x7FFFFFFF = not available
173 out->lat_deg = (double)lat * 1e-7;
174 out->lon_deg = (double)lon * 1e-7;
175 return true;
176}
177
178bool pc_n2k_decode_cog_sog_rapid(const uint8_t *payload, size_t len, N2kCogSogRapid *out)
179{
180 if (!payload || !out || len < 6) // SID(1) + ref(1) + COG(2) + SOG(2)
181 {
182 return false;
183 }
184 out->sid = payload[0];
185 out->cog_ref = (uint8_t)(payload[1] & 0x03u);
186 uint16_t cog = rd_u16le(payload + 2); // 0.0001 rad per bit
187 uint16_t sog = rd_u16le(payload + 4); // 0.01 m/s per bit
188 out->cog_valid = (cog != 0xFFFFu);
189 out->cog_rad = (float)cog * 0.0001f;
190 out->sog_valid = (sog != 0xFFFFu);
191 out->sog_mps = (float)sog * 0.01f;
192 return true;
193}
194
195bool pc_n2k_decode_engine_rapid(const uint8_t *payload, size_t len, N2kEngineRapid *out)
196{
197 if (!payload || !out || len < 6) // instance(1) + speed(2) + boost(2) + tilt(1)
198 {
199 return false;
200 }
201 out->instance = payload[0];
202 uint16_t rpm = rd_u16le(payload + 1); // 0.25 rpm per bit
203 out->speed_valid = (rpm != 0xFFFFu);
204 out->speed_rpm = (float)rpm * 0.25f;
205 uint16_t boost = rd_u16le(payload + 3); // 100 Pa per bit
206 out->boost_valid = (boost != 0xFFFFu);
207 out->boost_pa = (float)boost * 100.0f;
208 out->tilt_valid = (payload[5] != 0x7Fu); // 0x7F = not-available for a signed 1-octet field
209 out->tilt_pct = (int8_t)payload[5];
210 return true;
211}
212
213bool pc_n2k_decode_temperature(const uint8_t *payload, size_t len, N2kTemperature *out)
214{
215 if (!payload || !out || len < 7) // sid(1) + instance(1) + source(1) + actual(2) + set(2)
216 {
217 return false;
218 }
219 out->sid = payload[0];
220 out->instance = payload[1];
221 out->source = payload[2];
222 uint16_t act = rd_u16le(payload + 3); // 0.01 K per bit
223 out->actual_valid = (act != 0xFFFFu);
224 out->actual_c = (float)act * 0.01f - 273.15f; // Kelvin -> Celsius
225 uint16_t set = rd_u16le(payload + 5);
226 out->set_valid = (set != 0xFFFFu);
227 out->set_c = (float)set * 0.01f - 273.15f;
228 return true;
229}
230
231bool pc_n2k_decode_battery_status(const uint8_t *payload, size_t len, N2kBatteryStatus *out)
232{
233 if (!payload || !out || len < 8) // instance(1) + voltage(2) + current(2) + temperature(2) + sid(1)
234 {
235 return false;
236 }
237 out->instance = payload[0];
238 int16_t v = rd_i16le(payload + 1); // 0.01 V per bit, signed (0x7FFF = not available)
239 out->voltage_valid = (v != (int16_t)0x7FFF);
240 out->voltage_v = (float)v * 0.01f;
241 int16_t c = rd_i16le(payload + 3); // 0.1 A per bit, signed
242 out->current_valid = (c != (int16_t)0x7FFF);
243 out->current_a = (float)c * 0.1f;
244 uint16_t t = rd_u16le(payload + 5); // 0.01 K per bit, unsigned (0xFFFF = not available)
245 out->temp_valid = (t != 0xFFFFu);
246 out->temp_c = (float)t * 0.01f - 273.15f; // Kelvin -> Celsius
247 out->sid = payload[7];
248 return true;
249}
250
251bool pc_n2k_decode_fluid_level(const uint8_t *payload, size_t len, N2kFluidLevel *out)
252{
253 if (!payload || !out || len < 7) // instance/type(1) + level(2) + capacity(4)
254 {
255 return false;
256 }
257 out->instance = (uint8_t)(payload[0] & 0x0F); // instance in the low nibble
258 out->fluid_type = (uint8_t)((payload[0] >> 4) & 0x0F); // fluid type in the high nibble
259 int16_t lv = rd_i16le(payload + 1); // 0.004 % per bit, signed (0x7FFF = not available)
260 out->level_valid = (lv != (int16_t)0x7FFF);
261 out->level_pct = (float)lv * 0.004f;
262 uint32_t cap = rd_u32le(payload + 3); // 0.1 L per bit, unsigned (0xFFFFFFFF = not available)
263 out->capacity_valid = (cap != 0xFFFFFFFFu);
264 out->capacity_l = (float)cap * 0.1f;
265 return true;
266}
267
268bool pc_n2k_decode_actual_pressure(const uint8_t *payload, size_t len, N2kActualPressure *out)
269{
270 if (!payload || !out || len < 7) // sid(1) + instance(1) + source(1) + pressure(4)
271 {
272 return false;
273 }
274 out->sid = payload[0];
275 out->instance = payload[1];
276 out->source = payload[2];
277 int32_t p = rd_i32le(payload + 3); // 0.1 Pa per bit, signed (0x7FFFFFFF = not available)
278 out->pressure_valid = (p != (int32_t)0x7FFFFFFF);
279 out->pressure_pa = (float)p * 0.1f;
280 return true;
281}
282
283bool pc_n2k_decode_rudder(const uint8_t *payload, size_t len, N2kRudder *out)
284{
285 if (!payload || !out || len < 6) // instance(1) + direction(1) + angle order(2) + position(2)
286 {
287 return false;
288 }
289 out->instance = payload[0];
290 out->direction_order = (uint8_t)(payload[1] & 0x07u); // low 3 bits
291 int16_t angle = rd_i16le(payload + 2); // 0.0001 rad per bit, signed
292 out->angle_order_valid = ((uint16_t)angle != 0x7FFFu);
293 out->angle_order_rad = (float)angle * 0.0001f;
294 int16_t pos = rd_i16le(payload + 4);
295 out->position_valid = ((uint16_t)pos != 0x7FFFu);
296 out->position_rad = (float)pos * 0.0001f;
297 return true;
298}
299
300bool pc_n2k_decode_attitude(const uint8_t *payload, size_t len, N2kAttitude *out)
301{
302 if (!payload || !out || len < 7) // sid(1) + yaw(2) + pitch(2) + roll(2)
303 {
304 return false;
305 }
306 out->sid = payload[0];
307 int16_t yaw = rd_i16le(payload + 1); // 0.0001 rad per bit, signed
308 out->yaw_valid = ((uint16_t)yaw != 0x7FFFu);
309 out->yaw_rad = (float)yaw * 0.0001f;
310 int16_t pitch = rd_i16le(payload + 3);
311 out->pitch_valid = ((uint16_t)pitch != 0x7FFFu);
312 out->pitch_rad = (float)pitch * 0.0001f;
313 int16_t roll = rd_i16le(payload + 5);
314 out->roll_valid = ((uint16_t)roll != 0x7FFFu);
315 out->roll_rad = (float)roll * 0.0001f;
316 return true;
317}
318
319bool pc_n2k_decode_engine_dynamic(const uint8_t *payload, size_t len, N2kEngineDynamic *out)
320{
321 if (!payload || !out || len < 26)
322 {
323 return false;
324 }
325 out->instance = payload[0];
326 uint16_t oilp = rd_u16le(payload + 1); // 100 Pa per bit
327 out->oil_pressure_valid = (oilp != 0xFFFFu);
328 out->oil_pressure_pa = (float)oilp * 100.0f;
329 uint16_t oilt = rd_u16le(payload + 3); // 0.1 K per bit
330 out->oil_temp_valid = (oilt != 0xFFFFu);
331 out->oil_temp_c = (float)oilt * 0.1f - 273.15f;
332 uint16_t clt = rd_u16le(payload + 5); // 0.01 K per bit
333 out->coolant_temp_valid = (clt != 0xFFFFu);
334 out->coolant_temp_c = (float)clt * 0.01f - 273.15f;
335 int16_t alt = rd_i16le(payload + 7); // 0.01 V per bit, signed
336 out->alt_voltage_valid = ((uint16_t)alt != 0x7FFFu);
337 out->alt_voltage_v = (float)alt * 0.01f;
338 int16_t fr = rd_i16le(payload + 9); // 0.1 L/h per bit, signed
339 out->fuel_rate_valid = ((uint16_t)fr != 0x7FFFu);
340 out->fuel_rate_lph = (float)fr * 0.1f;
341 uint32_t hrs = rd_u32le(payload + 11); // 1 s per bit
342 out->engine_hours_valid = (hrs != 0xFFFFFFFFu);
343 out->engine_hours_s = hrs;
344 uint16_t clp = rd_u16le(payload + 15); // 100 Pa per bit
345 out->coolant_pressure_valid = (clp != 0xFFFFu);
346 out->coolant_pressure_pa = (float)clp * 100.0f;
347 uint16_t fp = rd_u16le(payload + 17); // 1000 Pa per bit
348 out->fuel_pressure_valid = (fp != 0xFFFFu);
349 out->fuel_pressure_pa = (float)fp * 1000.0f;
350 // payload[19] is reserved
351 out->discrete_status_1 = rd_u16le(payload + 20);
352 out->discrete_status_2 = rd_u16le(payload + 22);
353 out->load_valid = (payload[24] != 0x7Fu); // signed 1-octet, 0x7F = not-available
354 out->load_pct = (int8_t)payload[24];
355 out->torque_valid = (payload[25] != 0x7Fu);
356 out->torque_pct = (int8_t)payload[25];
357 return true;
358}
359
360bool pc_n2k_decode_wind_data(const uint8_t *payload, size_t len, N2kWindData *out)
361{
362 if (!payload || !out || len < 6)
363 {
364 return false;
365 }
366 out->sid = payload[0];
367 uint16_t speed = rd_u16le(payload + 1); // 0.01 m/s per bit
368 uint16_t angle = rd_u16le(payload + 3); // 0.0001 rad per bit
369 out->speed_valid = (speed != 0xFFFFu);
370 out->speed_mps = (float)speed * 0.01f;
371 out->angle_valid = (angle != 0xFFFFu);
372 out->angle_rad = (float)angle * 0.0001f;
373 out->reference = (uint8_t)(payload[5] & 0x07u);
374 return true;
375}
376
377bool pc_n2k_decode_speed(const uint8_t *payload, size_t len, N2kSpeed *out)
378{
379 if (!payload || !out || len < 6) // sid(1) + water(2) + ground(2) + type(1)
380 {
381 return false;
382 }
383 out->sid = payload[0];
384 uint16_t w = rd_u16le(payload + 1); // 0.01 m/s per bit
385 out->water_valid = (w != 0xFFFFu);
386 out->water_mps = (float)w * 0.01f;
387 uint16_t g = rd_u16le(payload + 3);
388 out->ground_valid = (g != 0xFFFFu);
389 out->ground_mps = (float)g * 0.01f;
390 out->water_ref_type = payload[5];
391 return true;
392}
393
394bool pc_n2k_decode_water_depth(const uint8_t *payload, size_t len, N2kWaterDepth *out)
395{
396 if (!payload || !out || len < 7) // SID(1) + depth(4) + offset(2)
397 {
398 return false;
399 }
400 out->sid = payload[0];
401 uint32_t depth = rd_u32le(payload + 1); // 0.01 m per bit
402 out->depth_valid = (depth != 0xFFFFFFFFu);
403 out->depth_m = (float)depth * 0.01f;
404 out->offset_m = (float)rd_i16le(payload + 5) * 0.001f; // 0.001 m per bit
405 return true;
406}
407
408bool pc_n2k_decode_vessel_heading(const uint8_t *payload, size_t len, N2kVesselHeading *out)
409{
410 if (!payload || !out || len < 8)
411 {
412 return false;
413 }
414 out->sid = payload[0];
415 uint16_t heading = rd_u16le(payload + 1); // 0.0001 rad per bit
416 out->heading_valid = (heading != 0xFFFFu);
417 out->heading_rad = (float)heading * 0.0001f;
418 out->deviation_rad = (float)rd_i16le(payload + 3) * 0.0001f;
419 out->variation_rad = (float)rd_i16le(payload + 5) * 0.0001f;
420 out->reference = (uint8_t)(payload[7] & 0x03u);
421 return true;
422}
423
424#endif // PC_ENABLE_NMEA2000
#define PC_CAN_MAX_DLC
classic CAN carries at most 8 data octets.
Definition can.h:30
NMEA 2000 codec (PC_ENABLE_NMEA2000) - the marine instrumentation network, built on J1939 over CAN.
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