Compare commits
4
Commits
v1.0
..
88b5883dda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
88b5883dda | ||
|
|
3bd9339baf | ||
|
|
69074e3321 | ||
|
|
2c057b631d |
No files matched your search
@@ -151,8 +151,11 @@ Files:
|
|||||||
|
|
||||||
- `dbc_parser.h`
|
- `dbc_parser.h`
|
||||||
- `dbc_parser.cpp`
|
- `dbc_parser.cpp`
|
||||||
|
- `dbc_statement_handler.h`
|
||||||
|
- `dbc_statement_handler.cpp`
|
||||||
|
- `typefactory.h`
|
||||||
|
|
||||||
These parse the DBC file itself.
|
The parser reads files and reports errors; registered statement handlers parse DBC constructs.
|
||||||
|
|
||||||
### Demo
|
### Demo
|
||||||
|
|
||||||
@@ -414,6 +417,34 @@ Fields:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Raw value representation and precision
|
||||||
|
|
||||||
|
`DecodedSignalValue::rawValue` is a C++17
|
||||||
|
`std::variant<std::int64_t, std::uint64_t>`. For a valid signal, signed definitions
|
||||||
|
produce the `int64_t` alternative and unsigned definitions produce the `uint64_t`
|
||||||
|
alternative. Raw values remain exact across their full 64-bit ranges. Check
|
||||||
|
`valid` before using the value; an invalid result's default value is not decoded data.
|
||||||
|
|
||||||
|
Use `std::get` when the numeric type is known, or `std::visit` to handle both:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
std::visit([] (auto raw) { std::cout << raw; }, decodedSignal.rawValue);
|
||||||
|
```
|
||||||
|
|
||||||
|
This replaces the previous `int64_t` field: consumers that read or print
|
||||||
|
`rawValue` directly must handle the variant.
|
||||||
|
|
||||||
|
Physical conversion uses the active numeric type:
|
||||||
|
`physicalValue = static_cast<double>(raw) * factor + offset`.
|
||||||
|
`physicalValue` remains a `double` and is approximate for large integers.
|
||||||
|
On usual IEEE 754 binary64 implementations, integers are exactly representable
|
||||||
|
through 2^53; above that, some integers round to the same floating-point value.
|
||||||
|
For example, unsigned `UINT64_MAX` remains exact in `rawValue`, but converts to
|
||||||
|
approximately 18446744073709551616 in binary64 before scaling.
|
||||||
|
Use `rawValue` whenever exact integer values are required.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## `DecodedFrameValue`
|
## `DecodedFrameValue`
|
||||||
|
|
||||||
Represents one decoded frame result.
|
Represents one decoded frame result.
|
||||||
@@ -477,6 +508,36 @@ Parsed fields:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
### Frame validation and parse errors
|
||||||
|
|
||||||
|
`BO_` definitions require a decimal CAN ID, a nonempty single-token frame name,
|
||||||
|
a colon, a decimal payload length, and a transmitter token. Spaces or tabs may
|
||||||
|
separate fields, and whitespace before the colon is accepted. Additional trailing
|
||||||
|
fields are rejected.
|
||||||
|
|
||||||
|
Numeric fields must contain decimal digits only; signs, hexadecimal notation,
|
||||||
|
fractional values, suffixes, and overflow are rejected. CAN IDs may contain the
|
||||||
|
29-bit identifier plus the optional DBC extended flag (bit 31). Bits 29 and 30
|
||||||
|
are rejected rather than silently discarded. For compatibility, unflagged IDs
|
||||||
|
above `0x7FF` are still treated as extended.
|
||||||
|
|
||||||
|
The payload length must fit `uint8_t` (`0..255`). This is a storage-range check,
|
||||||
|
not CAN or CAN FD protocol validation; no new 8-byte or 64-byte limit is imposed.
|
||||||
|
|
||||||
|
Parsing stops at the first detected error. Exceptions raised while parsing a
|
||||||
|
line are reported as `std::runtime_error` with the supplied file path and the
|
||||||
|
one-based physical line number, including blank lines:
|
||||||
|
|
||||||
|
```text
|
||||||
|
example.dbc:3: Invalid CAN ID: invalid
|
||||||
|
```
|
||||||
|
|
||||||
|
This change validates frame definitions. Signal syntax and unsupported DBC
|
||||||
|
constructs retain their existing behavior; comprehensive validation is still
|
||||||
|
planned. File-open errors contain the path but have no line number.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Signal definition
|
## Signal definition
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
@@ -709,20 +770,47 @@ That is why `DecodeDatabase` is a separate layer.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Why No Abstract Factory Is Used
|
## Statement Handler Factory
|
||||||
|
|
||||||
At the current stage, abstract factory is intentionally avoided.
|
`DbcParser` owns file reading, trimming, line numbers, and error reporting.
|
||||||
|
Each `ParseFile()` call creates a fresh `DbcParseContext` and a
|
||||||
|
`DbcStatementDispatcher`. The context contains the parsed database and current
|
||||||
|
frame; handlers receive it by reference and retain no parsing state.
|
||||||
|
|
||||||
The current design is already clean:
|
The dispatcher uses `TypeFactory<std::string, DbcStatementHandler>` to create
|
||||||
|
registered handlers once per parsing session, then reuses them for all matching
|
||||||
|
lines:
|
||||||
|
|
||||||
```text
|
- `BO_` → `DbcFrameHandler`
|
||||||
parser → parsed database → decode database
|
- `SG_` → `DbcSignalHandler`
|
||||||
↘ tree builder → UI tree
|
- `CM_` → `DbcCommentHandler`
|
||||||
|
|
||||||
|
Grammar implementations live in `dbc_statement_handler.cpp`; their interface,
|
||||||
|
context, and dispatcher live in `dbc_statement_handler.h`. `typefactory.h`
|
||||||
|
provides the generic registry and object creation.
|
||||||
|
|
||||||
|
To add a construct, implement a default-constructible handler deriving from
|
||||||
|
`DbcStatementHandler` with:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void Handle(const std::string &line, DbcParseContext &context) const override;
|
||||||
```
|
```
|
||||||
|
|
||||||
Introducing factory layers now would increase complexity without solving an immediate problem.
|
Register it in the dispatcher constructor:
|
||||||
|
|
||||||
If later the project requires multiple output representations or multiple build strategies, that can be added then.
|
```cpp
|
||||||
|
RegisterType<DbcValueHandler>("VAL_"); // after implementing DbcValueHandler
|
||||||
|
```
|
||||||
|
|
||||||
|
Registration creates one handler; replacing a registration replaces that instance.
|
||||||
|
Register during initialization before dispatching. A line is routed using its
|
||||||
|
full first whitespace-delimited keyword, so `BO_TX_BU_` cannot match `BO_`.
|
||||||
|
Unregistered keywords remain ignored. Existing `SG_` and `CM_` syntax policies
|
||||||
|
are preserved, including requiring a literal space after those keywords.
|
||||||
|
|
||||||
|
This factory handles statements inside DBC files. A separate format-level
|
||||||
|
factory can be introduced when another file format is implemented. The decoder
|
||||||
|
and database builders remain independent of statement dispatch.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -839,3 +927,10 @@ DBC parser → DbcDatabase → DecodeDatabase → DbcDecoder
|
|||||||
```
|
```
|
||||||
|
|
||||||
This gives FrameTap a much better foundation for real use, because both browsing and decoding are supported without forcing one representation to do the other's job.
|
This gives FrameTap a much better foundation for real use, because both browsing and decoding are supported without forcing one representation to do the other's job.
|
||||||
|
|
||||||
|
## Regression tests
|
||||||
|
|
||||||
|
See [tests/README.md](tests/README.md) for compiler and qmake commands.
|
||||||
|
Tests cover frame validation and error locations, Intel and Motorola extraction,
|
||||||
|
signed and unsigned 64-bit boundaries,
|
||||||
|
physical conversion, numeric output, and invalid signal payloads.
|
||||||
@@ -7,6 +7,7 @@ SOURCES += \
|
|||||||
dbc_decode_builder.cpp \
|
dbc_decode_builder.cpp \
|
||||||
dbc_decoder.cpp \
|
dbc_decoder.cpp \
|
||||||
dbc_parser.cpp \
|
dbc_parser.cpp \
|
||||||
|
dbc_statement_handler.cpp \
|
||||||
dbc_tree_builder.cpp \
|
dbc_tree_builder.cpp \
|
||||||
main.cpp \
|
main.cpp \
|
||||||
tree_node.cpp
|
tree_node.cpp
|
||||||
@@ -16,6 +17,8 @@ HEADERS += \
|
|||||||
dbc_decode_builder.h \
|
dbc_decode_builder.h \
|
||||||
dbc_decoder.h \
|
dbc_decoder.h \
|
||||||
dbc_parser.h \
|
dbc_parser.h \
|
||||||
|
dbc_statement_handler.h \
|
||||||
|
typefactory.h \
|
||||||
dbc_tree_builder.h \
|
dbc_tree_builder.h \
|
||||||
decode_database.h \
|
decode_database.h \
|
||||||
frame_info.h \
|
frame_info.h \
|
||||||
|
|||||||
+19
-21
@@ -55,10 +55,12 @@ DecodedFrameValue DbcDecoder::Decode (const DecodeDatabase &database,
|
|||||||
if (signal.valueType == ValueType::Signed)
|
if (signal.valueType == ValueType::Signed)
|
||||||
decoded.rawValue = SignExtend (unsignedValue, signal.length);
|
decoded.rawValue = SignExtend (unsignedValue, signal.length);
|
||||||
else
|
else
|
||||||
decoded.rawValue = static_cast<std::int64_t> (unsignedValue);
|
decoded.rawValue = unsignedValue;
|
||||||
|
|
||||||
decoded.physicalValue =
|
decoded.physicalValue =
|
||||||
(static_cast<double> (decoded.rawValue) * signal.factor) + signal.offset;
|
std::visit ([&signal] (auto rawValue) {
|
||||||
|
return (static_cast<double> (rawValue) * signal.factor) + signal.offset;
|
||||||
|
}, decoded.rawValue);
|
||||||
decoded.valid = true;
|
decoded.valid = true;
|
||||||
|
|
||||||
result.signals.push_back (decoded);
|
result.signals.push_back (decoded);
|
||||||
@@ -111,47 +113,43 @@ bool DbcDecoder::ExtractMotorola (const std::vector<std::uint8_t> &data,
|
|||||||
* - startBit points to the most significant bit of the signal
|
* - startBit points to the most significant bit of the signal
|
||||||
* - inside a byte, bit numbering goes 7..0
|
* - inside a byte, bit numbering goes 7..0
|
||||||
* - crossing byte boundary moves to the next byte, bit 7
|
* - crossing byte boundary moves to the next byte, bit 7
|
||||||
|
* Example: a signal starting at 7 follows 7..0, 15..8, 23..16.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
value = 0U;
|
value = 0U;
|
||||||
|
|
||||||
std::int32_t currentBit = static_cast<std::int32_t> (startBit);
|
std::size_t byteIndex = startBit / 8U;
|
||||||
|
std::uint32_t bitInByte = startBit % 8U;
|
||||||
|
|
||||||
for (std::uint32_t bitIndex = 0U; bitIndex < length; ++bitIndex) {
|
for (std::uint32_t bitIndex = 0U; bitIndex < length; ++bitIndex) {
|
||||||
if (currentBit < 0)
|
|
||||||
return false;
|
|
||||||
|
|
||||||
const std::uint32_t absoluteBit = static_cast<std::uint32_t> (currentBit);
|
|
||||||
const std::uint32_t byteIndex = absoluteBit / 8U;
|
|
||||||
const std::uint32_t bitFromMsb = absoluteBit % 8U;
|
|
||||||
const std::uint32_t bitInByte = 7U - bitFromMsb;
|
|
||||||
|
|
||||||
if (byteIndex >= data.size())
|
if (byteIndex >= data.size())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
value <<= 1U;
|
value <<= 1U;
|
||||||
value |= static_cast<std::uint64_t> ((data[byteIndex] >> bitInByte) & 0x01U);
|
value |= static_cast<std::uint64_t> ((data[byteIndex] >> bitInByte) & 0x01U);
|
||||||
|
|
||||||
if ((absoluteBit % 8U) == 7U)
|
if (bitInByte == 0U) {
|
||||||
currentBit = static_cast<std::int32_t> ((byteIndex + 1U) * 8U);
|
++byteIndex;
|
||||||
else
|
bitInByte = 7U;
|
||||||
--currentBit;
|
} else
|
||||||
|
--bitInByte;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::int64_t DbcDecoder::SignExtend (std::uint64_t value, std::uint32_t bitLength) {
|
std::int64_t DbcDecoder::SignExtend (std::uint64_t value, std::uint32_t bitLength) {
|
||||||
if ((bitLength == 0U) || (bitLength >= 64U))
|
if ((bitLength == 0U) || (bitLength > 64U))
|
||||||
return static_cast<std::int64_t> (value);
|
return 0;
|
||||||
|
|
||||||
const std::uint64_t signMask = (static_cast<std::uint64_t> (1U) << (bitLength - 1U));
|
|
||||||
const std::uint64_t valueMask = (static_cast<std::uint64_t> (1U) << bitLength) - 1U;
|
|
||||||
|
|
||||||
|
const std::uint64_t signMask = std::uint64_t (1U) << (bitLength - 1U);
|
||||||
|
const std::uint64_t valueMask = (bitLength == 64U)
|
||||||
|
? ~std::uint64_t (0U) : (std::uint64_t (1U) << bitLength) - 1U;
|
||||||
value &= valueMask;
|
value &= valueMask;
|
||||||
|
|
||||||
if ((value & signMask) == 0U)
|
if ((value & signMask) == 0U)
|
||||||
return static_cast<std::int64_t> (value);
|
return static_cast<std::int64_t> (value);
|
||||||
|
|
||||||
return static_cast<std::int64_t> (value | (~valueMask));
|
// The complemented magnitude fits int64_t, including for INT64_MIN.
|
||||||
|
return -1 - static_cast<std::int64_t> ((~value) & valueMask);
|
||||||
}
|
}
|
||||||
+3
-2
@@ -14,6 +14,7 @@
|
|||||||
|
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
#include "decode_database.h"
|
#include "decode_database.h"
|
||||||
|
|
||||||
@@ -37,13 +38,13 @@ struct RawCanFrame {
|
|||||||
*/
|
*/
|
||||||
struct DecodedSignalValue {
|
struct DecodedSignalValue {
|
||||||
const DecodeSignal *definition; /**< Signal definition. */
|
const DecodeSignal *definition; /**< Signal definition. */
|
||||||
std::int64_t rawValue; /**< Extracted raw integer value. */
|
std::variant<std::int64_t, std::uint64_t> rawValue; /**< Exact signed or unsigned raw value. */
|
||||||
double physicalValue; /**< Converted physical value. */
|
double physicalValue; /**< Converted physical value. */
|
||||||
bool valid; /**< true if decoding succeeded. */
|
bool valid; /**< true if decoding succeeded. */
|
||||||
|
|
||||||
DecodedSignalValue()
|
DecodedSignalValue()
|
||||||
: definition (nullptr)
|
: definition (nullptr)
|
||||||
, rawValue (0)
|
, rawValue (std::uint64_t (0U))
|
||||||
, physicalValue (0.0)
|
, physicalValue (0.0)
|
||||||
, valid (false) {
|
, valid (false) {
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-297
@@ -10,9 +10,9 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
#include "dbc_parser.h"
|
#include "dbc_parser.h"
|
||||||
|
#include "dbc_statement_handler.h"
|
||||||
|
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <sstream>
|
|
||||||
#include <stdexcept>
|
#include <stdexcept>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
|
||||||
@@ -43,314 +43,28 @@ DbcDatabase DbcParser::ParseFile (const std::string &filePath) const {
|
|||||||
throw std::runtime_error ("Failed to open DBC file: " + filePath);
|
throw std::runtime_error ("Failed to open DBC file: " + filePath);
|
||||||
|
|
||||||
DbcDatabase database;
|
DbcDatabase database;
|
||||||
FrameInfo *currentFrame = nullptr;
|
DbcParseContext context {database, nullptr};
|
||||||
|
const DbcStatementDispatcher dispatcher;
|
||||||
|
|
||||||
|
std::size_t lineNumber = 0U;
|
||||||
std::string line;
|
std::string line;
|
||||||
while (std::getline (input, line)) {
|
while (std::getline (input, line)) {
|
||||||
|
++lineNumber;
|
||||||
line = Trim (line);
|
line = Trim (line);
|
||||||
if (line.empty())
|
if (line.empty())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if (IsFrameLine (line)) {
|
try {
|
||||||
FrameInfo frame = ParseFrameLine (line);
|
dispatcher.Dispatch (line, context);
|
||||||
database.frames.push_back (frame);
|
} catch (const std::exception &error) {
|
||||||
currentFrame = &database.frames.back();
|
throw std::runtime_error (filePath + ":" + std::to_string (lineNumber) +
|
||||||
} else if (IsSignalLine (line)) {
|
": " + error.what());
|
||||||
if (currentFrame == nullptr)
|
}
|
||||||
throw std::runtime_error ("Signal found before any frame definition.");
|
|
||||||
|
|
||||||
SignalInfo signal = ParseSignalLine (line);
|
|
||||||
currentFrame->signals.push_back (signal);
|
|
||||||
} else if (IsCommentLine (line))
|
|
||||||
ParseCommentLine (line, database);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return database;
|
return database;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool DbcParser::IsFrameLine (const std::string &line) {
|
|
||||||
return (line.size() >= 4U) && (line.compare (0U, 4U, "BO_ ") == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DbcParser::IsSignalLine (const std::string &line) {
|
|
||||||
return (line.size() >= 4U) && (line.compare (0U, 4U, "SG_ ") == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool DbcParser::IsCommentLine (const std::string &line) {
|
|
||||||
return (line.size() >= 4U) && (line.compare (0U, 4U, "CM_ ") == 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string DbcParser::Trim (const std::string &text) {
|
std::string DbcParser::Trim (const std::string &text) {
|
||||||
return TrimText (text);
|
return TrimText (text);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> DbcParser::SplitReceivers (const std::string &text) {
|
|
||||||
std::vector<std::string> receivers;
|
|
||||||
std::string token;
|
|
||||||
std::istringstream stream (text);
|
|
||||||
|
|
||||||
while (std::getline (stream, token, ',')) {
|
|
||||||
token = TrimText (token);
|
|
||||||
if (!token.empty())
|
|
||||||
receivers.push_back (token);
|
|
||||||
}
|
|
||||||
|
|
||||||
return receivers;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DbcParser::NormalizeCanId (std::uint32_t rawCanId,
|
|
||||||
std::uint32_t &normalizedCanId,
|
|
||||||
bool &isExtended) {
|
|
||||||
/*
|
|
||||||
* DBC commonly stores extended identifiers with bit 31 set.
|
|
||||||
* Example:
|
|
||||||
* raw id = 0x80000000 | actual_29_bit_id
|
|
||||||
*/
|
|
||||||
if ((rawCanId & 0x80000000U) != 0U) {
|
|
||||||
isExtended = true;
|
|
||||||
normalizedCanId = (rawCanId & 0x1FFFFFFFU);
|
|
||||||
} else {
|
|
||||||
isExtended = (rawCanId > 0x7FFU);
|
|
||||||
normalizedCanId = rawCanId;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
std::uint32_t DbcParser::TryExtractPgn (std::uint32_t canId, bool isExtended, bool &hasPgn) {
|
|
||||||
hasPgn = false;
|
|
||||||
|
|
||||||
if (!isExtended)
|
|
||||||
return 0U;
|
|
||||||
|
|
||||||
if ((canId & 0x1FFFFFFFU) != canId)
|
|
||||||
return 0U;
|
|
||||||
|
|
||||||
const std::uint32_t pf = (canId >> 16U) & 0xFFU;
|
|
||||||
const std::uint32_t ps = (canId >> 8U) & 0xFFU;
|
|
||||||
const std::uint32_t dp = (canId >> 24U) & 0x01U;
|
|
||||||
|
|
||||||
std::uint32_t pgn = 0U;
|
|
||||||
|
|
||||||
if (pf < 240U)
|
|
||||||
pgn = (dp << 16U) | (pf << 8U);
|
|
||||||
else
|
|
||||||
pgn = (dp << 16U) | (pf << 8U) | ps;
|
|
||||||
|
|
||||||
hasPgn = true;
|
|
||||||
return pgn;
|
|
||||||
}
|
|
||||||
|
|
||||||
FrameInfo DbcParser::ParseFrameLine (const std::string &line) {
|
|
||||||
/*
|
|
||||||
* Example:
|
|
||||||
* BO_ 256 EngineData: 8 EEC1
|
|
||||||
*/
|
|
||||||
|
|
||||||
std::istringstream stream (line);
|
|
||||||
std::string token;
|
|
||||||
FrameInfo frame;
|
|
||||||
std::uint32_t rawCanId = 0U;
|
|
||||||
|
|
||||||
stream >> token;
|
|
||||||
if (token != "BO_")
|
|
||||||
throw std::runtime_error ("Invalid frame line: " + line);
|
|
||||||
|
|
||||||
stream >> rawCanId;
|
|
||||||
NormalizeCanId (rawCanId, frame.canId, frame.isExtended);
|
|
||||||
|
|
||||||
stream >> token;
|
|
||||||
if (token.empty())
|
|
||||||
throw std::runtime_error ("Missing frame name: " + line);
|
|
||||||
|
|
||||||
if (token[token.size() - 1U] == ':')
|
|
||||||
token.erase (token.size() - 1U, 1U);
|
|
||||||
|
|
||||||
frame.name = token;
|
|
||||||
|
|
||||||
{
|
|
||||||
unsigned int dlcValue = 0U;
|
|
||||||
stream >> dlcValue;
|
|
||||||
frame.dlc = static_cast<std::uint8_t> (dlcValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
stream >> frame.transmitter;
|
|
||||||
frame.pgn = TryExtractPgn (frame.canId, frame.isExtended, frame.hasPgn);
|
|
||||||
|
|
||||||
return frame;
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalInfo DbcParser::ParseSignalLine (const std::string &line) {
|
|
||||||
/*
|
|
||||||
* Example:
|
|
||||||
* SG_ EngineSpeed : 0|16@1+ (0.125,0) [0|8000] "rpm" ECU1,ECU2
|
|
||||||
*/
|
|
||||||
|
|
||||||
SignalInfo signal;
|
|
||||||
|
|
||||||
std::string work = TrimText (line);
|
|
||||||
if (work.compare (0U, 4U, "SG_ ") != 0)
|
|
||||||
throw std::runtime_error ("Invalid signal line: " + line);
|
|
||||||
|
|
||||||
work.erase (0U, 4U);
|
|
||||||
|
|
||||||
const std::string::size_type colonPos = work.find (':');
|
|
||||||
if (colonPos == std::string::npos)
|
|
||||||
throw std::runtime_error ("Signal line missing ':' : " + line);
|
|
||||||
|
|
||||||
signal.name = TrimText (work.substr (0U, colonPos));
|
|
||||||
std::string rest = TrimText (work.substr (colonPos + 1U));
|
|
||||||
|
|
||||||
const std::string::size_type pipePos = rest.find ('|');
|
|
||||||
const std::string::size_type atPos = rest.find ('@');
|
|
||||||
const std::string::size_type signPos = rest.find_first_of ("+-", atPos);
|
|
||||||
const std::string::size_type factorBegin = rest.find ('(');
|
|
||||||
const std::string::size_type factorComma = rest.find (',', factorBegin);
|
|
||||||
const std::string::size_type factorEnd = rest.find (')', factorComma);
|
|
||||||
const std::string::size_type rangeBegin = rest.find ('[');
|
|
||||||
const std::string::size_type rangeSep = rest.find ('|', rangeBegin);
|
|
||||||
const std::string::size_type rangeEnd = rest.find (']', rangeSep);
|
|
||||||
const std::string::size_type unitBegin = rest.find ('"', rangeEnd);
|
|
||||||
const std::string::size_type unitEnd = rest.find ('"', unitBegin + 1U);
|
|
||||||
|
|
||||||
if ((pipePos == std::string::npos) ||
|
|
||||||
(atPos == std::string::npos) ||
|
|
||||||
(signPos == std::string::npos) ||
|
|
||||||
(factorBegin == std::string::npos) ||
|
|
||||||
(factorComma == std::string::npos) ||
|
|
||||||
(factorEnd == std::string::npos) ||
|
|
||||||
(rangeBegin == std::string::npos) ||
|
|
||||||
(rangeSep == std::string::npos) ||
|
|
||||||
(rangeEnd == std::string::npos) ||
|
|
||||||
(unitBegin == std::string::npos) ||
|
|
||||||
(unitEnd == std::string::npos))
|
|
||||||
throw std::runtime_error ("Unsupported signal syntax: " + line);
|
|
||||||
|
|
||||||
signal.startBit = static_cast<std::uint32_t> (
|
|
||||||
std::stoul (TrimText (rest.substr (0U, pipePos)))
|
|
||||||
);
|
|
||||||
|
|
||||||
signal.length = static_cast<std::uint32_t> (
|
|
||||||
std::stoul (TrimText (rest.substr (pipePos + 1U, atPos - pipePos - 1U)))
|
|
||||||
);
|
|
||||||
|
|
||||||
{
|
|
||||||
if ((atPos + 1U) >= rest.size())
|
|
||||||
throw std::runtime_error ("Invalid endianness in signal: " + line);
|
|
||||||
|
|
||||||
const char endianChar = rest[atPos + 1U];
|
|
||||||
signal.isLittleEndian = (endianChar == '1');
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
const char signChar = rest[signPos];
|
|
||||||
signal.isSigned = (signChar == '-');
|
|
||||||
}
|
|
||||||
|
|
||||||
signal.factor = std::stod (
|
|
||||||
TrimText (rest.substr (factorBegin + 1U, factorComma - factorBegin - 1U))
|
|
||||||
);
|
|
||||||
|
|
||||||
signal.offset = std::stod (
|
|
||||||
TrimText (rest.substr (factorComma + 1U, factorEnd - factorComma - 1U))
|
|
||||||
);
|
|
||||||
|
|
||||||
signal.minimum = std::stod (
|
|
||||||
TrimText (rest.substr (rangeBegin + 1U, rangeSep - rangeBegin - 1U))
|
|
||||||
);
|
|
||||||
|
|
||||||
signal.maximum = std::stod (
|
|
||||||
TrimText (rest.substr (rangeSep + 1U, rangeEnd - rangeSep - 1U))
|
|
||||||
);
|
|
||||||
|
|
||||||
signal.unit = rest.substr (unitBegin + 1U, unitEnd - unitBegin - 1U);
|
|
||||||
|
|
||||||
{
|
|
||||||
const std::string receiversText = TrimText (rest.substr (unitEnd + 1U));
|
|
||||||
signal.receivers = SplitReceivers (receiversText);
|
|
||||||
}
|
|
||||||
|
|
||||||
return signal;
|
|
||||||
}
|
|
||||||
|
|
||||||
void DbcParser::ParseCommentLine (const std::string &line, DbcDatabase &database) {
|
|
||||||
/*
|
|
||||||
* Examples:
|
|
||||||
* CM_ BO_ 256 "Frame comment";
|
|
||||||
* CM_ SG_ 256 EngineSpeed "Signal comment";
|
|
||||||
*/
|
|
||||||
|
|
||||||
std::istringstream stream (line);
|
|
||||||
std::string token;
|
|
||||||
stream >> token;
|
|
||||||
|
|
||||||
if (token != "CM_")
|
|
||||||
return;
|
|
||||||
|
|
||||||
stream >> token;
|
|
||||||
|
|
||||||
if (token == "BO_") {
|
|
||||||
std::uint32_t rawCanId = 0U;
|
|
||||||
std::uint32_t canId = 0U;
|
|
||||||
bool isExtended = false;
|
|
||||||
|
|
||||||
stream >> rawCanId;
|
|
||||||
NormalizeCanId (rawCanId, canId, isExtended);
|
|
||||||
|
|
||||||
const std::string::size_type quoteBegin = line.find ('"');
|
|
||||||
const std::string::size_type quoteEnd = line.rfind ('"');
|
|
||||||
|
|
||||||
if ((quoteBegin == std::string::npos) ||
|
|
||||||
(quoteEnd == std::string::npos) ||
|
|
||||||
(quoteEnd <= quoteBegin))
|
|
||||||
return;
|
|
||||||
|
|
||||||
FrameInfo *frame = FindFrameById (database, canId, isExtended);
|
|
||||||
if (frame != nullptr)
|
|
||||||
frame->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U);
|
|
||||||
} else if (token == "SG_") {
|
|
||||||
std::uint32_t rawCanId = 0U;
|
|
||||||
std::uint32_t canId = 0U;
|
|
||||||
bool isExtended = false;
|
|
||||||
std::string signalName;
|
|
||||||
|
|
||||||
stream >> rawCanId;
|
|
||||||
stream >> signalName;
|
|
||||||
|
|
||||||
NormalizeCanId (rawCanId, canId, isExtended);
|
|
||||||
|
|
||||||
const std::string::size_type quoteBegin = line.find ('"');
|
|
||||||
const std::string::size_type quoteEnd = line.rfind ('"');
|
|
||||||
|
|
||||||
if ((quoteBegin == std::string::npos) ||
|
|
||||||
(quoteEnd == std::string::npos) ||
|
|
||||||
(quoteEnd <= quoteBegin))
|
|
||||||
return;
|
|
||||||
|
|
||||||
FrameInfo *frame = FindFrameById (database, canId, isExtended);
|
|
||||||
if (frame != nullptr) {
|
|
||||||
SignalInfo *signal = FindSignalByName (*frame, signalName);
|
|
||||||
if (signal != nullptr)
|
|
||||||
signal->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
FrameInfo *DbcParser::FindFrameById (DbcDatabase &database,
|
|
||||||
std::uint32_t canId,
|
|
||||||
bool isExtended) {
|
|
||||||
for (std::size_t index = 0U; index < database.frames.size(); ++index) {
|
|
||||||
if ((database.frames[index].canId == canId) &&
|
|
||||||
(database.frames[index].isExtended == isExtended))
|
|
||||||
return &database.frames[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
SignalInfo *DbcParser::FindSignalByName (FrameInfo &frame, const std::string &signalName) {
|
|
||||||
for (std::size_t index = 0U; index < frame.signals.size(); ++index) {
|
|
||||||
if (frame.signals[index].name == signalName)
|
|
||||||
return &frame.signals[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
+1
-22
@@ -13,8 +13,6 @@
|
|||||||
#define DBC_PARSER_H
|
#define DBC_PARSER_H
|
||||||
|
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
|
||||||
#include <cstdint>
|
|
||||||
|
|
||||||
#include "dbc_database.h"
|
#include "dbc_database.h"
|
||||||
|
|
||||||
@@ -39,31 +37,12 @@ class DbcParser {
|
|||||||
* @param filePath Path to input file.
|
* @param filePath Path to input file.
|
||||||
* @return Parsed database.
|
* @return Parsed database.
|
||||||
* @throws std::runtime_error on file or parse errors.
|
* @throws std::runtime_error on file or parse errors.
|
||||||
|
* Line parse errors include the file path and one-based line number.
|
||||||
*/
|
*/
|
||||||
DbcDatabase ParseFile (const std::string &filePath) const;
|
DbcDatabase ParseFile (const std::string &filePath) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
static bool IsFrameLine (const std::string &line);
|
|
||||||
static bool IsSignalLine (const std::string &line);
|
|
||||||
static bool IsCommentLine (const std::string &line);
|
|
||||||
static std::string Trim (const std::string &text);
|
static std::string Trim (const std::string &text);
|
||||||
static std::vector<std::string> SplitReceivers (const std::string &text);
|
|
||||||
static std::uint32_t TryExtractPgn (std::uint32_t canId, bool isExtended, bool &hasPgn);
|
|
||||||
|
|
||||||
static void NormalizeCanId (std::uint32_t rawCanId,
|
|
||||||
std::uint32_t &normalizedCanId,
|
|
||||||
bool &isExtended);
|
|
||||||
|
|
||||||
static FrameInfo ParseFrameLine (const std::string &line);
|
|
||||||
static SignalInfo ParseSignalLine (const std::string &line);
|
|
||||||
|
|
||||||
static void ParseCommentLine (const std::string &line, DbcDatabase &database);
|
|
||||||
|
|
||||||
static FrameInfo *FindFrameById (DbcDatabase &database,
|
|
||||||
std::uint32_t canId,
|
|
||||||
bool isExtended);
|
|
||||||
|
|
||||||
static SignalInfo *FindSignalByName (FrameInfo &frame, const std::string &signalName);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif /* DBC_PARSER_H */
|
#endif /* DBC_PARSER_H */
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
#include "dbc_statement_handler.h"
|
||||||
|
#include <sstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <cctype>
|
||||||
|
#include <charconv>
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::uint32_t ParseDecimal (const std::string &text,
|
||||||
|
std::uint32_t maximum,
|
||||||
|
const char *field) {
|
||||||
|
std::uint32_t value = 0U;
|
||||||
|
const auto parsed = std::from_chars (text.data(), text.data() + text.size(), value);
|
||||||
|
if (text.empty() || parsed.ec != std::errc() ||
|
||||||
|
parsed.ptr != text.data() + text.size() || value > maximum)
|
||||||
|
throw std::runtime_error (std::string ("Invalid ") + field + ": " + text);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Remove leading and trailing spaces.
|
||||||
|
* @param text Input text.
|
||||||
|
* @return Trimmed text.
|
||||||
|
*/
|
||||||
|
std::string TrimText (const std::string &text) {
|
||||||
|
std::string::size_type begin = 0U;
|
||||||
|
while ((begin < text.size()) &&
|
||||||
|
std::isspace (static_cast<unsigned char> (text[begin])))
|
||||||
|
++begin;
|
||||||
|
|
||||||
|
std::string::size_type end = text.size();
|
||||||
|
while ((end > begin) &&
|
||||||
|
std::isspace (static_cast<unsigned char> (text[end - 1U])))
|
||||||
|
--end;
|
||||||
|
|
||||||
|
return text.substr (begin, end - begin);
|
||||||
|
}
|
||||||
|
FrameInfo *FindFrameById (DbcDatabase &, std::uint32_t, bool);
|
||||||
|
SignalInfo *FindSignalByName (FrameInfo &, const std::string &);
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
std::vector<std::string> SplitReceivers (const std::string &text) {
|
||||||
|
std::vector<std::string> receivers;
|
||||||
|
std::string token;
|
||||||
|
std::istringstream stream (text);
|
||||||
|
|
||||||
|
while (std::getline (stream, token, ',')) {
|
||||||
|
token = TrimText (token);
|
||||||
|
if (!token.empty())
|
||||||
|
receivers.push_back (token);
|
||||||
|
}
|
||||||
|
|
||||||
|
return receivers;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NormalizeCanId (std::uint32_t rawCanId,
|
||||||
|
std::uint32_t &normalizedCanId,
|
||||||
|
bool &isExtended) {
|
||||||
|
/*
|
||||||
|
* DBC commonly stores extended identifiers with bit 31 set.
|
||||||
|
* Example:
|
||||||
|
* raw id = 0x80000000 | actual_29_bit_id
|
||||||
|
*/
|
||||||
|
if ((rawCanId & 0x80000000U) != 0U) {
|
||||||
|
isExtended = true;
|
||||||
|
normalizedCanId = (rawCanId & 0x1FFFFFFFU);
|
||||||
|
} else {
|
||||||
|
isExtended = (rawCanId > 0x7FFU);
|
||||||
|
normalizedCanId = rawCanId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::uint32_t TryExtractPgn (std::uint32_t canId, bool isExtended, bool &hasPgn) {
|
||||||
|
hasPgn = false;
|
||||||
|
|
||||||
|
if (!isExtended)
|
||||||
|
return 0U;
|
||||||
|
|
||||||
|
if ((canId & 0x1FFFFFFFU) != canId)
|
||||||
|
return 0U;
|
||||||
|
|
||||||
|
const std::uint32_t pf = (canId >> 16U) & 0xFFU;
|
||||||
|
const std::uint32_t ps = (canId >> 8U) & 0xFFU;
|
||||||
|
const std::uint32_t dp = (canId >> 24U) & 0x01U;
|
||||||
|
|
||||||
|
std::uint32_t pgn = 0U;
|
||||||
|
|
||||||
|
if (pf < 240U)
|
||||||
|
pgn = (dp << 16U) | (pf << 8U);
|
||||||
|
else
|
||||||
|
pgn = (dp << 16U) | (pf << 8U) | ps;
|
||||||
|
|
||||||
|
hasPgn = true;
|
||||||
|
return pgn;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
FrameInfo DbcFrameHandler::ParseFrameLine (const std::string &line) {
|
||||||
|
/*
|
||||||
|
* Example:
|
||||||
|
* BO_ 256 EngineData: 8 EEC1
|
||||||
|
*/
|
||||||
|
|
||||||
|
std::istringstream stream (line);
|
||||||
|
std::string keyword;
|
||||||
|
std::string idText;
|
||||||
|
if (!(stream >> keyword >> idText) || keyword != "BO_")
|
||||||
|
throw std::runtime_error ("Missing CAN ID in frame definition.");
|
||||||
|
|
||||||
|
const std::uint32_t rawCanId = ParseDecimal (
|
||||||
|
idText, std::numeric_limits<std::uint32_t>::max(), "CAN ID");
|
||||||
|
// Only bit 31 may accompany the actual 29-bit identifier.
|
||||||
|
if ((rawCanId & 0x60000000U) != 0U)
|
||||||
|
throw std::runtime_error ("CAN ID exceeds the 29-bit identifier range.");
|
||||||
|
|
||||||
|
std::string nameText;
|
||||||
|
if (!std::getline (stream, nameText, ':') || stream.eof())
|
||||||
|
throw std::runtime_error ("Missing ':' in frame definition.");
|
||||||
|
std::istringstream nameStream (nameText);
|
||||||
|
FrameInfo frame;
|
||||||
|
std::string extra;
|
||||||
|
if (!(nameStream >> frame.name) || (nameStream >> extra))
|
||||||
|
throw std::runtime_error ("Missing or invalid frame name.");
|
||||||
|
|
||||||
|
std::string lengthText;
|
||||||
|
if (!(stream >> lengthText))
|
||||||
|
throw std::runtime_error ("Missing payload length in frame definition.");
|
||||||
|
frame.dlc = static_cast<std::uint8_t> (ParseDecimal (
|
||||||
|
lengthText, std::numeric_limits<std::uint8_t>::max(), "payload length"));
|
||||||
|
if (!(stream >> frame.transmitter))
|
||||||
|
throw std::runtime_error ("Missing transmitter in frame definition.");
|
||||||
|
if (stream >> extra)
|
||||||
|
throw std::runtime_error ("Unexpected trailing field in frame definition: " + extra);
|
||||||
|
|
||||||
|
NormalizeCanId (rawCanId, frame.canId, frame.isExtended);
|
||||||
|
frame.pgn = TryExtractPgn (frame.canId, frame.isExtended, frame.hasPgn);
|
||||||
|
|
||||||
|
return frame;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalInfo DbcSignalHandler::ParseSignalLine (const std::string &line) {
|
||||||
|
/*
|
||||||
|
* Example:
|
||||||
|
* SG_ EngineSpeed : 0|16@1+ (0.125,0) [0|8000] "rpm" ECU1,ECU2
|
||||||
|
*/
|
||||||
|
|
||||||
|
SignalInfo signal;
|
||||||
|
|
||||||
|
std::string work = TrimText (line);
|
||||||
|
if (work.compare (0U, 4U, "SG_ ") != 0)
|
||||||
|
throw std::runtime_error ("Invalid signal line: " + line);
|
||||||
|
|
||||||
|
work.erase (0U, 4U);
|
||||||
|
|
||||||
|
const std::string::size_type colonPos = work.find (':');
|
||||||
|
if (colonPos == std::string::npos)
|
||||||
|
throw std::runtime_error ("Signal line missing ':' : " + line);
|
||||||
|
|
||||||
|
signal.name = TrimText (work.substr (0U, colonPos));
|
||||||
|
std::string rest = TrimText (work.substr (colonPos + 1U));
|
||||||
|
|
||||||
|
const std::string::size_type pipePos = rest.find ('|');
|
||||||
|
const std::string::size_type atPos = rest.find ('@');
|
||||||
|
const std::string::size_type signPos = rest.find_first_of ("+-", atPos);
|
||||||
|
const std::string::size_type factorBegin = rest.find ('(');
|
||||||
|
const std::string::size_type factorComma = rest.find (',', factorBegin);
|
||||||
|
const std::string::size_type factorEnd = rest.find (')', factorComma);
|
||||||
|
const std::string::size_type rangeBegin = rest.find ('[');
|
||||||
|
const std::string::size_type rangeSep = rest.find ('|', rangeBegin);
|
||||||
|
const std::string::size_type rangeEnd = rest.find (']', rangeSep);
|
||||||
|
const std::string::size_type unitBegin = rest.find ('"', rangeEnd);
|
||||||
|
const std::string::size_type unitEnd = rest.find ('"', unitBegin + 1U);
|
||||||
|
|
||||||
|
if ((pipePos == std::string::npos) ||
|
||||||
|
(atPos == std::string::npos) ||
|
||||||
|
(signPos == std::string::npos) ||
|
||||||
|
(factorBegin == std::string::npos) ||
|
||||||
|
(factorComma == std::string::npos) ||
|
||||||
|
(factorEnd == std::string::npos) ||
|
||||||
|
(rangeBegin == std::string::npos) ||
|
||||||
|
(rangeSep == std::string::npos) ||
|
||||||
|
(rangeEnd == std::string::npos) ||
|
||||||
|
(unitBegin == std::string::npos) ||
|
||||||
|
(unitEnd == std::string::npos))
|
||||||
|
throw std::runtime_error ("Unsupported signal syntax: " + line);
|
||||||
|
|
||||||
|
signal.startBit = static_cast<std::uint32_t> (
|
||||||
|
std::stoul (TrimText (rest.substr (0U, pipePos)))
|
||||||
|
);
|
||||||
|
|
||||||
|
signal.length = static_cast<std::uint32_t> (
|
||||||
|
std::stoul (TrimText (rest.substr (pipePos + 1U, atPos - pipePos - 1U)))
|
||||||
|
);
|
||||||
|
|
||||||
|
{
|
||||||
|
if ((atPos + 1U) >= rest.size())
|
||||||
|
throw std::runtime_error ("Invalid endianness in signal: " + line);
|
||||||
|
|
||||||
|
const char endianChar = rest[atPos + 1U];
|
||||||
|
signal.isLittleEndian = (endianChar == '1');
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
const char signChar = rest[signPos];
|
||||||
|
signal.isSigned = (signChar == '-');
|
||||||
|
}
|
||||||
|
|
||||||
|
signal.factor = std::stod (
|
||||||
|
TrimText (rest.substr (factorBegin + 1U, factorComma - factorBegin - 1U))
|
||||||
|
);
|
||||||
|
|
||||||
|
signal.offset = std::stod (
|
||||||
|
TrimText (rest.substr (factorComma + 1U, factorEnd - factorComma - 1U))
|
||||||
|
);
|
||||||
|
|
||||||
|
signal.minimum = std::stod (
|
||||||
|
TrimText (rest.substr (rangeBegin + 1U, rangeSep - rangeBegin - 1U))
|
||||||
|
);
|
||||||
|
|
||||||
|
signal.maximum = std::stod (
|
||||||
|
TrimText (rest.substr (rangeSep + 1U, rangeEnd - rangeSep - 1U))
|
||||||
|
);
|
||||||
|
|
||||||
|
signal.unit = rest.substr (unitBegin + 1U, unitEnd - unitBegin - 1U);
|
||||||
|
|
||||||
|
{
|
||||||
|
const std::string receiversText = TrimText (rest.substr (unitEnd + 1U));
|
||||||
|
signal.receivers = SplitReceivers (receiversText);
|
||||||
|
}
|
||||||
|
|
||||||
|
return signal;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbcCommentHandler::ParseCommentLine (const std::string &line, DbcDatabase &database) {
|
||||||
|
/*
|
||||||
|
* Examples:
|
||||||
|
* CM_ BO_ 256 "Frame comment";
|
||||||
|
* CM_ SG_ 256 EngineSpeed "Signal comment";
|
||||||
|
*/
|
||||||
|
|
||||||
|
std::istringstream stream (line);
|
||||||
|
std::string token;
|
||||||
|
stream >> token;
|
||||||
|
|
||||||
|
if (token != "CM_")
|
||||||
|
return;
|
||||||
|
|
||||||
|
stream >> token;
|
||||||
|
|
||||||
|
if (token == "BO_") {
|
||||||
|
std::uint32_t rawCanId = 0U;
|
||||||
|
std::uint32_t canId = 0U;
|
||||||
|
bool isExtended = false;
|
||||||
|
|
||||||
|
stream >> rawCanId;
|
||||||
|
NormalizeCanId (rawCanId, canId, isExtended);
|
||||||
|
|
||||||
|
const std::string::size_type quoteBegin = line.find ('"');
|
||||||
|
const std::string::size_type quoteEnd = line.rfind ('"');
|
||||||
|
|
||||||
|
if ((quoteBegin == std::string::npos) ||
|
||||||
|
(quoteEnd == std::string::npos) ||
|
||||||
|
(quoteEnd <= quoteBegin))
|
||||||
|
return;
|
||||||
|
|
||||||
|
FrameInfo *frame = FindFrameById (database, canId, isExtended);
|
||||||
|
if (frame != nullptr)
|
||||||
|
frame->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U);
|
||||||
|
} else if (token == "SG_") {
|
||||||
|
std::uint32_t rawCanId = 0U;
|
||||||
|
std::uint32_t canId = 0U;
|
||||||
|
bool isExtended = false;
|
||||||
|
std::string signalName;
|
||||||
|
|
||||||
|
stream >> rawCanId;
|
||||||
|
stream >> signalName;
|
||||||
|
|
||||||
|
NormalizeCanId (rawCanId, canId, isExtended);
|
||||||
|
|
||||||
|
const std::string::size_type quoteBegin = line.find ('"');
|
||||||
|
const std::string::size_type quoteEnd = line.rfind ('"');
|
||||||
|
|
||||||
|
if ((quoteBegin == std::string::npos) ||
|
||||||
|
(quoteEnd == std::string::npos) ||
|
||||||
|
(quoteEnd <= quoteBegin))
|
||||||
|
return;
|
||||||
|
|
||||||
|
FrameInfo *frame = FindFrameById (database, canId, isExtended);
|
||||||
|
if (frame != nullptr) {
|
||||||
|
SignalInfo *signal = FindSignalByName (*frame, signalName);
|
||||||
|
if (signal != nullptr)
|
||||||
|
signal->comment = line.substr (quoteBegin + 1U, quoteEnd - quoteBegin - 1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
FrameInfo *FindFrameById (DbcDatabase &database,
|
||||||
|
std::uint32_t canId,
|
||||||
|
bool isExtended) {
|
||||||
|
for (std::size_t index = 0U; index < database.frames.size(); ++index) {
|
||||||
|
if ((database.frames[index].canId == canId) &&
|
||||||
|
(database.frames[index].isExtended == isExtended))
|
||||||
|
return &database.frames[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
SignalInfo *FindSignalByName (FrameInfo &frame, const std::string &signalName) {
|
||||||
|
for (std::size_t index = 0U; index < frame.signals.size(); ++index) {
|
||||||
|
if (frame.signals[index].name == signalName)
|
||||||
|
return &frame.signals[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbcFrameHandler::Handle (const std::string &line, DbcParseContext &context) const {
|
||||||
|
context.database.frames.push_back (ParseFrameLine (line));
|
||||||
|
context.currentFrame = &context.database.frames.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbcSignalHandler::Handle (const std::string &line, DbcParseContext &context) const {
|
||||||
|
// Preserve the existing signal syntax policy during this refactor.
|
||||||
|
if (line.compare (0U, 4U, "SG_ ") != 0)
|
||||||
|
return;
|
||||||
|
if (context.currentFrame == nullptr)
|
||||||
|
throw std::runtime_error ("Signal found before any frame definition.");
|
||||||
|
context.currentFrame->signals.push_back (ParseSignalLine (line));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DbcCommentHandler::Handle (const std::string &line, DbcParseContext &context) const {
|
||||||
|
if (line.compare (0U, 4U, "CM_ ") == 0)
|
||||||
|
ParseCommentLine (line, context.database);
|
||||||
|
}
|
||||||
|
|
||||||
|
DbcStatementDispatcher::DbcStatementDispatcher() {
|
||||||
|
RegisterType<DbcFrameHandler> ("BO_");
|
||||||
|
RegisterType<DbcSignalHandler> ("SG_");
|
||||||
|
RegisterType<DbcCommentHandler> ("CM_");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DbcStatementDispatcher::Dispatch (const std::string &line,
|
||||||
|
DbcParseContext &context) const {
|
||||||
|
std::istringstream stream (line);
|
||||||
|
std::string keyword;
|
||||||
|
stream >> keyword;
|
||||||
|
const auto handler = m_handlers.find (keyword);
|
||||||
|
if (handler == m_handlers.end())
|
||||||
|
return false;
|
||||||
|
handler->second->Handle (line, context);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
#ifndef DBC_STATEMENT_HANDLER_H
|
||||||
|
#define DBC_STATEMENT_HANDLER_H
|
||||||
|
|
||||||
|
#include "dbc_database.h"
|
||||||
|
#include "typefactory.h"
|
||||||
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
|
|
||||||
|
/** Mutable state belonging to one file parse. Handlers do not retain it. */
|
||||||
|
struct DbcParseContext {
|
||||||
|
DbcDatabase &database;
|
||||||
|
FrameInfo *currentFrame = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DbcStatementHandler {
|
||||||
|
public:
|
||||||
|
virtual ~DbcStatementHandler() = default;
|
||||||
|
virtual void Handle (const std::string &line, DbcParseContext &context) const = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DbcFrameHandler : public DbcStatementHandler {
|
||||||
|
public:
|
||||||
|
void Handle (const std::string &line, DbcParseContext &context) const override;
|
||||||
|
private:
|
||||||
|
static FrameInfo ParseFrameLine (const std::string &line);
|
||||||
|
};
|
||||||
|
|
||||||
|
class DbcSignalHandler : public DbcStatementHandler {
|
||||||
|
public:
|
||||||
|
void Handle (const std::string &line, DbcParseContext &context) const override;
|
||||||
|
private:
|
||||||
|
static SignalInfo ParseSignalLine (const std::string &line);
|
||||||
|
};
|
||||||
|
|
||||||
|
class DbcCommentHandler : public DbcStatementHandler {
|
||||||
|
public:
|
||||||
|
void Handle (const std::string &line, DbcParseContext &context) const override;
|
||||||
|
private:
|
||||||
|
static void ParseCommentLine (const std::string &line, DbcDatabase &database);
|
||||||
|
};
|
||||||
|
|
||||||
|
/** Exact-key dispatch to handlers created once per registration via TypeFactory. */
|
||||||
|
class DbcStatementDispatcher {
|
||||||
|
public:
|
||||||
|
DbcStatementDispatcher();
|
||||||
|
|
||||||
|
template <class Handler>
|
||||||
|
void RegisterType (const std::string &keyword) {
|
||||||
|
m_factory.registerType<Handler> (keyword);
|
||||||
|
m_handlers.insert_or_assign (keyword, m_factory.create (keyword));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Returns false for an unregistered keyword; input must be trimmed. */
|
||||||
|
bool Dispatch (const std::string &line, DbcParseContext &context) const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
TypeFactory<std::string, DbcStatementHandler> m_factory;
|
||||||
|
std::unordered_map<std::string, std::shared_ptr<DbcStatementHandler> > m_handlers;
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif /* DBC_STATEMENT_HANDLER_H */
|
||||||
@@ -87,8 +87,9 @@ static void PrintDecodedFrame (const DecodedFrameValue &decoded) {
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
std::cout << " " << signal.definition->name
|
std::cout << " " << signal.definition->name
|
||||||
<< " raw=" << signal.rawValue
|
<< " raw=";
|
||||||
<< " physical=" << signal.physicalValue
|
std::visit ([] (auto rawValue) { std::cout << rawValue; }, signal.rawValue);
|
||||||
|
std::cout << " physical=" << signal.physicalValue
|
||||||
<< " " << signal.definition->unit
|
<< " " << signal.definition->unit
|
||||||
<< "\n";
|
<< "\n";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
# Decoder regression tests
|
||||||
|
|
||||||
|
Run from the repository root with a C++17 compiler:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
g++ -std=c++17 -Wall -Wextra -Wpedantic -I. \
|
||||||
|
tests/dbc_decoder_test.cpp dbc_decoder.cpp -o /tmp/dbc_decoder_test
|
||||||
|
/tmp/dbc_decoder_test
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, build the standalone qmake project out of source:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
mkdir -p /tmp/dbc_decoder_test_build
|
||||||
|
cd /tmp/dbc_decoder_test_build
|
||||||
|
qmake /absolute/path/to/dbc/tests/dbc_decoder_test.pro
|
||||||
|
make
|
||||||
|
./dbc_decoder_test
|
||||||
|
```
|
||||||
|
|
||||||
|
The executable returns a nonzero exit code if any case fails. Checks remain
|
||||||
|
active in release builds and do not depend on `assert` or an external test framework.
|
||||||
|
|
||||||
|
Coverage includes Motorola bit numbering within and across bytes, unaligned
|
||||||
|
signals, signed values, scaling, 64-bit extraction, signed/unsigned boundary values
|
||||||
|
(including `INT64_MIN`, `INT64_MAX`, and `UINT64_MAX`), numeric output, invalid lengths, insufficient
|
||||||
|
payloads, and Intel regression cases. Frame validity means the definition was
|
||||||
|
found; insufficient payloads are checked through signal validity.
|
||||||
|
|
||||||
|
## Parser regression tests
|
||||||
|
|
||||||
|
Run from the repository root:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
g++ -std=c++17 -Wall -Wextra -Wpedantic -Werror -I. \
|
||||||
|
tests/dbc_parser_test.cpp dbc_parser.cpp dbc_statement_handler.cpp -o /tmp/dbc_parser_test
|
||||||
|
/tmp/dbc_parser_test
|
||||||
|
```
|
||||||
|
|
||||||
|
For qmake, use `tests/dbc_parser_test.pro` in a separate build directory,
|
||||||
|
following the decoder instructions above, then run `./dbc_parser_test`.
|
||||||
|
|
||||||
|
Parser tests cover standard and extended CAN IDs, whitespace, payload storage
|
||||||
|
boundaries, missing fields, malformed and overflowing numbers, reserved ID bits,
|
||||||
|
error paths and line numbers, and integration with signals and comments.
|
||||||
|
Temporary DBC fixtures are created in the system temporary directory and removed
|
||||||
|
automatically.
|
||||||
|
|
||||||
|
Dispatch tests also verify exact keyword matching, handler construction and reuse,
|
||||||
|
unknown statements, and independent parsing sessions, including recovery after errors.
|
||||||
@@ -0,0 +1,154 @@
|
|||||||
|
#include "dbc_decoder.h"
|
||||||
|
|
||||||
|
#include <iostream>
|
||||||
|
#include <limits>
|
||||||
|
#include <sstream>
|
||||||
|
|
||||||
|
struct DecodeCase {
|
||||||
|
const char *name;
|
||||||
|
ByteOrder byteOrder;
|
||||||
|
std::uint32_t startBit;
|
||||||
|
std::uint32_t length;
|
||||||
|
ValueType valueType;
|
||||||
|
std::vector<std::uint8_t> data;
|
||||||
|
bool valid;
|
||||||
|
std::int64_t rawValue;
|
||||||
|
double factor;
|
||||||
|
double offset;
|
||||||
|
double physicalValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
const DecodeCase cases[] = {
|
||||||
|
{"Motorola reproduction", ByteOrder::Motorola, 7U, 16U, ValueType::Unsigned,
|
||||||
|
{0x12, 0x34}, true, 0x1234, 1.0, 0.0, 4660.0},
|
||||||
|
{"Motorola within byte", ByteOrder::Motorola, 5U, 4U, ValueType::Unsigned,
|
||||||
|
{0xAC}, true, 11, 1.0, 0.0, 11.0},
|
||||||
|
{"Motorola unaligned crossing", ByteOrder::Motorola, 3U, 12U, ValueType::Unsigned,
|
||||||
|
{0xAB, 0xCD}, true, 0xBCD, 1.0, 0.0, 3021.0},
|
||||||
|
{"Motorola starts at bit zero", ByteOrder::Motorola, 0U, 2U, ValueType::Unsigned,
|
||||||
|
{0x01, 0x80}, true, 3, 1.0, 0.0, 3.0},
|
||||||
|
{"Motorola second byte", ByteOrder::Motorola, 15U, 16U, ValueType::Unsigned,
|
||||||
|
{0xFF, 0x12, 0x34}, true, 0x1234, 1.0, 0.0, 4660.0},
|
||||||
|
{"Motorola signed scaled", ByteOrder::Motorola, 7U, 16U, ValueType::Signed,
|
||||||
|
{0xFF, 0xFE}, true, -2, 0.5, 10.0, 9.0},
|
||||||
|
{"Motorola 64 bits", ByteOrder::Motorola, 7U, 64U, ValueType::Unsigned,
|
||||||
|
{0, 0, 0, 0, 0, 0, 0x12, 0x34}, true, 0x1234, 1.0, 0.0, 4660.0},
|
||||||
|
{"Motorola 64 bits high bit", ByteOrder::Motorola, 7U, 64U, ValueType::Unsigned,
|
||||||
|
{0x40, 0, 0, 0, 0, 0, 0, 0}, true, 0x4000000000000000LL,
|
||||||
|
1.0, 0.0, 4611686018427387904.0},
|
||||||
|
{"Motorola short payload", ByteOrder::Motorola, 7U, 16U, ValueType::Unsigned,
|
||||||
|
{0x12}, false, 0, 1.0, 0.0, 0.0},
|
||||||
|
{"Motorola empty payload", ByteOrder::Motorola, 7U, 8U, ValueType::Unsigned,
|
||||||
|
{}, false, 0, 1.0, 0.0, 0.0},
|
||||||
|
{"Motorola large start", ByteOrder::Motorola,
|
||||||
|
std::numeric_limits<std::uint32_t>::max(), 8U, ValueType::Unsigned,
|
||||||
|
{0xFF}, false, 0, 1.0, 0.0, 0.0},
|
||||||
|
{"Motorola zero length", ByteOrder::Motorola, 7U, 0U, ValueType::Unsigned,
|
||||||
|
{0xFF}, false, 0, 1.0, 0.0, 0.0},
|
||||||
|
{"Motorola excessive length", ByteOrder::Motorola, 7U, 65U, ValueType::Unsigned,
|
||||||
|
std::vector<std::uint8_t> (9U, 0xFF), false, 0, 1.0, 0.0, 0.0},
|
||||||
|
{"Intel regression", ByteOrder::Intel, 0U, 16U, ValueType::Unsigned,
|
||||||
|
{0x12, 0x34}, true, 0x3412, 1.0, 0.0, 13330.0},
|
||||||
|
{"Intel signed regression", ByteOrder::Intel, 0U, 16U, ValueType::Signed,
|
||||||
|
{0xFE, 0xFF}, true, -2, 0.5, 10.0, 9.0},
|
||||||
|
{"Intel short payload", ByteOrder::Intel, 0U, 16U, ValueType::Unsigned,
|
||||||
|
{0x12}, false, 0, 1.0, 0.0, 0.0}
|
||||||
|
};
|
||||||
|
|
||||||
|
int failures = 0;
|
||||||
|
for (const DecodeCase &test : cases) {
|
||||||
|
DecodeSignal signal;
|
||||||
|
signal.name = test.name;
|
||||||
|
signal.byteOrder = test.byteOrder;
|
||||||
|
signal.startBit = test.startBit;
|
||||||
|
signal.length = test.length;
|
||||||
|
signal.valueType = test.valueType;
|
||||||
|
signal.factor = test.factor;
|
||||||
|
signal.offset = test.offset;
|
||||||
|
|
||||||
|
DecodeFrame definition;
|
||||||
|
definition.canId = 0x123U;
|
||||||
|
definition.dlc = static_cast<std::uint8_t> (test.data.size());
|
||||||
|
definition.signals.push_back (signal);
|
||||||
|
DecodeDatabase database;
|
||||||
|
database.frames.push_back (definition);
|
||||||
|
database.frameIndexByKey.emplace (FrameKey (definition.canId, false), 0U);
|
||||||
|
|
||||||
|
RawCanFrame frame;
|
||||||
|
frame.canId = definition.canId;
|
||||||
|
frame.data = test.data;
|
||||||
|
const DecodedFrameValue result = DbcDecoder().Decode (database, frame);
|
||||||
|
const bool passed = result.valid &&
|
||||||
|
(result.definition == &database.frames[0]) &&
|
||||||
|
(result.signals.size() == 1U) &&
|
||||||
|
(result.signals[0].definition == &database.frames[0].signals[0]) &&
|
||||||
|
(result.signals[0].valid == test.valid) &&
|
||||||
|
(!test.valid || (((test.valueType == ValueType::Signed)
|
||||||
|
? (std::get_if<std::int64_t> (&result.signals[0].rawValue) &&
|
||||||
|
std::get<std::int64_t> (result.signals[0].rawValue) == test.rawValue)
|
||||||
|
: (std::get_if<std::uint64_t> (&result.signals[0].rawValue) &&
|
||||||
|
std::get<std::uint64_t> (result.signals[0].rawValue) ==
|
||||||
|
static_cast<std::uint64_t> (test.rawValue))) &&
|
||||||
|
(result.signals[0].physicalValue == test.physicalValue)));
|
||||||
|
if (!passed) {
|
||||||
|
std::cerr << "FAIL: " << test.name << '\n';
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::size_t boundaryCases = 0U;
|
||||||
|
for (ByteOrder order : {ByteOrder::Intel, ByteOrder::Motorola}) {
|
||||||
|
for (ValueType type : {ValueType::Unsigned, ValueType::Signed}) {
|
||||||
|
for (std::uint64_t bits : {std::uint64_t (0U), std::uint64_t (0x7FFFFFFFFFFFFFFFULL),
|
||||||
|
std::uint64_t (0x8000000000000000ULL),
|
||||||
|
std::uint64_t (0x8000000000000001ULL),
|
||||||
|
std::uint64_t (0xFFFFFFFFFFFFFFFFULL)}) {
|
||||||
|
DecodeDatabase database;
|
||||||
|
DecodeFrame definition;
|
||||||
|
DecodeSignal signal;
|
||||||
|
signal.startBit = order == ByteOrder::Intel ? 0U : 7U;
|
||||||
|
signal.length = 64U;
|
||||||
|
signal.byteOrder = order;
|
||||||
|
signal.valueType = type;
|
||||||
|
signal.factor = 0.5;
|
||||||
|
signal.offset = 10.0;
|
||||||
|
definition.signals.push_back (signal);
|
||||||
|
database.frames.push_back (definition);
|
||||||
|
database.frameIndexByKey.emplace (FrameKey (0U, false), 0U);
|
||||||
|
RawCanFrame frame;
|
||||||
|
for (unsigned byte = 0U; byte < 8U; ++byte) {
|
||||||
|
const unsigned shift = (order == ByteOrder::Intel ? byte : 7U - byte) * 8U;
|
||||||
|
frame.data.push_back (static_cast<std::uint8_t> (bits >> shift));
|
||||||
|
}
|
||||||
|
const auto result = DbcDecoder().Decode (database, frame);
|
||||||
|
const std::int64_t signedExpected = bits <= 0x7FFFFFFFFFFFFFFFULL
|
||||||
|
? static_cast<std::int64_t> (bits)
|
||||||
|
: std::numeric_limits<std::int64_t>::min() +
|
||||||
|
static_cast<std::int64_t> (bits - 0x8000000000000000ULL);
|
||||||
|
const auto &decoded = result.signals.at (0U);
|
||||||
|
const auto expected = type == ValueType::Unsigned
|
||||||
|
? std::variant<std::int64_t, std::uint64_t> (bits)
|
||||||
|
: std::variant<std::int64_t, std::uint64_t> (signedExpected);
|
||||||
|
const double physical = (type == ValueType::Unsigned
|
||||||
|
? static_cast<double> (bits) : static_cast<double> (signedExpected)) * 0.5 + 10.0;
|
||||||
|
std::ostringstream actualText;
|
||||||
|
std::ostringstream expectedText;
|
||||||
|
std::visit ([&actualText] (auto raw) { actualText << raw; }, decoded.rawValue);
|
||||||
|
if (type == ValueType::Unsigned)
|
||||||
|
expectedText << bits;
|
||||||
|
else
|
||||||
|
expectedText << signedExpected;
|
||||||
|
if (!decoded.valid || decoded.rawValue != expected ||
|
||||||
|
decoded.physicalValue != physical || actualText.str() != expectedText.str()) {
|
||||||
|
std::cerr << "FAIL: 64-bit boundary " << bits << '\n';
|
||||||
|
++failures;
|
||||||
|
}
|
||||||
|
++boundaryCases;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (failures != 0)
|
||||||
|
return 1;
|
||||||
|
std::cout << "All " << sizeof (cases) / sizeof (cases[0]) + boundaryCases << " decoder cases passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
TEMPLATE = app
|
||||||
|
TARGET = dbc_decoder_test
|
||||||
|
CONFIG += console c++17
|
||||||
|
CONFIG -= app_bundle qt
|
||||||
|
INCLUDEPATH += ..
|
||||||
|
SOURCES += dbc_decoder_test.cpp ../dbc_decoder.cpp
|
||||||
|
HEADERS += ../dbc_decoder.h ../decode_database.h
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#include "dbc_parser.h"
|
||||||
|
#include "dbc_statement_handler.h"
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iostream>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
struct Fixture {
|
||||||
|
std::filesystem::path path = std::filesystem::temp_directory_path() /
|
||||||
|
("dbc_parser_test_" + std::to_string (
|
||||||
|
std::chrono::steady_clock::now().time_since_epoch().count()) + ".dbc");
|
||||||
|
~Fixture() { std::error_code error; std::filesystem::remove (path, error); }
|
||||||
|
void Write (const std::string &text) const {
|
||||||
|
std::ofstream output (path);
|
||||||
|
output << text;
|
||||||
|
if (!output)
|
||||||
|
throw std::runtime_error ("Cannot write test fixture.");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class CountingHandler : public DbcStatementHandler {
|
||||||
|
public:
|
||||||
|
inline static int instances = 0;
|
||||||
|
inline static int calls = 0;
|
||||||
|
CountingHandler() { ++instances; }
|
||||||
|
void Handle (const std::string &, DbcParseContext &context) const override {
|
||||||
|
++calls;
|
||||||
|
context.database.frames.emplace_back();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
Fixture fixture;
|
||||||
|
int failures = 0;
|
||||||
|
std::size_t count = 0U;
|
||||||
|
const auto check = [&] (bool passed, const std::string &name) {
|
||||||
|
++count;
|
||||||
|
if (!passed) { ++failures; std::cerr << "FAIL: " << name << '\n'; }
|
||||||
|
};
|
||||||
|
struct ValidCase { const char *line; std::uint32_t id; bool extended; unsigned length; };
|
||||||
|
const ValidCase valid[] = {
|
||||||
|
{"BO_ 0 Frame: 0 ECU", 0U, false, 0U},
|
||||||
|
{"BO_ 2047 Frame: 8 ECU", 2047U, false, 8U},
|
||||||
|
{"BO_ 2048 Frame: 64 ECU", 2048U, true, 64U},
|
||||||
|
{"BO_ 536870911 Frame: 255 ECU", 0x1FFFFFFFU, true, 255U},
|
||||||
|
{"BO_ 2147483649 Frame: 8 ECU", 1U, true, 8U},
|
||||||
|
{"BO_ 2684354559 Frame: 8 ECU", 0x1FFFFFFFU, true, 8U},
|
||||||
|
{" BO_\t256\tFrame :\t8\tECU \r", 256U, false, 8U}
|
||||||
|
};
|
||||||
|
for (const auto &test : valid) {
|
||||||
|
fixture.Write (std::string (test.line) + '\n');
|
||||||
|
try {
|
||||||
|
const auto database = DbcParser().ParseFile (fixture.path.string());
|
||||||
|
check (database.frames.size() == 1U && database.frames[0].name == "Frame" &&
|
||||||
|
database.frames[0].canId == test.id &&
|
||||||
|
database.frames[0].isExtended == test.extended &&
|
||||||
|
database.frames[0].dlc == test.length &&
|
||||||
|
database.frames[0].transmitter == "ECU", test.line);
|
||||||
|
} catch (const std::exception &error) { check (false, error.what()); }
|
||||||
|
}
|
||||||
|
const char *invalid[] = {
|
||||||
|
"BO_ invalid", "BO_", "BO_ 1", "BO_ 1 : 8 ECU", "BO_ 1 Frame 8 ECU",
|
||||||
|
"BO_ 1 Frame:", "BO_ 1 Frame: 8", "BO_ 1 Frame: 8 ECU extra",
|
||||||
|
"BO_ 1 Two Names: 8 ECU", "BO_ -1 Frame: 8 ECU", "BO_ +1 Frame: 8 ECU",
|
||||||
|
"BO_ 1x Frame: 8 ECU", "BO_ 0x123 Frame: 8 ECU", "BO_ 1.5 Frame: 8 ECU",
|
||||||
|
"BO_ 4294967296 Frame: 8 ECU", "BO_ 999999999999999999999 Frame: 8 ECU",
|
||||||
|
"BO_ 536870912 Frame: 8 ECU", "BO_ 1073741824 Frame: 8 ECU",
|
||||||
|
"BO_ 2684354560 Frame: 8 ECU", "BO_ 4294967295 Frame: 8 ECU",
|
||||||
|
"BO_ 1 Frame: -1 ECU", "BO_ 1 Frame: +8 ECU", "BO_ 1 Frame: 8x ECU",
|
||||||
|
"BO_ 1 Frame: 8.5 ECU", "BO_ 1 Frame: 256 ECU",
|
||||||
|
"BO_ 1 Frame: 4294967296 ECU", "BO_\tinvalid"
|
||||||
|
};
|
||||||
|
for (const auto *line : invalid) {
|
||||||
|
fixture.Write (std::string ("VERSION \"test\"\n\n") + line + '\n');
|
||||||
|
try {
|
||||||
|
DbcParser().ParseFile (fixture.path.string());
|
||||||
|
check (false, line);
|
||||||
|
} catch (const std::runtime_error &error) {
|
||||||
|
const std::string prefix = fixture.path.string() + ":3: ";
|
||||||
|
check (std::string (error.what()).find (prefix) == 0U &&
|
||||||
|
std::string (error.what()).size() > prefix.size(), line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fixture.Write (
|
||||||
|
"BO_TX_BU_ 256 : ECU;\nBO_ 256 Frame: 8 ECU\n"
|
||||||
|
" SG_ Speed : 0|16@1+ (0.5,1) [0|100] \"rpm\" ECU\n"
|
||||||
|
"CM_ BO_ 256 \"Frame comment\";\nCM_ SG_ 256 Speed \"Signal comment\";\n"
|
||||||
|
"BO_ 257 Second: 8 ECU\n");
|
||||||
|
const auto database = DbcParser().ParseFile (fixture.path.string());
|
||||||
|
check (database.frames.size() == 2U && database.frames[0].signals.size() == 1U &&
|
||||||
|
database.frames[0].comment == "Frame comment" &&
|
||||||
|
database.frames[0].signals[0].comment == "Signal comment", "Integration");
|
||||||
|
fixture.Write ("\nSG_ Speed : invalid\n");
|
||||||
|
try {
|
||||||
|
DbcParser().ParseFile (fixture.path.string());
|
||||||
|
check (false, "Signal error location");
|
||||||
|
} catch (const std::runtime_error &error) {
|
||||||
|
check (std::string (error.what()).find (fixture.path.string() + ":2: ") == 0U,
|
||||||
|
"Signal error location");
|
||||||
|
}
|
||||||
|
DbcDatabase dispatchDatabase;
|
||||||
|
DbcParseContext context {dispatchDatabase, nullptr};
|
||||||
|
DbcStatementDispatcher dispatcher;
|
||||||
|
dispatcher.RegisterType<CountingHandler> ("TEST_");
|
||||||
|
check (dispatcher.Dispatch ("TEST_ first", context) &&
|
||||||
|
dispatcher.Dispatch ("TEST_ second", context) &&
|
||||||
|
CountingHandler::instances == 1 && CountingHandler::calls == 2 &&
|
||||||
|
dispatchDatabase.frames.size() == 2U, "Factory dispatch and handler reuse");
|
||||||
|
check (!dispatcher.Dispatch ("TEST_OTHER data", context) &&
|
||||||
|
!dispatcher.Dispatch ("BO_TX_BU_ 1 : ECU;", context) &&
|
||||||
|
!dispatcher.Dispatch ("VAL_ 1 Speed 0 \"Stopped\";", context) &&
|
||||||
|
CountingHandler::calls == 2, "Exact and unknown keyword dispatch");
|
||||||
|
const DbcParser parser;
|
||||||
|
fixture.Write ("BO_ 1 First: 8 ECU\n");
|
||||||
|
check (parser.ParseFile (fixture.path.string()).frames.size() == 1U, "First parse");
|
||||||
|
fixture.Write ("SG_ Speed : invalid\n");
|
||||||
|
try {
|
||||||
|
parser.ParseFile (fixture.path.string());
|
||||||
|
check (false, "Independent parse context");
|
||||||
|
} catch (const std::runtime_error &error) {
|
||||||
|
check (std::string (error.what()).find ("Signal found before any frame") !=
|
||||||
|
std::string::npos, "Independent parse context");
|
||||||
|
}
|
||||||
|
fixture.Write ("BO_ 2 Second: 8 ECU\n");
|
||||||
|
const auto recovered = parser.ParseFile (fixture.path.string());
|
||||||
|
check (recovered.frames.size() == 1U && recovered.frames[0].canId == 2U,
|
||||||
|
"Fresh parse after error");
|
||||||
|
if (failures != 0) return 1;
|
||||||
|
std::cout << "All " << count << " parser cases passed.\n";
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
TEMPLATE = app
|
||||||
|
TARGET = dbc_parser_test
|
||||||
|
CONFIG += console c++17
|
||||||
|
CONFIG -= app_bundle qt
|
||||||
|
INCLUDEPATH += ..
|
||||||
|
SOURCES += dbc_parser_test.cpp ../dbc_parser.cpp ../dbc_statement_handler.cpp
|
||||||
|
HEADERS += ../dbc_statement_handler.h ../typefactory.h ../dbc_parser.h ../dbc_database.h ../frame_info.h ../signal_info.h
|
||||||
+118
@@ -0,0 +1,118 @@
|
|||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
// Copyright (c) 2026 Dim Himro
|
||||||
|
|
||||||
|
#ifndef TYPEFACTORY_H
|
||||||
|
#define TYPEFACTORY_H
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <type_traits>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility> // std::move
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Registry-based factory for creating objects by runtime identifier.
|
||||||
|
*
|
||||||
|
* Allows registering derived types and instantiating them using a ClassId key.
|
||||||
|
*
|
||||||
|
* Notes:
|
||||||
|
* - Not thread-safe. Register types during initialization phase only.
|
||||||
|
* - Re-registering the same id overwrites the previous entry ("last wins").
|
||||||
|
* - ClassId must be hashable (std::hash<ClassId> specialization must exist).
|
||||||
|
*
|
||||||
|
* @tparam ClassId -Unique class identifier type (e.g. std::string, int, enum).
|
||||||
|
* @tparam BaseClass -Base type for all created objects.
|
||||||
|
* @tparam Arg - Constructor arguments forwarded to registered derived types (by value).
|
||||||
|
*
|
||||||
|
* Usage example:
|
||||||
|
* @code
|
||||||
|
* #include <string>
|
||||||
|
*
|
||||||
|
* struct Base {
|
||||||
|
* virtual ~Base() = default;
|
||||||
|
* virtual int get() = 0;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* struct Derived1 : Base {
|
||||||
|
* explicit Derived1(int start) : m_start(start) {}
|
||||||
|
* int get() override { return m_start + 1; }
|
||||||
|
* int m_start = 0;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* struct Derived2 : Base {
|
||||||
|
* explicit Derived2(int start) : m_start(start) {}
|
||||||
|
* int get() override { return m_start + 2; }
|
||||||
|
* int m_start = 0;
|
||||||
|
* };
|
||||||
|
*
|
||||||
|
* TypeFactory<std::string, Base, int> factory;
|
||||||
|
* factory.registerType<Derived1>("one");
|
||||||
|
* factory.registerType<Derived2>("two");
|
||||||
|
*
|
||||||
|
* auto a = factory.create("one", 10);
|
||||||
|
* auto b = factory.creator("two")(10); // advanced API: get creator function
|
||||||
|
*
|
||||||
|
* (void)a->get();
|
||||||
|
* (void)b->get();
|
||||||
|
* @endcode
|
||||||
|
*/
|
||||||
|
template <class ClassId, class BaseClass, class... Args>
|
||||||
|
class TypeFactory {
|
||||||
|
public:
|
||||||
|
using BasePtr = std::shared_ptr<BaseClass>;
|
||||||
|
using CreatorFn = BasePtr (*)(Args...);
|
||||||
|
|
||||||
|
TypeFactory() = default;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Registers a Derived type under the given id.
|
||||||
|
*
|
||||||
|
* Requirements (checked at compile-time):
|
||||||
|
* - Derived must inherit from BaseClass
|
||||||
|
* - Derived must be constructible from Args...
|
||||||
|
*
|
||||||
|
* Re-registering the same id overwrites the previous entry ("last wins").
|
||||||
|
*/
|
||||||
|
template <class Derived>
|
||||||
|
void registerType(const ClassId& id) {
|
||||||
|
static_assert(std::is_base_of_v<BaseClass, Derived>,
|
||||||
|
"Derived must inherit from BaseClass");
|
||||||
|
static_assert(std::is_constructible_v<Derived, Args...>,
|
||||||
|
"Derived must be constructible from Args...");
|
||||||
|
|
||||||
|
classes.insert_or_assign(id, &instantiate<Derived>);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Returns creator function for the given id.
|
||||||
|
* @throws std::out_of_range when id is not registered.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] CreatorFn creator(const ClassId& id) const {
|
||||||
|
return classes.at(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Creates an instance for the given id using provided arguments.
|
||||||
|
* @throws std::out_of_range when id is not registered.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] BasePtr create(const ClassId& id, Args... args) const {
|
||||||
|
return creator(id)(std::move(args)...);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Checks whether an id is registered.
|
||||||
|
*/
|
||||||
|
[[nodiscard]] bool contains(const ClassId& id) const {
|
||||||
|
return classes.find(id) != classes.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Protected by design: derived factories may extend/inspect the registry.
|
||||||
|
std::unordered_map<ClassId, CreatorFn> classes;
|
||||||
|
|
||||||
|
private:
|
||||||
|
template <class Derived>
|
||||||
|
static BasePtr instantiate(Args... args) {
|
||||||
|
return std::make_shared<Derived>(std::move(args)...);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#endif // TYPEFACTORY_H
|
||||||
Reference in new issue
Block a user