• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fliuzzi02 / nmealib / 24080499673

07 Apr 2026 12:08PM UTC coverage: 91.86% (+0.1%) from 91.728%
24080499673

push

github

web-flow
Feature/n2k headers (#98)

* [ADD] Refactor Message2000 to use a vector for CAN ID and improve normalization of raw CAN frame strings

* Enhance verbose output formatting for NMEA2000 PGN tests

- Updated expected verbose strings in test cases for PGN 127250, 129025, 129026, and 130306 to include additional fields such as Priority, Data Page, PDU Format, Destination, and Source Address.
- Improved readability of the output by aligning field labels and values.

* Refactor Message2000 to use getCanId() and getCanFrame() for improved consistency and encapsulation

* [ADD] serialization tests for PGN messages with round-trip validation

* [FIX] rename reserved field getter to clarify purpose in PGN129026

82 of 89 new or added lines in 3 files covered. (92.13%)

4 existing lines in 2 files now uncovered.

2923 of 3182 relevant lines covered (91.86%)

26.15 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.92
/src/nmea2000.cpp
1
#include "nmealib/nmea2000.h"
2

3
#include "nmealib/detail/errorSupport.h"
4
#include "nmealib/detail/parse.h"
5

6
#include <cctype>
7
#include <iomanip>
8
#include <sstream>
9
#include <vector>
10
#include <algorithm>
11
#include <cstring>
12

13
namespace nmealib {
14
namespace nmea2000 {
15

16
/**
17
 * @brief Normalizes raw CAN frame string to the canonical "CANID:data" format.
18
 *
19
 * Supports multiple input formats:
20
 * - "CANID:data" (already canonical, returned as-is)
21
 * - "CANID#data" (alternative separator)
22
 * - "0xCANID, 0xBB 0xCC 0xDD ..." (comma-separated with 0x prefix)
23
 * - "0xCANID 0xBB 0xCC 0xDD ..." (space-separated with 0x prefix)
24
 * - "CANID BB CC DD ..." (space-separated without prefix)
25
 *
26
 * @param raw The raw CAN frame string in any supported format.
27
 * @return std::string The normalized string in "CANID:data" format.
28
 */
29
static std::string normalizeRawFormat(const std::string& raw) {
158✔
30
    if (raw.find(':') != std::string::npos) {
158✔
31
        return raw;
32
    }
33

34
    if (raw.find('#') != std::string::npos) {
7✔
35
        std::string normalized = raw;
36
        std::replace(normalized.begin(), normalized.end(), '#', ':');
37
        return normalized;
2✔
38
    }
39

40
    size_t commaPos = raw.find(',');
5✔
41
    if (commaPos != std::string::npos) {
5✔
42
        std::string canIdPart = raw.substr(0, commaPos);
1✔
43
        std::string dataPart  = raw.substr(commaPos + 1);
1✔
44

45
        canIdPart.erase(std::remove_if(canIdPart.begin(), canIdPart.end(),
1✔
46
                                       [](unsigned char c) { return std::isspace(c); }),
4✔
47
                        canIdPart.end());
48
        if (canIdPart.size() >= 2 &&
1✔
49
            (canIdPart.substr(0, 2) == "0x" || canIdPart.substr(0, 2) == "0X")) {
2✔
50
            canIdPart = canIdPart.substr(2);
2✔
51
        }
52

53
        std::string dataHex;
54
        std::istringstream iss(dataPart);
1✔
55
        std::string token;
56
        while (iss >> token) {
5✔
57
            if (token.size() >= 2 &&
4✔
58
                (token.substr(0, 2) == "0x" || token.substr(0, 2) == "0X")) {
8✔
59
                token = token.substr(2);
8✔
60
            }
61
            dataHex += token;
62
        }
63

64
        return canIdPart + ":" + dataHex;
2✔
65
    }
1✔
66

67
    if (raw.find(' ') != std::string::npos) {
4✔
68
        std::istringstream iss(raw);
2✔
69
        std::string token;
70
        std::string canId;
71
        std::string dataHex;
72
        bool firstToken = true;
73

74
        while (iss >> token) {
12✔
75
            if (token.size() >= 2 &&
10✔
76
                (token.substr(0, 2) == "0x" || token.substr(0, 2) == "0X")) {
25✔
77
                token = token.substr(2);
10✔
78
            }
79
            if (firstToken) {
10✔
80
                canId      = token;
81
                firstToken = false;
82
            } else {
83
                dataHex += token;
84
            }
85
        }
86

87
        return canId + ":" + dataHex;
4✔
88
    }
2✔
89

90
    return raw;
91
}
92

93
// ---------------------------------------------------------------------------
94
// Constructor
95
// ---------------------------------------------------------------------------
96

97
Message2000::Message2000(std::string raw,
150✔
98
                         TimePoint ts,
99
                         std::vector<uint8_t> canId,
100
                         std::vector<uint8_t> canFrame) noexcept
150✔
101
    : Message(std::move(raw), Type::NMEA2000, ts),
102
      canId_(std::move(canId)),
103
      canFrame_(std::move(canFrame)) {}
150✔
104

105
// ---------------------------------------------------------------------------
106
// Factory
107
// ---------------------------------------------------------------------------
108

109
std::unique_ptr<Message2000> Message2000::create(std::string raw, TimePoint ts) {
158✔
110
    const std::string context = "Message2000::create";
158✔
111

112
    std::string normalizedRaw = normalizeRawFormat(raw);
158✔
113

114
    // ---- Split at ':' -------------------------------------------------------
115
    size_t colonPos = normalizedRaw.find(':');
158✔
116
    if (colonPos == std::string::npos) {
158✔
117
        NMEALIB_RETURN_ERROR(InvalidCanFrameException(context, "This formatting is not supported"));
4✔
118
    }
119

120
    // ---- Parse CAN ID (29-bit value, stored in 4 bytes big-endian) ----------
121
    std::string canIdStr = normalizedRaw.substr(0, colonPos);
156✔
122
    std::transform(canIdStr.begin(), canIdStr.end(), canIdStr.begin(), ::toupper);
123
    std::vector<uint8_t> canId;
156✔
124

125
    if (canIdStr.empty()) {
156✔
126
        NMEALIB_RETURN_ERROR(InvalidCanFrameException(context, "CAN ID is missing"));
2✔
127
    }
128

129
    unsigned long canIdValue = 0;
155✔
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) {
155✔
133
        NMEALIB_RETURN_ERROR(InvalidCanFrameException(context, "Invalid CAN ID format"));
4✔
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]
153✔
149
    canId.push_back(static_cast<uint8_t>((canIdValue >> 16) & 0xFF)); // [1]
153✔
150
    canId.push_back(static_cast<uint8_t>((canIdValue >>  8) & 0xFF)); // [2]
153✔
151
    canId.push_back(static_cast<uint8_t>( canIdValue        & 0xFF)); // [3]
153✔
152

153
    // ---- Parse frame data ---------------------------------------------------
154
    std::string dataStr = normalizedRaw.substr(colonPos + 1);
153✔
155
    std::vector<uint8_t> frameData;
153✔
156

157
    if (!dataStr.empty()) {
153✔
158
        if (dataStr.length() % 2 != 0) {
150✔
159
            NMEALIB_RETURN_ERROR(InvalidCanFrameException(
2✔
160
                context, "Frame data must have even number of hex characters"));
161
        }
162

163
        for (size_t i = 0; i < dataStr.length(); i += 2) {
1,624✔
164
            std::string byteStr = dataStr.substr(i, 2);
1,476✔
165
            unsigned int byte   = 0;
1,476✔
166
            if (!detail::parseUnsigned(byteStr, byte, 16) || byte > 0xFFU) {
1,476✔
167
                NMEALIB_RETURN_ERROR(
2✔
168
                    InvalidCanFrameException(context, "Invalid frame data hex format"));
169
            }
170
            frameData.push_back(static_cast<uint8_t>(byte));
1,476✔
171
        }
172
    }
173

174
    // ---- Validate frame length (0-223 bytes) --------------------------------
175
    if (frameData.size() > 223) {
151✔
176
        NMEALIB_RETURN_ERROR(FrameTooLongException(
3✔
177
            context, "Frame length: " + std::to_string(frameData.size())));
178
    }
179

180
    // ---- Validate PGN -------------------------------------------------------
181
    uint32_t pgn = extractPgnFromCanId(canId);
182
    if (!isValidPgn(pgn)) {
183
        NMEALIB_RETURN_ERROR(InvalidPgnException(
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)));
450✔
189
}
309✔
190

191
// ---------------------------------------------------------------------------
192
// PGN extraction
193
// ---------------------------------------------------------------------------
194

NEW
195
uint32_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
163✔
214
    const uint8_t pf  = canId[1];        // PDU Format
163✔
215
    const uint8_t ps  = canId[2];        // PDU Specific
163✔
216

217
    if (pf < 0xF0) {
163✔
218
        // PDU1 — addressed; lower 8 bits of PGN are always zero
219
        return (static_cast<uint32_t>(rdp) << 16) |
46✔
220
               (static_cast<uint32_t>(pf)  <<  8);
46✔
221
    } else {
222
        // PDU2 — broadcast; PS contributes to the PGN
223
        return (static_cast<uint32_t>(rdp) << 16) |
117✔
224
               (static_cast<uint32_t>(pf)  <<  8) |
117✔
225
                static_cast<uint32_t>(ps);
117✔
226
    }
227
}
228

229
// ---------------------------------------------------------------------------
230
// PGN validation
231
// ---------------------------------------------------------------------------
232

UNCOV
233
bool Message2000::isValidPgn(uint32_t pgn) noexcept {
×
234
    // PGN is 18 bits wide (RDP=2 + PF=8 + PS=8), so max value is 0x3FFFF.
NEW
235
    return pgn <= 0x3FFFFU;
×
236
}
237

238
// ---------------------------------------------------------------------------
239
// Clone
240
// ---------------------------------------------------------------------------
241

242
std::unique_ptr<nmealib::Message> Message2000::clone() const {
2✔
243
    return std::unique_ptr<Message2000>(new Message2000(*this));
2✔
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].
256
uint8_t Message2000::getPriority() const noexcept {
5✔
257
    return (getCanId()[0] >> 2) & 0x07;
20✔
258
}
259

260
// Individual priority bits (P3 is the MSB of the 3-bit priority field).
261
bool Message2000::getPriority3() const noexcept { return (getCanId()[0] & 0x10) != 0; } // bit 4
4✔
262
bool Message2000::getPriority2() const noexcept { return (getCanId()[0] & 0x08) != 0; } // bit 3
4✔
263
bool Message2000::getPriority1() const noexcept { return (getCanId()[0] & 0x04) != 0; } // bit 2
4✔
264

265
// Reserved bit (R1) is bit 1 of canId[0].
266
bool Message2000::getReserved() const noexcept  { return (getCanId()[0] & 0x02) != 0; }
3✔
267

268
// Data Page (DP) is bit 0 of canId[0].
269
bool Message2000::getDataPage() const noexcept  { return (getCanId()[0] & 0x01) != 0; }
18✔
270

271
// PDU Format is the full canId[1] byte.
272
uint8_t Message2000::getPDUFormat() const noexcept   { return getCanId()[1]; }
4✔
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.
NEW
276
uint8_t Message2000::getPDUSpecific() const noexcept { return getCanId()[2]; }
×
277

278
// Source Address is the full canId[3] byte.
279
uint8_t Message2000::getSourceAddress() const noexcept { return getCanId()[3]; }
19✔
280

281
// Destination address: only meaningful for PDU1 messages (PF < 0xF0).
282
// For PDU2 messages the destination is always global (255).
NEW
283
uint8_t Message2000::getDestinationAddress() const noexcept {
×
NEW
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

296
uint32_t Message2000::getPgn() const noexcept {
139✔
297
    return extractPgnFromCanId(getCanId());
139✔
298
}
299

300
// ---------------------------------------------------------------------------
301
// Frame accessors
302
// ---------------------------------------------------------------------------
303

304
const std::vector<uint8_t>& Message2000::getCanId() const noexcept {
2✔
305
    return canId_;
17✔
306
}
307

308
const std::vector<uint8_t>& Message2000::getCanFrame() const noexcept {
317✔
309
    return canFrame_;
331✔
310
}
311

312
uint8_t Message2000::getCanFrameLength() const noexcept {
56✔
313
    return static_cast<uint8_t>(getCanFrame().size());
71✔
314
}
315

316
// ---------------------------------------------------------------------------
317
// String representation
318
// ---------------------------------------------------------------------------
319

320
std::string Message2000::getStringContent(bool verbose) const noexcept {
1✔
321
    std::ostringstream oss;
1✔
322

323
    if (verbose) {
1✔
NEW
324
        oss << toString(true) << "\n";
×
325
    } else {
326
        oss << toString(false) << "Data=";
2✔
327
        for (size_t i = 0; i < getCanFrame().size(); ++i) {
3✔
328
            if (i > 0) oss << " ";
2✔
329
            oss << std::hex << std::setfill('0') << std::setw(2)
330
                << static_cast<int>(getCanFrame()[i]);
2✔
331
        }
332
        oss << std::dec;
333
    }
334
    return oss.str();
1✔
335
}
1✔
336

337
std::string Message2000::toString(bool verbose) const noexcept {
24✔
338
    std::ostringstream oss;
24✔
339

340
    if (verbose) {
24✔
341
        const uint32_t pgn = getPgn();
342
        const uint8_t  pf  = getPDUFormat();
343

344
        oss << "--------------------------------\n";
15✔
345
        oss << "Protocol:    " << typeToString(type_) << "\n";
45✔
346
        oss << "Priority:    " << static_cast<int>(getPriority()) << "\n";
15✔
347
        oss << "Data Page:   " << (getDataPage() ? "1" : "0") << "\n";
15✔
348
        oss << "PDU Format:  0x" << std::hex << std::setw(2) << std::setfill('0')
349
            << static_cast<int>(pf) << std::dec
15✔
350
            << (pf < 0xF0 ? " (PDU1 - addressed)" : " (PDU2 - broadcast)") << "\n";
30✔
351
        oss << "Destination: ";
15✔
352
        if (pf < 0xF0) {
15✔
NEW
353
            oss << static_cast<int>(getDestinationAddress());
×
354
        } else {
355
            oss << "255 (global)";
15✔
356
        }
357
        oss << "\n";
15✔
358
        oss << "Source Addr: " << static_cast<int>(getSourceAddress()) << "\n";
15✔
359
        oss << "PGN:         " << pgn
360
            << " (0x" << std::hex << pgn << std::dec << ")\n";
15✔
361
        oss << "Frame Len:   " << static_cast<int>(getCanFrameLength()) << " bytes\n";
15✔
362
        oss << "Frame Data:  ";
15✔
363
        for (size_t i = 0; i < getCanFrame().size(); ++i) {
135✔
364
            if (i > 0) oss << " ";
120✔
365
            oss << std::hex << std::setfill('0') << std::setw(2)
366
                << static_cast<int>(getCanFrame()[i]);
120✔
367
        }
368
        oss << std::dec;
369
    } else {
370
        oss << "[OK] " << typeToString(type_) << " PGN" << getPgn() << ": ";
27✔
371
    }
372

373
    return oss.str();
24✔
374
}
24✔
375

376
// ---------------------------------------------------------------------------
377
// Serialization
378
// ---------------------------------------------------------------------------
379

380
std::string Message2000::serialize() const {
12✔
381
    std::ostringstream oss;
12✔
382
    oss << std::hex << std::uppercase << std::setfill('0');
383

384
    for (const auto byte : getCanId()) {
60✔
385
        oss << std::setw(2) << static_cast<int>(byte);
48✔
386
    }
387

388
    oss << ":";
12✔
389

390
    for (const auto byte : getCanFrame()) {
96✔
391
        oss << std::setw(2) << static_cast<int>(byte);
84✔
392
    }
393

394
    return oss.str();
12✔
395
}
12✔
396

397
// ---------------------------------------------------------------------------
398
// Equality and validation
399
// ---------------------------------------------------------------------------
400

401
bool Message2000::operator==(const Message2000& other) const noexcept {
15✔
402
    return getType() == other.getType() &&
15✔
403
           getCanId() == other.getCanId() &&
15✔
404
           getCanFrame() == other.getCanFrame();
14✔
405
}
406

407
bool Message2000::validate() const {
8✔
408
    if (!isValidPgn(getPgn())) {
409
        return false;
410
    }
411
    if (getCanFrame().size() > 223) {
8✔
UNCOV
412
        return false;
×
413
    }
414
    return true;
415
}
416

417
} // namespace nmea2000
418
} // namespace nmealib
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc