nmealib 0.0.4
NMEA 0183/NMEA 2000 parsing library
Loading...
Searching...
No Matches
nmea2000.cpp
Go to the documentation of this file.
1#include "nmealib/nmea2000.h"
2
5
6#include <cctype>
7#include <iomanip>
8#include <sstream>
9#include <vector>
10#include <algorithm>
11#include <cstring>
12
13namespace nmealib {
14namespace nmea2000 {
15
29static std::string normalizeRawFormat(const std::string& raw) {
30 if (raw.find(':') != std::string::npos) {
31 return raw;
32 }
33
34 if (raw.find('#') != std::string::npos) {
35 std::string normalized = raw;
36 std::replace(normalized.begin(), normalized.end(), '#', ':');
37 return normalized;
38 }
39
40 size_t commaPos = raw.find(',');
41 if (commaPos != std::string::npos) {
42 std::string canIdPart = raw.substr(0, commaPos);
43 std::string dataPart = raw.substr(commaPos + 1);
44
45 canIdPart.erase(std::remove_if(canIdPart.begin(), canIdPart.end(),
46 [](unsigned char c) { return std::isspace(c); }),
47 canIdPart.end());
48 if (canIdPart.size() >= 2 &&
49 (canIdPart.substr(0, 2) == "0x" || canIdPart.substr(0, 2) == "0X")) {
50 canIdPart = canIdPart.substr(2);
51 }
52
53 std::string dataHex;
54 std::istringstream iss(dataPart);
55 std::string token;
56 while (iss >> token) {
57 if (token.size() >= 2 &&
58 (token.substr(0, 2) == "0x" || token.substr(0, 2) == "0X")) {
59 token = token.substr(2);
60 }
61 dataHex += token;
62 }
63
64 return canIdPart + ":" + dataHex;
65 }
66
67 if (raw.find(' ') != std::string::npos) {
68 std::istringstream iss(raw);
69 std::string token;
70 std::string canId;
71 std::string dataHex;
72 bool firstToken = true;
73
74 while (iss >> token) {
75 if (token.size() >= 2 &&
76 (token.substr(0, 2) == "0x" || token.substr(0, 2) == "0X")) {
77 token = token.substr(2);
78 }
79 if (firstToken) {
80 canId = token;
81 firstToken = false;
82 } else {
83 dataHex += token;
84 }
85 }
86
87 return canId + ":" + dataHex;
88 }
89
90 return raw;
91}
92
93// ---------------------------------------------------------------------------
94// Constructor
95// ---------------------------------------------------------------------------
96
97Message2000::Message2000(std::string raw,
98 TimePoint ts,
99 std::vector<uint8_t> canId,
100 std::vector<uint8_t> canFrame) noexcept
101 : Message(std::move(raw), Type::NMEA2000, ts),
102 canId_(std::move(canId)),
103 canFrame_(std::move(canFrame)) {}
104
105// ---------------------------------------------------------------------------
106// Factory
107// ---------------------------------------------------------------------------
108
109std::unique_ptr<Message2000> Message2000::create(std::string raw, TimePoint ts) {
110 const std::string context = "Message2000::create";
111
112 std::string normalizedRaw = normalizeRawFormat(raw);
113
114 // ---- Split at ':' -------------------------------------------------------
115 size_t colonPos = normalizedRaw.find(':');
116 if (colonPos == std::string::npos) {
117 NMEALIB_RETURN_ERROR(InvalidCanFrameException(context, "This formatting is not supported"));
118 }
119
120 // ---- Parse CAN ID (29-bit value, stored in 4 bytes big-endian) ----------
121 std::string canIdStr = normalizedRaw.substr(0, colonPos);
122 std::transform(canIdStr.begin(), canIdStr.end(), canIdStr.begin(), ::toupper);
123 std::vector<uint8_t> canId;
124
125 if (canIdStr.empty()) {
126 NMEALIB_RETURN_ERROR(InvalidCanFrameException(context, "CAN ID is missing"));
127 }
128
129 unsigned long canIdValue = 0;
130 // A 29-bit value fits in at most 8 hex digits; the top 3 bits of a uint32
131 // must be zero (max valid value is 0x1FFFFFFF).
132 if (!detail::parseUnsigned(canIdStr, canIdValue, 16) || canIdValue > 0x1FFFFFFFU) {
133 NMEALIB_RETURN_ERROR(InvalidCanFrameException(context, "Invalid CAN ID format"));
134 }
135
136 // Store as 4 bytes, big-endian.
137 //
138 // With a 29-bit value right-aligned in a uint32_t the byte layout is:
139 //
140 // canId[0] = bits 28-21: [0 0 0 P3 P2 P1 R1 DP ]
141 // canId[1] = bits 20-13: [PF8 PF7 PF6 PF5 PF4 PF3 PF2 PF1]
142 // canId[2] = bits 12- 5: [PS8 PS7 PS6 PS5 PS4 PS3 PS2 PS1]
143 // canId[3] = bits 4- 0: [SA8 SA7 SA6 SA5 SA4 SA3 SA2 SA1]
144 // (the three MSBs of byte 0 are always 0)
145 //
146 // Note: RTR and DLC are part of the CAN bus framing layer; they are handled
147 // by the hardware/driver and are NOT present in the 29-bit CAN Id.
148 canId.push_back(static_cast<uint8_t>((canIdValue >> 24) & 0xFF)); // [0]
149 canId.push_back(static_cast<uint8_t>((canIdValue >> 16) & 0xFF)); // [1]
150 canId.push_back(static_cast<uint8_t>((canIdValue >> 8) & 0xFF)); // [2]
151 canId.push_back(static_cast<uint8_t>( canIdValue & 0xFF)); // [3]
152
153 // ---- Parse frame data ---------------------------------------------------
154 std::string dataStr = normalizedRaw.substr(colonPos + 1);
155 std::vector<uint8_t> frameData;
156
157 if (!dataStr.empty()) {
158 if (dataStr.length() % 2 != 0) {
160 context, "Frame data must have even number of hex characters"));
161 }
162
163 for (size_t i = 0; i < dataStr.length(); i += 2) {
164 std::string byteStr = dataStr.substr(i, 2);
165 unsigned int byte = 0;
166 if (!detail::parseUnsigned(byteStr, byte, 16) || byte > 0xFFU) {
168 InvalidCanFrameException(context, "Invalid frame data hex format"));
169 }
170 frameData.push_back(static_cast<uint8_t>(byte));
171 }
172 }
173
174 // ---- Validate frame length (0-223 bytes) --------------------------------
175 if (frameData.size() > 223) {
177 context, "Frame length: " + std::to_string(frameData.size())));
178 }
179
180 // ---- Validate PGN -------------------------------------------------------
181 uint32_t pgn = extractPgnFromCanId(canId);
182 if (!isValidPgn(pgn)) {
184 context, "PGN out of valid range: 0x" + std::to_string(pgn)));
185 }
186
187 return std::unique_ptr<Message2000>(
188 new Message2000(std::move(raw), ts, std::move(canId), std::move(frameData)));
189}
190
191// ---------------------------------------------------------------------------
192// PGN extraction
193// ---------------------------------------------------------------------------
194
195uint32_t Message2000::extractPgnFromCanId(const std::vector<uint8_t>& canId) noexcept {
196 // canId layout (4 bytes, big-endian, 29-bit value right-aligned):
197 //
198 // canId[0]: [0 0 0 P3 P2 P1 R1 DP ]
199 // canId[1]: [PF8 PF7 PF6 PF5 PF4 PF3 PF2 PF1] ← full PDU Format byte
200 // canId[2]: [PS8 PS7 PS6 PS5 PS4 PS3 PS2 PS1] ← full PDU Specific byte
201 // canId[3]: [SA8 SA7 SA6 SA5 SA4 SA3 SA2 SA1] ← full Source Address byte
202 //
203 // RDP = { R1, DP } = the two LSBs of canId[0].
204 // PF = canId[1]
205 // PS = canId[2]
206 //
207 // PGN formation (per ISO 11783 / CANboat spec):
208 // PDU1 (PF < 0xF0): PS is the destination address; PGN lower byte is 0.
209 // PGN = [RDP][PF][00]
210 // PDU2 (PF >= 0xF0): destination is always global (255); PS is part of PGN.
211 // PGN = [RDP][PF][PS]
212
213 const uint8_t rdp = canId[0] & 0x03; // bits: R1=1, DP=0
214 const uint8_t pf = canId[1]; // PDU Format
215 const uint8_t ps = canId[2]; // PDU Specific
216
217 if (pf < 0xF0) {
218 // PDU1 — addressed; lower 8 bits of PGN are always zero
219 return (static_cast<uint32_t>(rdp) << 16) |
220 (static_cast<uint32_t>(pf) << 8);
221 } else {
222 // PDU2 — broadcast; PS contributes to the PGN
223 return (static_cast<uint32_t>(rdp) << 16) |
224 (static_cast<uint32_t>(pf) << 8) |
225 static_cast<uint32_t>(ps);
226 }
227}
228
229// ---------------------------------------------------------------------------
230// PGN validation
231// ---------------------------------------------------------------------------
232
233bool Message2000::isValidPgn(uint32_t pgn) noexcept {
234 // PGN is 18 bits wide (RDP=2 + PF=8 + PS=8), so max value is 0x3FFFF.
235 return pgn <= 0x3FFFFU;
236}
237
238// ---------------------------------------------------------------------------
239// Clone
240// ---------------------------------------------------------------------------
241
242std::unique_ptr<nmealib::Message> Message2000::clone() const {
243 return std::unique_ptr<Message2000>(new Message2000(*this));
244}
245
246// ---------------------------------------------------------------------------
247// CAN Id field accessors
248//
249// canId[0]: [0 0 0 P3 P2 P1 R1 DP]
250// canId[1]: [PF8 PF7 PF6 PF5 PF4 PF3 PF2 PF1]
251// canId[2]: [PS8 PS7 PS6 PS5 PS4 PS3 PS2 PS1]
252// canId[3]: [SA8 SA7 SA6 SA5 SA4 SA3 SA2 SA1]
253// ---------------------------------------------------------------------------
254
255// Priority is a 3-bit value in bits [4:2] of canId[0].
256uint8_t Message2000::getPriority() const noexcept {
257 return (getCanId()[0] >> 2) & 0x07;
258}
259
260// Individual priority bits (P3 is the MSB of the 3-bit priority field).
261bool Message2000::getPriority3() const noexcept { return (getCanId()[0] & 0x10) != 0; } // bit 4
262bool Message2000::getPriority2() const noexcept { return (getCanId()[0] & 0x08) != 0; } // bit 3
263bool Message2000::getPriority1() const noexcept { return (getCanId()[0] & 0x04) != 0; } // bit 2
264
265// Reserved bit (R1) is bit 1 of canId[0].
266bool Message2000::getHeaderReserved() const noexcept { return (getCanId()[0] & 0x02) != 0; }
267
268// Data Page (DP) is bit 0 of canId[0].
269bool Message2000::getDataPage() const noexcept { return (getCanId()[0] & 0x01) != 0; }
270
271// PDU Format is the full canId[1] byte.
272uint8_t Message2000::getPDUFormat() const noexcept { return getCanId()[1]; }
273
274// PDU Specific is the full canId[2] byte.
275// When PF < 0xF0 this is the destination address; otherwise it is the PGN group extension.
276uint8_t Message2000::getPDUSpecific() const noexcept { return getCanId()[2]; }
277
278// Source Address is the full canId[3] byte.
279uint8_t Message2000::getSourceAddress() const noexcept { return getCanId()[3]; }
280
281// Destination address: only meaningful for PDU1 messages (PF < 0xF0).
282// For PDU2 messages the destination is always global (255).
283uint8_t Message2000::getDestinationAddress() const noexcept {
284 return (getPDUFormat() < 0xF0) ? getPDUSpecific() : 0xFF;
285}
286
287// NOTE: RTR and DLC are CAN bus framing fields handled by the hardware/driver.
288// They are NOT encoded in the 29-bit CAN Id and therefore cannot be extracted
289// from canId_. The original getRemoteTransmissionRequest() and getDataLengthCode()
290// methods have been removed. If needed they must be stored as separate fields.
291
292// ---------------------------------------------------------------------------
293// PGN accessor
294// ---------------------------------------------------------------------------
295
296uint32_t Message2000::getPgn() const noexcept {
297 return extractPgnFromCanId(getCanId());
298}
299
300// ---------------------------------------------------------------------------
301// Frame accessors
302// ---------------------------------------------------------------------------
303
304const std::vector<uint8_t>& Message2000::getCanId() const noexcept {
305 return canId_;
306}
307
308const std::vector<uint8_t>& Message2000::getCanFrame() const noexcept {
309 return canFrame_;
310}
311
312uint8_t Message2000::getCanFrameLength() const noexcept {
313 return static_cast<uint8_t>(getCanFrame().size());
314}
315
316// ---------------------------------------------------------------------------
317// String representation
318// ---------------------------------------------------------------------------
319
320std::string Message2000::getStringContent(bool verbose) const noexcept {
321 std::ostringstream oss;
322
323 if (verbose) {
324 oss << toString(true) << "\n";
325 } else {
326 oss << toString(false) << "Data=";
327 for (size_t i = 0; i < getCanFrame().size(); ++i) {
328 if (i > 0) oss << " ";
329 oss << std::hex << std::setfill('0') << std::setw(2)
330 << static_cast<int>(getCanFrame()[i]);
331 }
332 oss << std::dec;
333 }
334 return oss.str();
335}
336
337std::string Message2000::toString(bool verbose) const noexcept {
338 std::ostringstream oss;
339
340 if (verbose) {
341 const uint32_t pgn = getPgn();
342 const uint8_t pf = getPDUFormat();
343
344 oss << "--------------------------------\n";
345 oss << "Protocol: " << typeToString(type_) << "\n";
346 oss << "Priority: " << static_cast<int>(getPriority()) << "\n";
347 oss << "Data Page: " << (getDataPage() ? "1" : "0") << "\n";
348 oss << "PDU Format: 0x" << std::hex << std::setw(2) << std::setfill('0')
349 << static_cast<int>(pf) << std::dec
350 << (pf < 0xF0 ? " (PDU1 - addressed)" : " (PDU2 - broadcast)") << "\n";
351 oss << "Destination: ";
352 if (pf < 0xF0) {
353 oss << static_cast<int>(getDestinationAddress());
354 } else {
355 oss << "255 (global)";
356 }
357 oss << "\n";
358 oss << "Source Addr: " << static_cast<int>(getSourceAddress()) << "\n";
359 oss << "PGN: " << pgn
360 << " (0x" << std::hex << pgn << std::dec << ")\n";
361 oss << "Frame Len: " << static_cast<int>(getCanFrameLength()) << " bytes\n";
362 oss << "Frame Data: ";
363 for (size_t i = 0; i < getCanFrame().size(); ++i) {
364 if (i > 0) oss << " ";
365 oss << std::hex << std::setfill('0') << std::setw(2)
366 << static_cast<int>(getCanFrame()[i]);
367 }
368 oss << std::dec;
369 } else {
370 oss << "[OK] " << typeToString(type_) << " PGN" << getPgn() << ": ";
371 }
372
373 return oss.str();
374}
375
376// ---------------------------------------------------------------------------
377// Serialization
378// ---------------------------------------------------------------------------
379
380std::string Message2000::serialize() const {
381 std::ostringstream oss;
382 oss << std::hex << std::uppercase << std::setfill('0');
383
384 for (const auto byte : getCanId()) {
385 oss << std::setw(2) << static_cast<int>(byte);
386 }
387
388 oss << ":";
389
390 for (const auto byte : getCanFrame()) {
391 oss << std::setw(2) << static_cast<int>(byte);
392 }
393
394 return oss.str();
395}
396
397// ---------------------------------------------------------------------------
398// Equality and validation
399// ---------------------------------------------------------------------------
400
401bool Message2000::operator==(const Message2000& other) const noexcept {
402 return getType() == other.getType() &&
403 getCanId() == other.getCanId() &&
404 getCanFrame() == other.getCanFrame();
405}
406
408 if (!isValidPgn(getPgn())) {
409 return false;
410 }
411 if (getCanFrame().size() > 223) {
412 return false;
413 }
414 return true;
415}
416
417} // namespace nmea2000
418} // namespace nmealib
std::chrono::system_clock::time_point TimePoint
Definition message.h:30
Exception thrown when a CAN frame exceeds the maximum length of 223 bytes.
Definition nmea2000.h:36
Exception thrown when the CAN frame is invalid.
Definition nmea2000.h:27
Exception thrown when an NMEA 2000 PGN is outside the valid range.
Definition nmea2000.h:18
Represents a generic NMEA 2000 message encapsulating a CAN frame.
Definition nmea2000.h:62
uint8_t getSourceAddress() const noexcept
Returns the source address of the transmitting device.
Definition nmea2000.cpp:279
Message2000(const Message2000 &)=default
std::vector< uint8_t > canId_
29-bit CAN Id stored as 4 bytes (big-endian)
Definition nmea2000.h:200
const std::vector< uint8_t > & getCanFrame() const noexcept
Returns the raw CAN frame payload (0-223 bytes).
Definition nmea2000.cpp:308
bool getPriority2() const noexcept
Bit 3 of canId_[0].
Definition nmea2000.cpp:262
std::string serialize() const override
Serializes the message to "CANID:data" hex format.
Definition nmea2000.cpp:380
std::vector< uint8_t > canFrame_
CAN frame payload (0-223 bytes)
Definition nmea2000.h:201
uint32_t getPgn() const noexcept
Returns the PGN (Parameter Group Number) extracted from the CAN Id.
Definition nmea2000.cpp:296
bool getPriority3() const noexcept
Returns individual priority bits (P3 is the MSB).
Definition nmea2000.cpp:261
uint8_t getPDUSpecific() const noexcept
Returns the PDU Specific byte (PS).
Definition nmea2000.cpp:276
bool validate() const override
Returns true if the PGN fits in 18 bits and the frame is 0-223 bytes.
Definition nmea2000.cpp:407
bool getHeaderReserved() const noexcept
Returns the Reserved bit (R1).
Definition nmea2000.cpp:266
bool getDataPage() const noexcept
Returns the Data Page bit (DP).
Definition nmea2000.cpp:269
std::string toString(bool verbose) const noexcept
Returns the base string common to all PGNs (used by getStringContent).
Definition nmea2000.cpp:337
std::unique_ptr< nmealib::Message > clone() const override
Creates a polymorphic deep copy of this Message2000.
Definition nmea2000.cpp:242
uint8_t getPDUFormat() const noexcept
Returns the PDU Format byte (PF).
Definition nmea2000.cpp:272
static std::unique_ptr< Message2000 > create(std::string raw, TimePoint ts=std::chrono::system_clock::now())
Protected factory — parses "CANID:data" (and variant formats) into a Message2000.
Definition nmea2000.cpp:109
virtual std::string getStringContent(bool verbose) const noexcept
Returns a human-readable string representation of the message.
Definition nmea2000.cpp:320
const std::vector< uint8_t > & getCanId() const noexcept
Returns the 29-bit CAN identifier as 4 bytes (big-endian).
Definition nmea2000.cpp:304
uint8_t getCanFrameLength() const noexcept
Returns the number of bytes in the CAN frame payload.
Definition nmea2000.cpp:312
virtual bool operator==(const Message2000 &other) const noexcept
Definition nmea2000.cpp:401
bool getPriority1() const noexcept
Bit 2 of canId_[0].
Definition nmea2000.cpp:263
uint8_t getDestinationAddress() const noexcept
Returns the destination address.
Definition nmea2000.cpp:283
uint8_t getPriority() const noexcept
Returns the 3-bit message priority (0 = highest, 7 = lowest).
Definition nmea2000.cpp:256
#define NMEALIB_RETURN_ERROR(exceptionExpr)
bool parseUnsigned(const std::string &text, T &value, int base=10) noexcept
Definition parse.h:15