Compare commits

...
6 Commits
26 changed files with 1359 additions and 527 deletions

No files matched your search

+65
View File
@@ -0,0 +1,65 @@
BasedOnStyle: LLVM
AccessModifierOffset: -4
AlignAfterOpenBracket: Align
AlignConsecutiveAssignments: None
AlignOperands: Align
AllowAllArgumentsOnNextLine: false
AllowAllConstructorInitializersOnNextLine: false
AllowAllParametersOfDeclarationOnNextLine: false
AllowShortBlocksOnASingleLine: Always
AllowShortCaseLabelsOnASingleLine: false
AllowShortFunctionsOnASingleLine: All
AllowShortIfStatementsOnASingleLine: Always
AllowShortLambdasOnASingleLine: All
AllowShortLoopsOnASingleLine: true
AlwaysBreakAfterReturnType: None
AlwaysBreakTemplateDeclarations: Yes
BreakBeforeBraces: Custom
BraceWrapping:
AfterCaseLabel: false
AfterClass: false
AfterControlStatement: Never
AfterEnum: false
AfterFunction: false
AfterNamespace: false
AfterUnion: false
BeforeCatch: false
BeforeElse: false
IndentBraces: false
SplitEmptyFunction: false
SplitEmptyRecord: true
BreakBeforeBinaryOperators: None
BreakBeforeTernaryOperators: true
BreakConstructorInitializers: BeforeColon
BreakInheritanceList: BeforeColon
ColumnLimit: 0
CompactNamespaces: false
ContinuationIndentWidth: 8
IndentCaseLabels: true
IndentPPDirectives: None
IndentWidth: 4
KeepEmptyLinesAtTheStartOfBlocks: true
MaxEmptyLinesToKeep: 2
NamespaceIndentation: All
ObjCSpaceAfterProperty: false
ObjCSpaceBeforeProtocolList: true
PointerAlignment: Right
ReflowComments: false
SpaceAfterCStyleCast: true
SpaceAfterLogicalNot: false
SpaceAfterTemplateKeyword: false
SpaceBeforeAssignmentOperators: true
SpaceBeforeCpp11BracedList: false
SpaceBeforeCtorInitializerColon: true
SpaceBeforeInheritanceColon: true
SpaceBeforeParens: ControlStatements
SpaceBeforeRangeBasedForLoopColon: false
SpaceInEmptyParentheses: false
SpacesBeforeTrailingComments: 0
SpacesInAngles: false
SpacesInCStyleCastParentheses: false
SpacesInContainerLiterals: false
SpacesInParentheses: false
SpacesInSquareBrackets: false
TabWidth: 4
UseTab: Never
+132 -9
View File
@@ -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,35 @@ 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 definitions are also validated as described below; comprehensive
DBC grammar coverage is still planned. File-open errors contain the path but have no line number.
---
## Signal definition ## Signal definition
Example: Example:
@@ -501,6 +561,35 @@ Parsed fields:
--- ---
### Signal validation and parse errors
`SG_` statements are matched completely. Signal names must be nonempty and
contain no whitespace. Spaces and tabs are accepted between syntax elements;
byte order and signedness remain adjacent (`@0+`, `@0-`, `@1+`, or `@1-`).
A quoted unit (which may be empty) and a nonempty comma-separated receiver
list are required. Unexpected trailing fields are rejected.
Start bits must be unsigned decimal integers fitting `uint32_t`; lengths must
be within `1..64` bits. Signs, fractional values, numeric suffixes, and overflow
are rejected for these fields. Every signal bit must fit within the frame's
declared payload length, using Intel increasing bit positions or Motorola
sawtooth traversal. This uses the stored payload length without introducing
an additional CAN or CAN FD payload limit.
Factor, offset, minimum, and maximum accept signed decimal numbers, fractions,
and scientific notation. The entire numeric token must be valid; hexadecimal
numbers, NaN, infinity, conversion overflow, and conversion underflow are
rejected. Minimum must not exceed maximum. Zero and negative factors are allowed.
Multiplexing markers (`M`, `mN`, and combinations) are currently rejected rather
than interpreted as part of a signal name. Multiplexing support and duplicate
frame/signal detection remain separate work.
Invalid signal definitions stop parsing with `path:line: description`, including
when the signal exceeds the declared payload length.
---
## Comments ## Comments
Frame comment example: Frame comment example:
@@ -709,20 +798,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. `SG_` accepts spaces and tabs after its keyword; `CM_` retains its existing
policy requiring a literal space.
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 +955,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.
+3
View File
@@ -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 \
+4 -4
View File
@@ -11,7 +11,7 @@
#include "dbc_decode_builder.h" #include "dbc_decode_builder.h"
DecodeDatabase DbcDecodeBuilder::Build (const DbcDatabase &source) const { DecodeDatabase DbcDecodeBuilder::Build(const DbcDatabase &source) const {
DecodeDatabase result; DecodeDatabase result;
for (std::size_t frameIndex = 0U; frameIndex < source.frames.size(); ++frameIndex) { for (std::size_t frameIndex = 0U; frameIndex < source.frames.size(); ++frameIndex) {
@@ -44,11 +44,11 @@ DecodeDatabase DbcDecodeBuilder::Build (const DbcDatabase &source) const {
targetSignal.receivers = sourceSignal.receivers; targetSignal.receivers = sourceSignal.receivers;
targetSignal.comment = sourceSignal.comment; targetSignal.comment = sourceSignal.comment;
targetFrame.signals.push_back (targetSignal); targetFrame.signals.push_back(targetSignal);
} }
result.frames.push_back (targetFrame); result.frames.push_back(targetFrame);
result.frameIndexByKey[FrameKey (targetFrame.canId, targetFrame.isExtended)] = result.frameIndexByKey[FrameKey(targetFrame.canId, targetFrame.isExtended)] =
result.frames.size() - 1U; result.frames.size() - 1U;
} }
+2 -2
View File
@@ -19,13 +19,13 @@
* @brief Converts parsed DBC data into runtime decode database. * @brief Converts parsed DBC data into runtime decode database.
*/ */
class DbcDecodeBuilder { class DbcDecodeBuilder {
public: public:
/** /**
* @brief Build runtime decode database. * @brief Build runtime decode database.
* @param source Parsed DBC database. * @param source Parsed DBC database.
* @return Runtime-ready decode database. * @return Runtime-ready decode database.
*/ */
DecodeDatabase Build (const DbcDatabase &source) const; DecodeDatabase Build(const DbcDatabase &source) const;
}; };
#endif /* DBC_DECODE_BUILDER_H */ #endif /* DBC_DECODE_BUILDER_H */
+39 -39
View File
@@ -11,12 +11,12 @@
#include "dbc_decoder.h" #include "dbc_decoder.h"
const DecodeFrame *DbcDecoder::FindFrame (const DecodeDatabase &database, const DecodeFrame *DbcDecoder::FindFrame(const DecodeDatabase &database,
std::uint32_t canId, std::uint32_t canId,
bool isExtended) const { bool isExtended) const {
const FrameKey key (canId, isExtended); const FrameKey key(canId, isExtended);
const std::unordered_map<FrameKey, std::size_t, FrameKeyHasher>::const_iterator it = const std::unordered_map<FrameKey, std::size_t, FrameKeyHasher>::const_iterator it =
database.frameIndexByKey.find (key); database.frameIndexByKey.find(key);
if (it == database.frameIndexByKey.end()) if (it == database.frameIndexByKey.end())
return nullptr; return nullptr;
@@ -28,10 +28,10 @@ const DecodeFrame *DbcDecoder::FindFrame (const DecodeDatabase &database,
return &database.frames[index]; return &database.frames[index];
} }
DecodedFrameValue DbcDecoder::Decode (const DecodeDatabase &database, DecodedFrameValue DbcDecoder::Decode(const DecodeDatabase &database,
const RawCanFrame &frame) const { const RawCanFrame &frame) const {
DecodedFrameValue result; DecodedFrameValue result;
const DecodeFrame *definition = FindFrame (database, frame.canId, frame.isExtended); const DecodeFrame *definition = FindFrame(database, frame.canId, frame.isExtended);
if (definition == nullptr) if (definition == nullptr)
return result; return result;
@@ -46,40 +46,43 @@ DecodedFrameValue DbcDecoder::Decode (const DecodeDatabase &database,
decoded.definition = &signal; decoded.definition = &signal;
if (!ExtractUnsigned (frame.data, signal, unsignedValue)) { if (!ExtractUnsigned(frame.data, signal, unsignedValue)) {
decoded.valid = false; decoded.valid = false;
result.signals.push_back (decoded); result.signals.push_back(decoded);
continue; continue;
} }
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);
} }
return result; return result;
} }
bool DbcDecoder::ExtractUnsigned (const std::vector<std::uint8_t> &data, bool DbcDecoder::ExtractUnsigned(const std::vector<std::uint8_t> &data,
const DecodeSignal &signal, const DecodeSignal &signal,
std::uint64_t &value) { std::uint64_t &value) {
if ((signal.length == 0U) || (signal.length > 64U)) if ((signal.length == 0U) || (signal.length > 64U))
return false; return false;
if (signal.byteOrder == ByteOrder::Intel) if (signal.byteOrder == ByteOrder::Intel)
return ExtractIntel (data, signal.startBit, signal.length, value); return ExtractIntel(data, signal.startBit, signal.length, value);
return ExtractMotorola (data, signal.startBit, signal.length, value); return ExtractMotorola(data, signal.startBit, signal.length, value);
} }
bool DbcDecoder::ExtractIntel (const std::vector<std::uint8_t> &data, bool DbcDecoder::ExtractIntel(const std::vector<std::uint8_t> &data,
std::uint32_t startBit, std::uint32_t startBit,
std::uint32_t length, std::uint32_t length,
std::uint64_t &value) { std::uint64_t &value) {
@@ -94,7 +97,7 @@ bool DbcDecoder::ExtractIntel (const std::vector<std::uint8_t> &data,
return false; return false;
const std::uint64_t bitValue = const std::uint64_t bitValue =
(static_cast<std::uint64_t> ((data[byteIndex] >> bitInByte) & 0x01U) << bitIndex); (static_cast<std::uint64_t>((data[byteIndex] >> bitInByte) & 0x01U) << bitIndex);
value |= bitValue; value |= bitValue;
} }
@@ -102,7 +105,7 @@ bool DbcDecoder::ExtractIntel (const std::vector<std::uint8_t> &data,
return true; return true;
} }
bool DbcDecoder::ExtractMotorola (const std::vector<std::uint8_t> &data, bool DbcDecoder::ExtractMotorola(const std::vector<std::uint8_t> &data,
std::uint32_t startBit, std::uint32_t startBit,
std::uint32_t length, std::uint32_t length,
std::uint64_t &value) { std::uint64_t &value) {
@@ -111,47 +114,44 @@ 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);
} }
+14 -20
View File
@@ -12,8 +12,9 @@
#ifndef DBC_DECODER_H #ifndef DBC_DECODER_H
#define DBC_DECODER_H #define DBC_DECODER_H
#include <vector>
#include <cstdint> #include <cstdint>
#include <variant>
#include <vector>
#include "decode_database.h" #include "decode_database.h"
@@ -26,9 +27,7 @@ struct RawCanFrame {
std::vector<std::uint8_t> data; /**< Payload bytes. */ std::vector<std::uint8_t> data; /**< Payload bytes. */
RawCanFrame() RawCanFrame()
: canId (0U) : canId(0U), isExtended(false), data() {
, isExtended (false)
, data() {
} }
}; };
@@ -37,15 +36,12 @@ 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(std::uint64_t(0U)), physicalValue(0.0), valid(false) {
, rawValue (0)
, physicalValue (0.0)
, valid (false) {
} }
}; };
@@ -58,9 +54,7 @@ struct DecodedFrameValue {
bool valid; /**< true if frame was matched. */ bool valid; /**< true if frame was matched. */
DecodedFrameValue() DecodedFrameValue()
: definition (nullptr) : definition(nullptr), signals(), valid(false) {
, signals()
, valid (false) {
} }
}; };
@@ -68,7 +62,7 @@ struct DecodedFrameValue {
* @brief Runtime CAN decoder using prebuilt decode database. * @brief Runtime CAN decoder using prebuilt decode database.
*/ */
class DbcDecoder { class DbcDecoder {
public: public:
/** /**
* @brief Find frame definition by CAN ID. * @brief Find frame definition by CAN ID.
* @param database Runtime decode database. * @param database Runtime decode database.
@@ -76,7 +70,7 @@ class DbcDecoder {
* @param isExtended true for extended frame. * @param isExtended true for extended frame.
* @return Pointer to frame definition or nullptr. * @return Pointer to frame definition or nullptr.
*/ */
const DecodeFrame *FindFrame (const DecodeDatabase &database, const DecodeFrame *FindFrame(const DecodeDatabase &database,
std::uint32_t canId, std::uint32_t canId,
bool isExtended) const; bool isExtended) const;
@@ -86,25 +80,25 @@ class DbcDecoder {
* @param frame Raw CAN frame. * @param frame Raw CAN frame.
* @return Decoded frame value. * @return Decoded frame value.
*/ */
DecodedFrameValue Decode (const DecodeDatabase &database, DecodedFrameValue Decode(const DecodeDatabase &database,
const RawCanFrame &frame) const; const RawCanFrame &frame) const;
private: private:
static bool ExtractUnsigned (const std::vector<std::uint8_t> &data, static bool ExtractUnsigned(const std::vector<std::uint8_t> &data,
const DecodeSignal &signal, const DecodeSignal &signal,
std::uint64_t &value); std::uint64_t &value);
static bool ExtractIntel (const std::vector<std::uint8_t> &data, static bool ExtractIntel(const std::vector<std::uint8_t> &data,
std::uint32_t startBit, std::uint32_t startBit,
std::uint32_t length, std::uint32_t length,
std::uint64_t &value); std::uint64_t &value);
static bool ExtractMotorola (const std::vector<std::uint8_t> &data, static bool ExtractMotorola(const std::vector<std::uint8_t> &data,
std::uint32_t startBit, std::uint32_t startBit,
std::uint32_t length, std::uint32_t length,
std::uint64_t &value); std::uint64_t &value);
static std::int64_t SignExtend (std::uint64_t value, std::uint32_t bitLength); static std::int64_t SignExtend(std::uint64_t value, std::uint32_t bitLength);
}; };
#endif /* DBC_DECODER_H */ #endif /* DBC_DECODER_H */
+25 -311
View File
@@ -10,11 +10,11 @@
*/ */
#include "dbc_parser.h" #include "dbc_parser.h"
#include "dbc_statement_handler.h"
#include <fstream>
#include <sstream>
#include <stdexcept>
#include <cctype> #include <cctype>
#include <fstream>
#include <stdexcept>
namespace { namespace {
/** /**
@@ -22,335 +22,49 @@ namespace {
* @param text Input text. * @param text Input text.
* @return Trimmed text. * @return Trimmed text.
*/ */
std::string TrimText (const std::string &text) { std::string TrimText(const std::string &text) {
std::string::size_type begin = 0U; std::string::size_type begin = 0U;
while ((begin < text.size()) && while ((begin < text.size()) &&
std::isspace (static_cast<unsigned char> (text[begin]))) std::isspace(static_cast<unsigned char>(text[begin])))
++begin; ++begin;
std::string::size_type end = text.size(); std::string::size_type end = text.size();
while ((end > begin) && while ((end > begin) &&
std::isspace (static_cast<unsigned char> (text[end - 1U]))) std::isspace(static_cast<unsigned char>(text[end - 1U])))
--end; --end;
return text.substr (begin, end - begin); return text.substr(begin, end - begin);
} }
} }// namespace
DbcDatabase DbcParser::ParseFile (const std::string &filePath) const { DbcDatabase DbcParser::ParseFile(const std::string &filePath) const {
std::ifstream input (filePath.c_str()); std::ifstream input(filePath.c_str());
if (!input.is_open()) if (!input.is_open())
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)) {
line = Trim (line); ++lineNumber;
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) { std::string DbcParser::Trim(const std::string &text) {
return (line.size() >= 4U) && (line.compare (0U, 4U, "BO_ ") == 0); return TrimText(text);
}
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) {
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;
} }
+5 -26
View File
@@ -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"
@@ -33,37 +31,18 @@
* - value tables * - value tables
*/ */
class DbcParser { class DbcParser {
public: public:
/** /**
* @brief Parse DBC file. * @brief Parse DBC file.
* @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 std::string Trim(const std::string &text);
static bool IsSignalLine (const std::string &line);
static bool IsCommentLine (const std::string &line);
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 */
+323
View File
@@ -0,0 +1,323 @@
// SPDX-License-Identifier: MIT
#include "dbc_statement_handler.h"
#include <cctype>
#include <charconv>
#include <cmath>
#include <limits>
#include <regex>
#include <sstream>
#include <stdexcept>
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
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;
}
}// namespace
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) {
// Match the complete statement before converting any numeric fields.
static const std::regex syntax(
R"dbc(^SG_[ \t]+([^:]+):[ \t]*([^| \t]+)[ \t]*\|[ \t]*([^@ \t]+)[ \t]*@[ \t]*([01])([+-])[ \t]*\([ \t]*([^, \t]+)[ \t]*,[ \t]*([^ )\t]+)[ \t]*\)[ \t]*\[[ \t]*([^| \t]+)[ \t]*\|[ \t]*([^\] \t]+)[ \t]*\][ \t]*"([^"\r\n]*)"[ \t]+([^ \t,]+(?:[ \t]*,[ \t]*[^ \t,]+)*)[ \t]*$)dbc");
std::smatch fields;
if (!std::regex_match(line, fields, syntax))
throw std::runtime_error("Malformed or unsupported SG_ signal definition.");
SignalInfo signal;
signal.name = TrimText(fields[1].str());
if (signal.name.empty() || signal.name.find_first_of(" \t\r\n") != std::string::npos)
throw std::runtime_error("Invalid signal name or unsupported multiplexing syntax.");
signal.startBit = ParseDecimal(fields[2].str(),
std::numeric_limits<std::uint32_t>::max(), "signal start bit");
signal.length = ParseDecimal(fields[3].str(), 64U, "signal length");
if (signal.length == 0U)
throw std::runtime_error("Signal length must be within 1..64 bits.");
signal.isLittleEndian = fields[4].str() == "1";
signal.isSigned = fields[5].str() == "-";
const auto parsePhysical = [](const std::string &text, const char *field) {
static const std::regex number(
R"(^[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?$)");
if (!std::regex_match(text, number))
throw std::runtime_error(std::string("Invalid ") + field + ": " + text);
std::size_t consumed = 0U;
double value;
try {
value = std::stod(text, &consumed);
} catch (const std::exception &) {
throw std::runtime_error(std::string("Invalid ") + field + ": " + text);
}
if (consumed != text.size() || !std::isfinite(value))
throw std::runtime_error(std::string("Invalid ") + field + ": " + text);
return value;
};
signal.factor = parsePhysical(fields[6].str(), "signal factor");
signal.offset = parsePhysical(fields[7].str(), "signal offset");
signal.minimum = parsePhysical(fields[8].str(), "signal minimum");
signal.maximum = parsePhysical(fields[9].str(), "signal maximum");
if (signal.minimum > signal.maximum)
throw std::runtime_error("Signal minimum exceeds maximum.");
signal.unit = fields[10].str();
signal.receivers = SplitReceivers(fields[11].str());
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;
}
}// namespace
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 {
if (context.currentFrame == nullptr)
throw std::runtime_error("Signal found before any frame definition.");
SignalInfo signal = ParseSignalLine(line);
const std::uint64_t payloadBits = static_cast<std::uint64_t>(context.currentFrame->dlc) * 8U;
std::uint64_t bit = signal.startBit;
for (std::uint32_t index = 0U; index < signal.length; ++index) {
if (bit >= payloadBits)
throw std::runtime_error("Signal exceeds the frame payload length.");
if (signal.isLittleEndian)
++bit;
else
bit = (bit % 8U == 0U) ? bit + 15U : bit - 1U;
}
context.currentFrame->signals.push_back(std::move(signal));
}
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;
}
+65
View File
@@ -0,0 +1,65 @@
// 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 */
+6 -6
View File
@@ -11,20 +11,20 @@
#include "dbc_tree_builder.h" #include "dbc_tree_builder.h"
std::unique_ptr<TreeNode> DbcTreeBuilder::Build (const DbcDatabase &database) const { std::unique_ptr<TreeNode> DbcTreeBuilder::Build(const DbcDatabase &database) const {
std::unique_ptr<TreeNode> root (new TreeNode()); std::unique_ptr<TreeNode> root(new TreeNode());
for (std::size_t frameIndex = 0U; frameIndex < database.frames.size(); ++frameIndex) { for (std::size_t frameIndex = 0U; frameIndex < database.frames.size(); ++frameIndex) {
const FrameInfo &frame = database.frames[frameIndex]; const FrameInfo &frame = database.frames[frameIndex];
std::unique_ptr<TreeNode> frameNode (new TreeNode (frame)); std::unique_ptr<TreeNode> frameNode(new TreeNode(frame));
for (std::size_t signalIndex = 0U; signalIndex < frame.signals.size(); ++signalIndex) { for (std::size_t signalIndex = 0U; signalIndex < frame.signals.size(); ++signalIndex) {
const SignalInfo &signal = frame.signals[signalIndex]; const SignalInfo &signal = frame.signals[signalIndex];
std::unique_ptr<TreeNode> signalNode (new TreeNode (signal)); std::unique_ptr<TreeNode> signalNode(new TreeNode(signal));
frameNode->AddChild (std::move (signalNode)); frameNode->AddChild(std::move(signalNode));
} }
root->AddChild (std::move (frameNode)); root->AddChild(std::move(frameNode));
} }
return root; return root;
+2 -2
View File
@@ -21,13 +21,13 @@
* @brief Builds a simple tree from parsed DBC database. * @brief Builds a simple tree from parsed DBC database.
*/ */
class DbcTreeBuilder { class DbcTreeBuilder {
public: public:
/** /**
* @brief Build tree representation of parsed DBC data. * @brief Build tree representation of parsed DBC data.
* @param database Parsed database. * @param database Parsed database.
* @return Root node of the tree. * @return Root node of the tree.
*/ */
std::unique_ptr<TreeNode> Build (const DbcDatabase &database) const; std::unique_ptr<TreeNode> Build(const DbcDatabase &database) const;
}; };
#endif /* DBC_TREE_BUILDER_H */ #endif /* DBC_TREE_BUILDER_H */
+11 -32
View File
@@ -12,10 +12,10 @@
#ifndef DECODE_DATABASE_H #ifndef DECODE_DATABASE_H
#define DECODE_DATABASE_H #define DECODE_DATABASE_H
#include <string>
#include <vector>
#include <unordered_map>
#include <cstdint> #include <cstdint>
#include <string>
#include <unordered_map>
#include <vector>
/** /**
* @brief Signal byte order used for runtime decoding. * @brief Signal byte order used for runtime decoding.
@@ -51,18 +51,7 @@ struct DecodeSignal {
std::string comment; /**< Comment. */ std::string comment; /**< Comment. */
DecodeSignal() DecodeSignal()
: name() : name(), startBit(0U), length(0U), byteOrder(ByteOrder::Intel), valueType(ValueType::Unsigned), factor(1.0), offset(0.0), minimum(0.0), maximum(0.0), unit(), receivers(), comment() {
, startBit (0U)
, length (0U)
, byteOrder (ByteOrder::Intel)
, valueType (ValueType::Unsigned)
, factor (1.0)
, offset (0.0)
, minimum (0.0)
, maximum (0.0)
, unit()
, receivers()
, comment() {
} }
}; };
@@ -81,15 +70,7 @@ struct DecodeFrame {
std::vector<DecodeSignal> signals; /**< Signal definitions. */ std::vector<DecodeSignal> signals; /**< Signal definitions. */
DecodeFrame() DecodeFrame()
: name() : name(), canId(0U), isExtended(false), dlc(0U), pgn(0U), hasPgn(false), transmitter(), comment(), signals() {
, canId (0U)
, isExtended (false)
, dlc (0U)
, pgn (0U)
, hasPgn (false)
, transmitter()
, comment()
, signals() {
} }
}; };
@@ -101,16 +82,14 @@ struct FrameKey {
bool isExtended; bool isExtended;
FrameKey() FrameKey()
: canId (0U) : canId(0U), isExtended(false) {
, isExtended (false) {
} }
FrameKey (std::uint32_t id, bool extended) FrameKey(std::uint32_t id, bool extended)
: canId (id) : canId(id), isExtended(extended) {
, isExtended (extended) {
} }
bool operator== (const FrameKey &other) const { bool operator==(const FrameKey &other) const {
return (canId == other.canId) && (isExtended == other.isExtended); return (canId == other.canId) && (isExtended == other.isExtended);
} }
}; };
@@ -119,8 +98,8 @@ struct FrameKey {
* @brief Hasher for frame key. * @brief Hasher for frame key.
*/ */
struct FrameKeyHasher { struct FrameKeyHasher {
std::size_t operator() (const FrameKey &key) const { std::size_t operator()(const FrameKey &key) const {
const std::size_t a = static_cast<std::size_t> (key.canId); const std::size_t a = static_cast<std::size_t>(key.canId);
const std::size_t b = key.isExtended ? 1U : 0U; const std::size_t b = key.isExtended ? 1U : 0U;
return (a * 1315423911U) ^ b; return (a * 1315423911U) ^ b;
} }
+2 -10
View File
@@ -12,9 +12,9 @@
#ifndef FRAME_INFO_H #ifndef FRAME_INFO_H
#define FRAME_INFO_H #define FRAME_INFO_H
#include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
#include <cstdint>
#include "signal_info.h" #include "signal_info.h"
@@ -33,15 +33,7 @@ struct FrameInfo {
std::vector<SignalInfo> signals; /**< Signals contained in the frame. */ std::vector<SignalInfo> signals; /**< Signals contained in the frame. */
FrameInfo() FrameInfo()
: name() : name(), canId(0U), isExtended(false), pgn(0U), hasPgn(false), dlc(0U), transmitter(), comment(), signals() {
, canId (0U)
, isExtended (false)
, pgn (0U)
, hasPgn (false)
, dlc (0U)
, transmitter()
, comment()
, signals() {
} }
}; };
+40
View File
@@ -0,0 +1,40 @@
# Validate `SG_` signal definitions and report malformed or unsupported syntax
## Description
Following strict validation of `BO_` frame definitions, add equivalent validation for `SG_` signal definitions. Currently, some malformed statements are accepted silently, and unsupported syntax can produce incorrect decoding.
Examples of current behavior:
- `@2+` is interpreted as Motorola instead of being rejected.
- Numeric fields containing suffixes, such as `0junk` or `1junk`, are partially parsed and accepted.
- `SG_ Value m1 : ...` is treated as an ordinary signal named `Value m1`, without multiplexing conditions.
- A statement containing a tab after `SG_` is silently skipped.
## Scope
- Validate statement structure and require a nonempty signal name without whitespace.
- Accept spaces and tabs between syntax elements.
- Parse numeric fields completely, rejecting overflow and trailing characters; preserve support for fractional values and scientific notation in physical parameters.
- Accept only byte order `@0` or `@1` and signedness `+` or `-`.
- Require signal lengths within `1..64` bits.
- Validate that the signal fits within the frame's declared payload length, using the appropriate Intel or Motorola bit traversal.
- Reject NaN and infinite values for factor, offset, minimum, and maximum; require `minimum <= maximum`.
- Explicitly reject multiplexing markers `M`, `mN`, and their combinations until multiplexing is implemented.
- Preserve the error format `path:line: description`.
## Acceptance criteria
- [ ] Existing valid signal definitions continue to parse successfully.
- [ ] Malformed and unsupported definitions stop parsing with a clear error.
- [ ] Regression tests cover the cases above, numeric boundaries, and Intel/Motorola payload boundaries.
- [ ] Existing parser and decoder tests pass.
- [ ] README documents the new validation rules and limitations.
## Out of scope
Duplicate detection, multiplexing support, and `VAL_` support remain separate tasks.
## Suggested branch
`codex/validate-sg-definitions`
+19 -18
View File
@@ -9,18 +9,18 @@
* See LICENSE file in the project root for full license text. * See LICENSE file in the project root for full license text.
*/ */
#include <iostream> #include <cstddef>
#include <iomanip> #include <iomanip>
#include <iostream>
#include <memory> #include <memory>
#include <vector> #include <vector>
#include <cstddef>
#include "dbc_parser.h"
#include "dbc_tree_builder.h"
#include "dbc_decode_builder.h" #include "dbc_decode_builder.h"
#include "dbc_decoder.h" #include "dbc_decoder.h"
#include "dbc_parser.h"
#include "dbc_tree_builder.h"
static void PrintTree (const TreeNode *node, int indent) { static void PrintTree(const TreeNode *node, int indent) {
if (node == nullptr) if (node == nullptr)
return; return;
@@ -39,7 +39,7 @@ static void PrintTree (const TreeNode *node, int indent) {
if (frame != nullptr) { if (frame != nullptr) {
std::cout << " id=0x" << std::hex << frame->canId << std::dec std::cout << " id=0x" << std::hex << frame->canId << std::dec
<< " ext=" << (frame->isExtended ? "yes" : "no") << " ext=" << (frame->isExtended ? "yes" : "no")
<< " dlc=" << static_cast<unsigned int> (frame->dlc); << " dlc=" << static_cast<unsigned int>(frame->dlc);
if (frame->hasPgn) if (frame->hasPgn)
std::cout << " pgn=" << frame->pgn; std::cout << " pgn=" << frame->pgn;
@@ -69,10 +69,10 @@ static void PrintTree (const TreeNode *node, int indent) {
} }
for (std::size_t i = 0U; i < node->GetChildCount(); ++i) for (std::size_t i = 0U; i < node->GetChildCount(); ++i)
PrintTree (node->GetChild (i), indent + 1); PrintTree(node->GetChild(i), indent + 1);
} }
static void PrintDecodedFrame (const DecodedFrameValue &decoded) { static void PrintDecodedFrame(const DecodedFrameValue &decoded) {
if (!decoded.valid || (decoded.definition == nullptr)) { if (!decoded.valid || (decoded.definition == nullptr)) {
std::cout << "No frame definition found.\n"; std::cout << "No frame definition found.\n";
return; return;
@@ -87,14 +87,15 @@ 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";
} }
} }
int main (int argc, char *argv[]) { int main(int argc, char *argv[]) {
if (argc < 2) { if (argc < 2) {
std::cerr << "Usage: dbc_demo <file.dbc>\n"; std::cerr << "Usage: dbc_demo <file.dbc>\n";
return 1; return 1;
@@ -102,16 +103,16 @@ int main (int argc, char *argv[]) {
try { try {
DbcParser parser; DbcParser parser;
DbcDatabase database = parser.ParseFile (argv[1]); DbcDatabase database = parser.ParseFile(argv[1]);
DbcTreeBuilder treeBuilder; DbcTreeBuilder treeBuilder;
std::unique_ptr<TreeNode> root = treeBuilder.Build (database); std::unique_ptr<TreeNode> root = treeBuilder.Build(database);
std::cout << "=== Parsed tree ===\n"; std::cout << "=== Parsed tree ===\n";
PrintTree (root.get(), 0); PrintTree(root.get(), 0);
DbcDecodeBuilder decodeBuilder; DbcDecodeBuilder decodeBuilder;
DecodeDatabase decodeDatabase = decodeBuilder.Build (database); DecodeDatabase decodeDatabase = decodeBuilder.Build(database);
/* /*
* Example raw frame. * Example raw frame.
@@ -120,13 +121,13 @@ int main (int argc, char *argv[]) {
RawCanFrame rawFrame; RawCanFrame rawFrame;
rawFrame.canId = decodeDatabase.frames.empty() ? 0U : decodeDatabase.frames[0].canId; rawFrame.canId = decodeDatabase.frames.empty() ? 0U : decodeDatabase.frames[0].canId;
rawFrame.isExtended = decodeDatabase.frames.empty() ? false : decodeDatabase.frames[0].isExtended; rawFrame.isExtended = decodeDatabase.frames.empty() ? false : decodeDatabase.frames[0].isExtended;
rawFrame.data.resize (8U, 0U); rawFrame.data.resize(8U, 0U);
DbcDecoder decoder; DbcDecoder decoder;
DecodedFrameValue decoded = decoder.Decode (decodeDatabase, rawFrame); DecodedFrameValue decoded = decoder.Decode(decodeDatabase, rawFrame);
std::cout << "\n=== Decoded frame ===\n"; std::cout << "\n=== Decoded frame ===\n";
PrintDecodedFrame (decoded); PrintDecodedFrame(decoded);
} catch (const std::exception &ex) { } catch (const std::exception &ex) {
std::cerr << "Error: " << ex.what() << "\n"; std::cerr << "Error: " << ex.what() << "\n";
return 2; return 2;
+2 -13
View File
@@ -12,9 +12,9 @@
#ifndef SIGNAL_INFO_H #ifndef SIGNAL_INFO_H
#define SIGNAL_INFO_H #define SIGNAL_INFO_H
#include <cstdint>
#include <string> #include <string>
#include <vector> #include <vector>
#include <cstdint>
/** /**
* @brief Describes one signal inside a DBC frame. * @brief Describes one signal inside a DBC frame.
@@ -34,18 +34,7 @@ struct SignalInfo {
std::string comment; /**< Optional signal comment. */ std::string comment; /**< Optional signal comment. */
SignalInfo() SignalInfo()
: name() : name(), startBit(0U), length(0U), isLittleEndian(true), isSigned(false), factor(1.0), offset(0.0), minimum(0.0), maximum(0.0), unit(), receivers(), comment() {
, startBit (0U)
, length (0U)
, isLittleEndian (true)
, isSigned (false)
, factor (1.0)
, offset (0.0)
, minimum (0.0)
, maximum (0.0)
, unit()
, receivers()
, comment() {
} }
}; };
+53
View File
@@ -0,0 +1,53 @@
# 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.
Signal tests cover complete syntax and numeric validation, scientific notation,
finite physical parameters and ordered ranges, spaces and tabs, receiver lists,
unsupported multiplexing, and Intel/Motorola declared-payload boundaries.
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.
+139
View File
@@ -0,0 +1,139 @@
#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;
}
+7
View File
@@ -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
+253
View File
@@ -0,0 +1,253 @@
#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");
}
const DbcParser signalParser;
const auto signalLine = [](const std::string &start, const std::string &length,
const std::string &order, const std::string &factor,
const std::string &offset, const std::string &minimum,
const std::string &maximum) {
return "SG_ Value : " + start + "|" + length + "@" + order +
" (" + factor + "," + offset + ") [" + minimum + "|" + maximum + "] \"\" ECU";
};
const auto rejectSignal = [&](const std::string &line, unsigned payload = 8U) {
fixture.Write("BO_ 1 Frame: " + std::to_string(payload) + " ECU\n\n" + line + "\n");
try {
signalParser.ParseFile(fixture.path.string());
check(false, "Reject " + 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(),
"Reject " + line);
}
};
const std::string ordinary = signalLine("0", "8", "1+", "1", "0", "0", "255");
for (const std::string value: {"-1", "+1", "0junk", "0x10", "1.5",
"4294967296", "999999999999999999999"}) {
rejectSignal(signalLine(value, "8", "1+", "1", "0", "0", "255"));
rejectSignal(signalLine("0", value, "1+", "1", "0", "0", "255"));
}
for (const std::string length: {"0", "65"})
rejectSignal(signalLine("0", length, "1+", "1", "0", "0", "255"));
for (const std::string order: {"2+", "9-", "1", "1x", "1++", "01+"})
rejectSignal(signalLine("0", "8", order, "1", "0", "0", "255"));
for (const std::string value: {"1junk", "nan", "NaN", "inf", "-inf", "1e999", "1e-999", "0x1p2", "", "1e", "1.2.3"}) {
rejectSignal(signalLine("0", "8", "1+", value, "0", "0", "255"));
rejectSignal(signalLine("0", "8", "1+", "1", value, "0", "255"));
rejectSignal(signalLine("0", "8", "1+", "1", "0", value, "255"));
rejectSignal(signalLine("0", "8", "1+", "1", "0", "0", value));
}
rejectSignal(signalLine("0", "8", "1+", "1", "0", "2", "1"));
for (const std::string name: {"", "Two Names", "Value M", "Value m1", "Value m1M"}) {
std::string line = ordinary;
line.replace(4U, 5U, name);
rejectSignal(line);
}
for (const std::string broken: {
"SG_", "SG_ Value", "SG_ Value : 0|8@1+ (1,0) [0|255] ECU",
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\"",
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU,",
"SG_ Value : 0|8@1+ (1,0) [0|255] \"\" ECU extra"})
rejectSignal(broken);
rejectSignal(signalLine("64", "1", "1+", "1", "0", "0", "1"));
rejectSignal(signalLine("63", "2", "1+", "1", "0", "0", "3"));
rejectSignal(signalLine("56", "2", "0+", "1", "0", "0", "3"));
rejectSignal(signalLine("4294967295", "64", "1+", "1", "0", "0", "1"));
rejectSignal(ordinary, 0U);
for (const char delimiter: {'|', '@', '(', ')', '[', ']', '"'}) {
std::string line = ordinary;
line.erase(line.find(delimiter), 1U);
rejectSignal(line);
}
fixture.Write("BO_ 1 Frame: 8 ECU\n" +
signalLine("0", "8", "1+", "0", "0", "0", "0") + "\n" +
signalLine("8", "8", "1-", "-1", "0", "-255", "0") + "\n");
const auto factors = signalParser.ParseFile(fixture.path.string());
check(factors.frames[0].signals.size() == 2U &&
factors.frames[0].signals[0].factor == 0.0 &&
factors.frames[0].signals[1].factor == -1.0,
"Zero and negative factors and equal range boundaries");
struct SignalCase {
const char *start;
const char *length;
const char *order;
unsigned payload;
};
const SignalCase validSignals[] = {
{"0", "1", "1+", 1U},
{"7", "1", "0-", 1U},
{"0", "64", "1+", 8U},
{"7", "64", "0-", 8U},
{"63", "1", "1+", 8U},
{"56", "1", "0+", 8U},
{"3", "12", "0+", 2U},
{"2039", "1", "1+", 255U}};
for (const auto &test: validSignals) {
fixture.Write("BO_ 1 Frame: " + std::to_string(test.payload) + " ECU\n" +
signalLine(test.start, test.length, test.order, ".5e+1", "-2.5E-1", "-1.", "+2e2") + "\n");
try {
const auto parsed = signalParser.ParseFile(fixture.path.string());
const auto &signal = parsed.frames.at(0).signals.at(0);
check(signal.startBit == std::stoul(test.start) &&
signal.length == std::stoul(test.length) &&
signal.isLittleEndian == (test.order[0] == '1') &&
signal.isSigned == (test.order[1] == '-') &&
signal.factor == 5.0 && signal.offset == -0.25 &&
signal.minimum == -1.0 && signal.maximum == 200.0,
"Valid signal boundary and numeric metadata");
} catch (const std::exception &error) { check(false, error.what()); }
}
fixture.Write("BO_ 1 Frame: 8 ECU\n"
"SG_\tValue\t:\t0 | 8 @1+ ( 1 , 0 ) [ 0 | 255 ] \"rpm\" ECU1,\tECU2\n");
try {
const auto parsed = signalParser.ParseFile(fixture.path.string());
const auto &signal = parsed.frames.at(0).signals.at(0);
check(signal.name == "Value" && signal.unit == "rpm" &&
signal.receivers == std::vector<std::string>({"ECU1", "ECU2"}),
"Signal tabs, spaces, and receiver list");
} catch (const std::exception &error) { check(false, error.what()); }
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;
}
+7
View File
@@ -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
+9 -21
View File
@@ -12,46 +12,34 @@
#include "tree_node.h" #include "tree_node.h"
TreeNode::TreeNode() TreeNode::TreeNode()
: m_type (NodeType::Root) : m_type(NodeType::Root), m_name("dbc"), m_children(), m_frame(), m_signal() {
, m_name ("dbc")
, m_children()
, m_frame()
, m_signal() {
} }
TreeNode::TreeNode (const FrameInfo &frame) TreeNode::TreeNode(const FrameInfo &frame)
: m_type (NodeType::Frame) : m_type(NodeType::Frame), m_name(frame.name), m_children(), m_frame(new FrameInfo(frame)), m_signal() {
, m_name (frame.name)
, m_children()
, m_frame (new FrameInfo (frame))
, m_signal() {
} }
TreeNode::TreeNode (const SignalInfo &signal) TreeNode::TreeNode(const SignalInfo &signal)
: m_type (NodeType::Signal) : m_type(NodeType::Signal), m_name(signal.name), m_children(), m_frame(), m_signal(new SignalInfo(signal)) {
, m_name (signal.name)
, m_children()
, m_frame()
, m_signal (new SignalInfo (signal)) {
} }
void TreeNode::AddChild (std::unique_ptr<TreeNode> child) { void TreeNode::AddChild(std::unique_ptr<TreeNode> child) {
if (child) if (child)
m_children.push_back (std::move (child)); m_children.push_back(std::move(child));
} }
std::size_t TreeNode::GetChildCount() const { std::size_t TreeNode::GetChildCount() const {
return m_children.size(); return m_children.size();
} }
const TreeNode *TreeNode::GetChild (std::size_t index) const { const TreeNode *TreeNode::GetChild(std::size_t index) const {
if (index >= m_children.size()) if (index >= m_children.size())
return nullptr; return nullptr;
return m_children[index].get(); return m_children[index].get();
} }
TreeNode *TreeNode::GetChild (std::size_t index) { TreeNode *TreeNode::GetChild(std::size_t index) {
if (index >= m_children.size()) if (index >= m_children.size())
return nullptr; return nullptr;
+14 -14
View File
@@ -12,10 +12,10 @@
#ifndef TREE_NODE_H #ifndef TREE_NODE_H
#define TREE_NODE_H #define TREE_NODE_H
#include <cstddef>
#include <memory>
#include <string> #include <string>
#include <vector> #include <vector>
#include <memory>
#include <cstddef>
#include "frame_info.h" #include "frame_info.h"
#include "signal_info.h" #include "signal_info.h"
@@ -33,7 +33,7 @@ enum class NodeType {
* @brief Tree node for later use in model/view or other hierarchy consumers. * @brief Tree node for later use in model/view or other hierarchy consumers.
*/ */
class TreeNode { class TreeNode {
public: public:
/** /**
* @brief Create root node. * @brief Create root node.
*/ */
@@ -43,27 +43,27 @@ class TreeNode {
* @brief Create frame node. * @brief Create frame node.
* @param frame Frame payload. * @param frame Frame payload.
*/ */
explicit TreeNode (const FrameInfo &frame); explicit TreeNode(const FrameInfo &frame);
/** /**
* @brief Create signal node. * @brief Create signal node.
* @param signal Signal payload. * @param signal Signal payload.
*/ */
explicit TreeNode (const SignalInfo &signal); explicit TreeNode(const SignalInfo &signal);
~TreeNode() = default; ~TreeNode() = default;
TreeNode (const TreeNode &) = delete; TreeNode(const TreeNode &) = delete;
TreeNode &operator= (const TreeNode &) = delete; TreeNode &operator=(const TreeNode &) = delete;
TreeNode (TreeNode &&) = default; TreeNode(TreeNode &&) = default;
TreeNode &operator= (TreeNode &&) = default; TreeNode &operator=(TreeNode &&) = default;
/** /**
* @brief Add child node. * @brief Add child node.
* @param child Child node to add. * @param child Child node to add.
*/ */
void AddChild (std::unique_ptr<TreeNode> child); void AddChild(std::unique_ptr<TreeNode> child);
/** /**
* @brief Get child count. * @brief Get child count.
@@ -76,14 +76,14 @@ class TreeNode {
* @param index Child index. * @param index Child index.
* @return Child pointer or nullptr if index is invalid. * @return Child pointer or nullptr if index is invalid.
*/ */
const TreeNode *GetChild (std::size_t index) const; const TreeNode *GetChild(std::size_t index) const;
/** /**
* @brief Get mutable child by index. * @brief Get mutable child by index.
* @param index Child index. * @param index Child index.
* @return Child pointer or nullptr if index is invalid. * @return Child pointer or nullptr if index is invalid.
*/ */
TreeNode *GetChild (std::size_t index); TreeNode *GetChild(std::size_t index);
/** /**
* @brief Get node type. * @brief Get node type.
@@ -109,10 +109,10 @@ class TreeNode {
*/ */
const SignalInfo *GetSignal() const; const SignalInfo *GetSignal() const;
private: private:
NodeType m_type; NodeType m_type;
std::string m_name; std::string m_name;
std::vector<std::unique_ptr<TreeNode> > m_children; std::vector<std::unique_ptr<TreeNode>> m_children;
std::unique_ptr<FrameInfo> m_frame; std::unique_ptr<FrameInfo> m_frame;
std::unique_ptr<SignalInfo> m_signal; std::unique_ptr<SignalInfo> m_signal;
}; };
+118
View File
@@ -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