ProtoCore v0.0.1
Deterministic, zero-heap network stack for embedded targets
Loading...
Searching...
No Matches
nmea2000.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 nmea2000.h
6 * @brief NMEA 2000 codec (PC_ENABLE_NMEA2000) - the marine instrumentation network, built on
7 * J1939 over CAN.
8 *
9 * NMEA 2000 is J1939 at the transport layer (the same 29-bit priority / PGN / source /
10 * destination identifier), so this codec reuses the J1939 id encode / decode
11 * (`PC_ENABLE_NMEA2000` force-enables `PC_ENABLE_J1939`). What it adds is the
12 * NMEA-specific **Fast Packet** transport: messages of 9..223 octets are split across CAN
13 * frames using a per-frame control octet (sequence counter + frame counter) instead of the
14 * J1939 BAM/CMDT protocol. The first frame carries the total length; continuations carry 7
15 * data octets each.
16 *
17 * Pure and host-tested. Drive it from the ESP32 TWAI peripheral or an MCP2515 over SPI to
18 * bridge an NMEA 2000 backbone (GPS, wind, depth, engine PGNs) onto Wi-Fi.
19 *
20 * @author Douglas Quigg (dstroy0)
21 * @date 2026
22 */
23
24#ifndef PROTOCORE_NMEA2000_H
25#define PROTOCORE_NMEA2000_H
26
27#include "protocore_config.h"
28
29#if PC_ENABLE_NMEA2000
30
31#include "services/fieldbus/j1939/j1939.h" // reuses the J1939 29-bit identifier codec
33#include <stddef.h>
34#include <stdint.h>
35
36#define N2K_FP_SEQ_SHIFT 5 ///< control octet: sequence counter in bits 7..5
37#define N2K_FP_FRAME_MASK 0x1Fu ///< control octet: frame counter in bits 4..0
38#define N2K_FP_F0_DATA 6u ///< data octets in the first frame (after control + length octets)
39#define N2K_FP_FN_DATA 7u ///< data octets in a continuation frame
40
41/** @brief Result of feeding a frame to the Fast Packet reassembler. */
42enum class N2kFpResult : uint8_t
43{
44 N2K_FP_IGNORED = 0, ///< not part of the active sequence
45 N2K_FP_STARTED, ///< first frame opened a sequence
46 N2K_FP_PROGRESS, ///< a continuation frame was accepted
47 N2K_FP_COMPLETE, ///< the message is fully reassembled
48 N2K_FP_ERR, ///< out-of-order / too large
49};
50
51/** @brief Fast Packet reassembly context (one in-flight message). */
52struct N2kFastPacketRx
53{
54 bool active;
55 uint8_t seq; ///< sequence counter of the in-progress message
56 uint8_t sa; ///< source address
57 uint32_t pgn; ///< the message PGN
58 uint16_t total_len; ///< announced total length
59 uint16_t received; ///< octets stored so far
60 uint8_t next_frame; ///< next expected frame counter
61 uint8_t buf[PC_N2K_FP_MAX];
62};
63
64/** @brief Number of Fast Packet frames needed for @p total_len octets. */
65uint8_t pc_n2k_fastpacket_num_frames(uint16_t total_len);
66
67/**
68 * @brief Build Fast Packet frame @p frame_idx (0-based) of a message.
69 * @p seq is the 0..7 sequence counter for this message; @p total_len is the whole payload.
70 */
71bool pc_n2k_fastpacket_build_frame(CanFrame *out, uint8_t seq, uint8_t frame_idx, uint8_t priority, uint32_t pgn,
72 uint8_t sa, uint8_t da, const uint8_t *data, uint16_t total_len);
73
74/** @brief Reset a Fast Packet reassembly context to idle. */
75void pc_n2k_fastpacket_reset(N2kFastPacketRx *rx);
76
77/** @brief Feed a received frame to the Fast Packet reassembler; see @ref N2kFpResult. */
78N2kFpResult pc_n2k_fastpacket_feed(N2kFastPacketRx *rx, const CanFrame *f);
79
80/** @brief Build a single-frame (<= 8 octet) NMEA 2000 message (a thin wrap of J1939). */
81bool pc_n2k_build_single(CanFrame *out, uint8_t priority, uint32_t pgn, uint8_t sa, uint8_t da, const uint8_t *data,
82 uint8_t len);
83
84// --- typed decoders for common single-frame PGNs ---
85//
86// These decode a raw PGN payload (a single frame's data[] or a reassembled Fast Packet buffer) into
87// engineering units. The caller matches the PGN off the CAN id first, then calls the matching decoder.
88// NMEA 2000 marks a field "not available" with an all-ones raw (0xFFFF for a U2, 0x7FFFFFFF for the
89// signed lat/lon), which clears the field's validity flag.
90
91#define N2K_PGN_POSITION_RAPID 129025u ///< Position, Rapid Update: latitude + longitude
92#define N2K_PGN_COG_SOG_RAPID 129026u ///< COG & SOG, Rapid Update: course + speed over ground
93#define N2K_PGN_ENGINE_RAPID 127488u ///< Engine Parameters, Rapid Update: speed + boost + tilt/trim
94#define N2K_PGN_ENGINE_DYNAMIC 127489u ///< Engine Parameters, Dynamic: oil / coolant / hours / load / ...
95#define N2K_PGN_WIND_DATA 130306u ///< Wind Data: speed + angle + reference
96#define N2K_PGN_SPEED 128259u ///< Speed: water-referenced + ground-referenced speed
97#define N2K_PGN_WATER_DEPTH 128267u ///< Water Depth: depth below transducer + offset
98#define N2K_PGN_VESSEL_HEADING 127250u ///< Vessel Heading: heading + deviation + variation
99#define N2K_PGN_RUDDER 127245u ///< Rudder: direction order + angle order + position
100#define N2K_PGN_ATTITUDE 127257u ///< Attitude: yaw + pitch + roll
101#define N2K_PGN_TEMPERATURE 130312u ///< Temperature: instance + source + actual / set temperature
102
103// Temperature source (PGN 130312 byte 2).
104#define N2K_TEMP_SRC_SEA 0
105#define N2K_TEMP_SRC_OUTSIDE 1
106#define N2K_TEMP_SRC_INSIDE 2
107#define N2K_TEMP_SRC_ENGINE_ROOM 3
108#define N2K_TEMP_SRC_MAIN_CABIN 4
109#define N2K_TEMP_SRC_LIVE_WELL 5
110#define N2K_TEMP_SRC_BAIT_WELL 6
111#define N2K_TEMP_SRC_REFRIGERATION 7
112#define N2K_TEMP_SRC_HEATING_SYSTEM 8
113#define N2K_TEMP_SRC_FREEZER 13
114#define N2K_TEMP_SRC_EXHAUST_GAS 14
115
116// COG reference (PGN 129026 byte 1, low 2 bits).
117#define N2K_COG_REF_TRUE 0 ///< course over ground referenced to True North
118#define N2K_COG_REF_MAGNETIC 1 ///< course over ground referenced to Magnetic North
119#define N2K_COG_REF_ERROR 2 ///< reference in error
120#define N2K_COG_REF_NULL 3 ///< reference not available / null
121
122// Heading reference (PGN 127250 byte 7, low 2 bits).
123#define N2K_HEADING_REF_TRUE 0 ///< true heading
124#define N2K_HEADING_REF_MAGNETIC 1 ///< magnetic heading
125
126// Wind reference (PGN 130306 byte 5, low 3 bits).
127#define N2K_WIND_REF_TRUE_NORTH 0 ///< true, referenced to North
128#define N2K_WIND_REF_MAGNETIC 1 ///< magnetic, referenced to North
129#define N2K_WIND_REF_APPARENT 2 ///< apparent
130#define N2K_WIND_REF_TRUE_BOAT 3 ///< true, referenced to the vessel (boat)
131#define N2K_WIND_REF_TRUE_WATER 4 ///< true, referenced to the water
132
133/** @brief Decoded Position Rapid Update (PGN 129025). */
134struct N2kPositionRapid
135{
136 bool valid; ///< false if either coordinate is not-available
137 double lat_deg; ///< latitude in decimal degrees (1e-7 deg/bit)
138 double lon_deg; ///< longitude in decimal degrees
139};
140
141/** @brief Decoded COG & SOG Rapid Update (PGN 129026). */
142struct N2kCogSogRapid
143{
144 uint8_t sid; ///< sequence id
145 uint8_t cog_ref; ///< course reference (@ref N2K_COG_REF_TRUE / _MAGNETIC)
146 bool cog_valid; ///< false if the course over ground is not-available
147 float cog_rad; ///< course over ground (radians, 0.0001 rad per bit)
148 bool sog_valid; ///< false if the speed over ground is not-available
149 float sog_mps; ///< speed over ground (m/s, 0.01 m/s per bit)
150};
151
152/** @brief Decoded Engine Parameters, Rapid Update (PGN 127488). */
153struct N2kEngineRapid
154{
155 uint8_t instance; ///< engine instance (0 = single / port, 1 = starboard, ...)
156 bool speed_valid; ///< false if the engine speed is not-available
157 float speed_rpm; ///< engine speed (rpm, 0.25 rpm per bit)
158 bool boost_valid; ///< false if the boost pressure is not-available
159 float boost_pa; ///< engine boost pressure (Pa, 100 Pa per bit)
160 bool tilt_valid; ///< false if the tilt/trim is not-available
161 int8_t tilt_pct; ///< engine tilt / trim (percent, 1 %/bit, signed)
162};
163
164// Rudder direction order (PGN 127245 byte 1, low 3 bits).
165#define N2K_RUDDER_NO_ORDER 0
166#define N2K_RUDDER_MOVE_TO_STARBOARD 1
167#define N2K_RUDDER_MOVE_TO_PORT 2
168
169/** @brief Decoded Rudder (PGN 127245): the commanded and actual rudder state. */
170struct N2kRudder
171{
172 uint8_t instance; ///< rudder instance
173 uint8_t direction_order; ///< commanded direction (@ref N2K_RUDDER_NO_ORDER etc.)
174 bool angle_order_valid; ///< false if the angle order is not-available
175 float angle_order_rad; ///< commanded rudder angle (radians, 0.0001 rad per bit)
176 bool position_valid; ///< false if the position is not-available
177 float position_rad; ///< actual rudder position (radians, 0.0001 rad per bit)
178};
179
180/** @brief Decoded Attitude (PGN 127257): the vessel's orientation. Each angle is signed at 0.0001 rad/bit. */
181struct N2kAttitude
182{
183 uint8_t sid; ///< sequence id
184 bool yaw_valid; ///< false if yaw is not-available
185 float yaw_rad; ///< yaw (radians); + = bow rotating to starboard
186 bool pitch_valid; ///< false if pitch is not-available
187 float pitch_rad; ///< pitch (radians); + = bow up
188 bool roll_valid; ///< false if roll is not-available
189 float roll_rad; ///< roll (radians); + = starboard side down
190};
191
192/** @brief Decoded Temperature (PGN 130312). Temperatures are carried in Kelvin (0.01 K/bit) on the wire and
193 * exposed here in Celsius. */
194struct N2kTemperature
195{
196 uint8_t sid; ///< sequence id
197 uint8_t instance; ///< temperature instance
198 uint8_t source; ///< temperature source (@ref N2K_TEMP_SRC_SEA etc.)
199 bool actual_valid; ///< false if the actual temperature is not-available
200 float actual_c; ///< actual temperature (degrees Celsius)
201 bool set_valid; ///< false if the set/target temperature is not-available
202 float set_c; ///< set / target temperature (degrees Celsius)
203};
204
205/** @brief Decoded Engine Parameters, Dynamic (PGN 127489): the full engine-monitoring picture. Each measured
206 * field clears its validity flag for a not-available raw; the two discrete-status words are raw bitfields. */
207struct N2kEngineDynamic // NOSONAR(cpp:S1820): one decoded PGN is one logical message; the field count mirrors the
208 // protocol's signals, so splitting the struct would be artificial, not clearer
209{
210 uint8_t instance; ///< engine instance
211 bool oil_pressure_valid;
212 float oil_pressure_pa; ///< engine oil pressure (Pa, 100 Pa per bit)
213 bool oil_temp_valid;
214 float oil_temp_c; ///< engine oil temperature (degrees Celsius; 0.1 K/bit on the wire)
215 bool coolant_temp_valid;
216 float coolant_temp_c; ///< engine coolant temperature (degrees Celsius; 0.01 K/bit on the wire)
217 bool alt_voltage_valid;
218 float alt_voltage_v; ///< alternator potential (V, 0.01 V per bit, signed)
219 bool fuel_rate_valid;
220 float fuel_rate_lph; ///< fuel rate (L/h, 0.1 L/h per bit, signed)
221 bool engine_hours_valid;
222 uint32_t engine_hours_s; ///< total engine hours (seconds, 1 s per bit)
223 bool coolant_pressure_valid;
224 float coolant_pressure_pa; ///< coolant pressure (Pa, 100 Pa per bit)
225 bool fuel_pressure_valid;
226 float fuel_pressure_pa; ///< fuel pressure (Pa, 1000 Pa per bit)
227 uint16_t discrete_status_1; ///< discrete status 1 (raw bitfield)
228 uint16_t discrete_status_2; ///< discrete status 2 (raw bitfield)
229 bool load_valid;
230 int8_t load_pct; ///< percent engine load (%, 1 %/bit, signed)
231 bool torque_valid;
232 int8_t torque_pct; ///< percent engine torque (%, 1 %/bit, signed)
233};
234
235/** @brief Decoded Wind Data (PGN 130306). */
236struct N2kWindData
237{
238 uint8_t sid; ///< sequence id
239 bool speed_valid; ///< false if the wind speed is not-available
240 float speed_mps; ///< wind speed (m/s, 0.01 m/s per bit)
241 bool angle_valid; ///< false if the wind angle is not-available
242 float angle_rad; ///< wind angle (radians, 0.0001 rad per bit)
243 uint8_t reference; ///< wind reference (@ref N2K_WIND_REF_TRUE_NORTH etc.)
244};
245
246// Speed water-referenced sensor type (PGN 128259 byte 5).
247#define N2K_SPEED_TYPE_PADDLE_WHEEL 0
248#define N2K_SPEED_TYPE_PITOT_TUBE 1
249#define N2K_SPEED_TYPE_DOPPLER 2
250#define N2K_SPEED_TYPE_CORRELATION 3 ///< correlation / ultrasound
251#define N2K_SPEED_TYPE_ELECTROMAGNETIC 4 ///< electromagnetic
252
253/** @brief Decoded Speed (PGN 128259): through-water and over-ground speed. */
254struct N2kSpeed
255{
256 uint8_t sid; ///< sequence id
257 bool water_valid; ///< false if the water-referenced speed is not-available
258 float water_mps; ///< speed through water (m/s, 0.01 m/s per bit)
259 bool ground_valid; ///< false if the ground-referenced speed is not-available
260 float ground_mps; ///< speed over ground (m/s, 0.01 m/s per bit)
261 uint8_t water_ref_type; ///< water-speed sensor type (@ref N2K_SPEED_TYPE_PADDLE_WHEEL etc.)
262};
263
264/** @brief Decoded Water Depth (PGN 128267). */
265struct N2kWaterDepth
266{
267 uint8_t sid; ///< sequence id
268 bool depth_valid; ///< false if the depth is not-available
269 float depth_m; ///< water depth below the transducer (metres, 0.01 m per bit)
270 float offset_m; ///< transducer offset (m); positive = distance to waterline, negative = to keel
271};
272
273/** @brief Decoded Vessel Heading (PGN 127250). */
274struct N2kVesselHeading
275{
276 uint8_t sid; ///< sequence id
277 bool heading_valid; ///< false if the heading is not-available
278 float heading_rad; ///< heading (radians, 0.0001 rad per bit)
279 float deviation_rad; ///< magnetic deviation (radians)
280 float variation_rad; ///< magnetic variation (radians)
281 uint8_t reference; ///< heading reference (@ref N2K_HEADING_REF_TRUE / _MAGNETIC)
282};
283
284/**
285 * @brief Decode a Position Rapid Update (PGN 129025) payload into @p out.
286 * @return true iff @p len is at least 8 octets; false otherwise.
287 */
288bool pc_n2k_decode_position_rapid(const uint8_t *payload, size_t len, N2kPositionRapid *out);
289
290/**
291 * @brief Decode a COG & SOG Rapid Update (PGN 129026) payload into @p out.
292 * @return true iff @p len is at least 6 octets (SID + reference + COG + SOG); false otherwise.
293 */
294bool pc_n2k_decode_cog_sog_rapid(const uint8_t *payload, size_t len, N2kCogSogRapid *out);
295
296/**
297 * @brief Decode an Engine Parameters Rapid Update (PGN 127488) payload into @p out.
298 * @return true iff @p len is at least 6 octets (instance + speed + boost + tilt); false otherwise.
299 */
300bool pc_n2k_decode_engine_rapid(const uint8_t *payload, size_t len, N2kEngineRapid *out);
301
302/**
303 * @brief Decode an Engine Parameters Dynamic (PGN 127489) payload into @p out. This is a Fast Packet PGN, so
304 * @p payload is the reassembled message body.
305 * @return true iff @p len is at least 26 octets (the full engine-parameters record); false otherwise.
306 */
307bool pc_n2k_decode_engine_dynamic(const uint8_t *payload, size_t len, N2kEngineDynamic *out);
308
309/**
310 * @brief Decode a Rudder (PGN 127245) payload into @p out.
311 * @return true iff @p len is at least 6 octets (instance + direction + angle order + position); false otherwise.
312 */
313bool pc_n2k_decode_rudder(const uint8_t *payload, size_t len, N2kRudder *out);
314
315/**
316 * @brief Decode an Attitude (PGN 127257) payload into @p out.
317 * @return true iff @p len is at least 7 octets (SID + yaw + pitch + roll); false otherwise.
318 */
319bool pc_n2k_decode_attitude(const uint8_t *payload, size_t len, N2kAttitude *out);
320
321/**
322 * @brief Decode a Temperature (PGN 130312) payload into @p out.
323 * @return true iff @p len is at least 7 octets (SID + instance + source + actual + set); false otherwise.
324 */
325bool pc_n2k_decode_temperature(const uint8_t *payload, size_t len, N2kTemperature *out);
326
327/** @brief Decoded Battery Status (PGN 127508): a battery bank's voltage, current, and temperature. Each
328 * measurement clears a validity flag for a not-available raw. */
329struct N2kBatteryStatus
330{
331 uint8_t instance; ///< battery instance
332 bool voltage_valid; ///< false when the raw voltage is in the not-available range
333 float voltage_v; ///< battery voltage (V, 0.01 V/bit)
334 bool current_valid;
335 float current_a; ///< battery current (A, 0.1 A/bit, signed)
336 bool temp_valid;
337 float temp_c; ///< battery temperature (degrees C; carried as Kelvin at 0.01 K/bit on the wire)
338 uint8_t sid; ///< sequence id (correlates related messages)
339};
340
341/**
342 * @brief Decode a Battery Status (PGN 127508) payload into @p out: instance + voltage (2, signed, 0.01 V) +
343 * current (2, signed, 0.1 A) + temperature (2, unsigned, 0.01 K) + SID.
344 * @return true iff @p len is at least 8 octets; false otherwise.
345 */
346bool pc_n2k_decode_battery_status(const uint8_t *payload, size_t len, N2kBatteryStatus *out);
347
348// Fluid / tank type (PGN 127505 byte 0, high nibble).
349#define PC_N2K_FLUID_FUEL 0
350#define PC_N2K_FLUID_WATER 1
351#define PC_N2K_FLUID_GRAY_WATER 2
352#define PC_N2K_FLUID_LIVE_WELL 3
353#define PC_N2K_FLUID_OIL 4
354#define PC_N2K_FLUID_BLACK_WATER 5
355
356/** @brief Decoded Fluid Level (PGN 127505): a tank's fill level and total capacity. */
357struct N2kFluidLevel
358{
359 uint8_t instance; ///< tank instance (0..15)
360 uint8_t fluid_type; ///< fluid / tank type (PC_N2K_FLUID_*)
361 bool level_valid; ///< false when the raw level is in the not-available range
362 float level_pct; ///< tank level as a percentage full (0.004 %/bit)
363 bool capacity_valid;
364 float capacity_l; ///< total tank capacity (litres, 0.1 L/bit)
365};
366
367/**
368 * @brief Decode a Fluid Level (PGN 127505) payload into @p out: the instance + fluid type (packed in byte 0),
369 * the level (2, signed, 0.004 %) and the total capacity (4, unsigned, 0.1 L).
370 * @return true iff @p len is at least 7 octets; false otherwise.
371 */
372bool pc_n2k_decode_fluid_level(const uint8_t *payload, size_t len, N2kFluidLevel *out);
373
374// Pressure source (PGN 130314 byte 2).
375#define PC_N2K_PRESSURE_ATMOSPHERIC 0
376#define PC_N2K_PRESSURE_WATER 1
377#define PC_N2K_PRESSURE_STEAM 2
378#define PC_N2K_PRESSURE_COMPRESSED_AIR 3
379#define PC_N2K_PRESSURE_HYDRAULIC 4
380#define PC_N2K_PRESSURE_FILTER 5
381#define PC_N2K_PRESSURE_ALTIMETER_SETTING 6
382#define PC_N2K_PRESSURE_OIL 7
383#define PC_N2K_PRESSURE_FUEL 8
384
385/** @brief Decoded Actual Pressure (PGN 130314): a measured pressure from one of several sources. */
386struct N2kActualPressure
387{
388 uint8_t sid; ///< sequence id (correlates related messages)
389 uint8_t instance; ///< pressure instance
390 uint8_t source; ///< pressure source (PC_N2K_PRESSURE_*)
391 bool pressure_valid; ///< false when the raw pressure is in the not-available range
392 float pressure_pa; ///< measured pressure (Pa, 0.1 Pa/bit, signed)
393};
394
395/**
396 * @brief Decode an Actual Pressure (PGN 130314) payload into @p out: SID + instance + source + pressure
397 * (4, signed, 0.1 Pa/bit). The trailing reserved octet is not required.
398 * @return true iff @p len is at least 7 octets (SID + instance + source + pressure); false otherwise.
399 */
400bool pc_n2k_decode_actual_pressure(const uint8_t *payload, size_t len, N2kActualPressure *out);
401
402/**
403 * @brief Decode a Wind Data (PGN 130306) payload into @p out.
404 * @return true iff @p len is at least 6 octets; false otherwise.
405 */
406bool pc_n2k_decode_wind_data(const uint8_t *payload, size_t len, N2kWindData *out);
407
408/**
409 * @brief Decode a Speed (PGN 128259) payload into @p out.
410 * @return true iff @p len is at least 6 octets (SID + water speed + ground speed + type); false otherwise.
411 */
412bool pc_n2k_decode_speed(const uint8_t *payload, size_t len, N2kSpeed *out);
413
414/**
415 * @brief Decode a Water Depth (PGN 128267) payload into @p out.
416 * @return true iff @p len is at least 7 octets (SID + depth + offset); false otherwise.
417 */
418bool pc_n2k_decode_water_depth(const uint8_t *payload, size_t len, N2kWaterDepth *out);
419
420/**
421 * @brief Decode a Vessel Heading (PGN 127250) payload into @p out.
422 * @return true iff @p len is at least 8 octets; false otherwise.
423 */
424bool pc_n2k_decode_vessel_heading(const uint8_t *payload, size_t len, N2kVesselHeading *out);
425
426#endif // PC_ENABLE_NMEA2000
427#endif // PROTOCORE_NMEA2000_H
Shared CAN 2.0 frame type for the CAN-based industrial codecs (one source of truth).
SAE J1939 message codec (PC_ENABLE_J1939) - the heavy-duty-vehicle / agriculture / marine / genset CA...
User-facing configuration for ProtoCore.
Definition can.h:44