1628 lines
55 KiB
Markdown
1628 lines
55 KiB
Markdown
# DBC Parser and Runtime Decode Module Documentation
|
||
|
||
## Overview
|
||
|
||
This module provides a minimal but extensible DBC parser and runtime decode foundation for the **FrameTap** project.
|
||
|
||
Its purpose is to:
|
||
|
||
- parse DBC files
|
||
- store parsed frame and signal metadata
|
||
- build a tree for future UI integration
|
||
- build a runtime-ready decode database
|
||
- decode live CAN frames
|
||
- decode CAN trace records using the same decoder
|
||
|
||
The implementation follows a simple and practical design:
|
||
|
||
- parser logic is separated from UI logic
|
||
- runtime decode structures are separated from tree structures
|
||
- Qt is not required at the parser or decoder level
|
||
- the same decode engine can be reused for live traffic and trace replay
|
||
|
||
The core library uses C++17 and has no Qt dependency. Qt integration targets the
|
||
installed Qt 6.8.2, which is the only supported Qt version. Qt Core and Qt Test are
|
||
used by the regression tests; model tests also link QtGui for graphical metatypes.
|
||
The optional adapter requires only Qt Core. Live CAN acquisition, trace-file readers,
|
||
are left to the consuming application. An optional Qt Core model adapter is provided in `qt/`.
|
||
|
||
See [changelog.md](changelog.md) for the release and development history,
|
||
[tests/README.md](tests/README.md) for test instructions, and
|
||
[generated API documentation](docs/html/index.html) for the API reference.
|
||
|
||
---
|
||
|
||
## High-Level Architecture
|
||
|
||
The module is divided into two main paths.
|
||
|
||
### 1. Parse and UI path
|
||
|
||
Used for:
|
||
|
||
- browsing frames and signals
|
||
- later integration with Qt `Model/View`
|
||
- displaying metadata
|
||
|
||
Pipeline:
|
||
|
||
```text
|
||
DBC file
|
||
↓
|
||
DbcParser
|
||
↓
|
||
DbcDatabase
|
||
↓
|
||
DbcTreeBuilder
|
||
↓
|
||
TreeNode hierarchy
|
||
↓
|
||
DbcTreeModel → Qt views
|
||
```
|
||
|
||
### 2. Runtime decode path
|
||
|
||
Used for:
|
||
|
||
- live CAN decoding
|
||
- CAN trace decoding
|
||
- fast lookup by CAN ID
|
||
|
||
Pipeline:
|
||
|
||
```text
|
||
DBC file
|
||
↓
|
||
DbcParser
|
||
↓
|
||
DbcDatabase
|
||
↓
|
||
DbcDecodeBuilder
|
||
↓
|
||
DecodeDatabase
|
||
↓
|
||
DbcDecoder
|
||
↓
|
||
Decoded values
|
||
```
|
||
|
||
This separation is intentional.
|
||
|
||
The tree is useful for UI, but it is **not** the main data structure for runtime decoding.
|
||
|
||
---
|
||
|
||
## Why the Runtime Decode Layer Exists
|
||
|
||
A tree structure is convenient for browsing, but a runtime decoder needs something different:
|
||
|
||
- fast lookup by CAN ID
|
||
- direct access to signal decode definitions
|
||
- minimal overhead during repeated decoding
|
||
- the same logic for live frames and trace frames
|
||
|
||
Because of that, the design uses a dedicated runtime-ready structure:
|
||
|
||
- `DecodeDatabase`
|
||
- `DecodeFrame`
|
||
- `DecodeSignal`
|
||
|
||
This avoids forcing UI-oriented structures into a decode role they were not meant for.
|
||
|
||
---
|
||
|
||
## Module Layout
|
||
|
||
```text
|
||
core/ C++17 parser, statement handlers, metadata, tree, and decoder
|
||
examples/ Command-line demo (built separately)
|
||
qt/ Optional read-only Qt Core model adapter
|
||
tests/ Parser, decoder, tree, and optional model tests
|
||
docs/ Generated Doxygen documentation
|
||
dbc.pro Root qmake project: core + tests
|
||
Doxyfile API documentation configuration
|
||
```
|
||
|
||
| Component | Source files |
|
||
| --- | --- |
|
||
| Parsed metadata | `core/dbc_database.h`, `core/frame_info.h`, `core/signal_info.h` |
|
||
| Parser and dispatch | `core/dbc_parser.*`, `core/dbc_diagnostic.h`, `core/dbc_statement_handler.*`, `core/typefactory.h` |
|
||
| UI tree | `core/tree_node.*`, `core/dbc_tree_builder.*` |
|
||
| Runtime database | `core/decode_database.h`, `core/dbc_decode_builder.*` |
|
||
| Decoder | `core/dbc_decoder.*` |
|
||
| Demo | `examples/main.cpp`, `examples/demo.pro` |
|
||
| Build integration | `core/core.pro`, `core/core_files.pri`, `core/core.pri` |
|
||
|
||
---
|
||
|
||
## Parsed Data Structures
|
||
|
||
## `SignalInfo`
|
||
|
||
Represents one signal as parsed from the DBC file.
|
||
|
||
Fields:
|
||
|
||
- `name`
|
||
- `startBit`
|
||
- `length`
|
||
- `isLittleEndian`
|
||
- `isSigned`
|
||
- `factor`
|
||
- `offset`
|
||
- `minimum`
|
||
- `maximum`
|
||
- `unit`
|
||
- `receivers`
|
||
- `comment`
|
||
|
||
Notes:
|
||
|
||
- `receivers` is a list because a signal may have more than one receiver ECU
|
||
- `factor` and `offset` define physical conversion
|
||
- this structure is close to DBC content and easy to inspect
|
||
|
||
Physical value rule:
|
||
|
||
```text
|
||
physical = raw * factor + offset
|
||
```
|
||
|
||
---
|
||
|
||
## `FrameInfo`
|
||
|
||
Represents one frame as parsed from the DBC file.
|
||
|
||
Fields:
|
||
|
||
- `name`
|
||
- `canId`
|
||
- `isExtended`
|
||
- `pgn`
|
||
- `hasPgn`
|
||
- `dlc`
|
||
- `transmitter`
|
||
- `comment`
|
||
- `signals`
|
||
|
||
Notes:
|
||
|
||
- `signals` is a list of `SignalInfo`
|
||
- `isExtended` is determined during CAN ID normalization
|
||
- `pgn` is derived using simplified J1939 logic when applicable
|
||
|
||
---
|
||
|
||
## `DbcDatabase`
|
||
|
||
Top-level parsed DBC container.
|
||
|
||
Conceptually:
|
||
|
||
```text
|
||
DbcDatabase
|
||
└── vector<FrameInfo>
|
||
```
|
||
|
||
This is the central structure produced by `DbcParser`.
|
||
|
||
---
|
||
|
||
## UI Tree Layer
|
||
|
||
## `TreeNode`
|
||
|
||
The UI tree contains three node types:
|
||
|
||
- `Root`
|
||
- `Frame`
|
||
- `Signal`
|
||
|
||
Example hierarchy:
|
||
|
||
```text
|
||
dbc
|
||
├── EngineData
|
||
│ ├── EngineSpeed
|
||
│ ├── OilTemp
|
||
│ └── CoolantTemp
|
||
└── VehicleData
|
||
├── VehicleSpeed
|
||
└── Odometer
|
||
```
|
||
|
||
Frame and signal nodes store copies of the corresponding metadata; the root
|
||
has no metadata payload. The payload types are:
|
||
|
||
- `FrameInfo`
|
||
- `SignalInfo`
|
||
|
||
The tree supports browsing through the optional `DbcTreeModel` adapter.
|
||
|
||
It is **not** the primary runtime decode structure.
|
||
|
||
---
|
||
|
||
## Runtime Decode Layer
|
||
|
||
## Purpose
|
||
|
||
The decode layer exists so that decoding can be fast and independent from UI concerns.
|
||
|
||
Instead of searching a tree, the decoder uses a prepared database with direct lookup.
|
||
|
||
---
|
||
|
||
## `ByteOrder`
|
||
|
||
Runtime byte order enum:
|
||
|
||
- `Intel`
|
||
- `Motorola`
|
||
|
||
This is better for decode code than passing around raw DBC characters.
|
||
|
||
---
|
||
|
||
## `ValueType`
|
||
|
||
Numeric type enum:
|
||
|
||
- `Unsigned`
|
||
- `Signed`
|
||
|
||
This is clearer than combining multiple boolean flags during runtime logic.
|
||
|
||
---
|
||
|
||
## `DecodeSignal`
|
||
|
||
Represents one runtime-ready signal definition.
|
||
|
||
Fields:
|
||
|
||
- `name`
|
||
- `startBit`
|
||
- `length`
|
||
- `byteOrder`
|
||
- `valueType`
|
||
- `factor`
|
||
- `offset`
|
||
- `minimum`
|
||
- `maximum`
|
||
- `unit`
|
||
- `receivers`
|
||
- `comment`
|
||
|
||
This structure contains all information required for extracting and converting a signal value from raw frame data.
|
||
|
||
---
|
||
|
||
## `DecodeFrame`
|
||
|
||
Represents one runtime-ready frame definition.
|
||
|
||
Fields:
|
||
|
||
- `name`
|
||
- `canId`
|
||
- `isExtended`
|
||
- `dlc`
|
||
- `pgn`
|
||
- `hasPgn`
|
||
- `transmitter`
|
||
- `comment`
|
||
- `signals`
|
||
|
||
This structure is used directly by the decoder.
|
||
|
||
---
|
||
|
||
## `FrameKey`
|
||
|
||
Stable identifier for runtime frame matching and application state.
|
||
|
||
Fields:
|
||
|
||
- `canId`
|
||
- `isExtended`
|
||
|
||
This matters because the same numeric identifier must not be confused between standard and extended frames.
|
||
|
||
---
|
||
|
||
## `DecodeDatabase`
|
||
|
||
Immutable runtime snapshot with a private frame index. Build it with
|
||
`DbcDecodeBuilder::Build(parsed)` or construct it from `std::vector<DecodeFrame>`.
|
||
Construction validates all definitions and duplicate keys before building the lookup
|
||
index or publishing the snapshot, and throws
|
||
`std::invalid_argument` for invalid metadata, including manually supplied data:
|
||
|
||
- Normalized CAN IDs must fit 11 bits for standard frames or 29 bits for extended
|
||
frames. DBC's bit-31 encoding flag must already be removed.
|
||
- Frame keys must be unique; signal names must be unique within their frame.
|
||
- Signal lengths must be 1–64 bits, with Intel/Motorola layouts entirely inside
|
||
the declared payload. Bit coordinates use overflow-safe 64-bit arithmetic.
|
||
- Byte order, numeric type, and multiplexing role must be valid enum values.
|
||
- Factor, offset, minimum, and maximum must be finite; minimum must not exceed
|
||
maximum. Zero and negative factors are allowed.
|
||
- Multiplexing requires one unsigned selector when branches are present, with
|
||
branch values in its raw range. Signals may overlap only when they are branches
|
||
selected by different raw values, including mixed byte orders.
|
||
|
||
`dlc` remains a byte count in the storage range 0–255; construction imposes no
|
||
additional CAN/CAN FD payload limit. A zero-byte frame may contain no signals.
|
||
Both `DbcDecodeBuilder::Build` and the direct constructor apply these rules.
|
||
Construction copies the input, including nested metadata, even for rvalue input.
|
||
Saved mutable aliases into that input cannot modify definitions or invalidate the
|
||
lookup index. Database copies still share the frozen snapshot; a failed
|
||
construction or replacement leaves the previous snapshot and its lookups intact.
|
||
|
||
- `GetFrames()` returns a borrowed `const std::vector<DecodeFrame>&`.
|
||
- `FindFrame(FrameKey)` returns `std::shared_ptr<const DecodeFrame>`.
|
||
- `FindSignal(SignalKey)` returns `std::shared_ptr<const DecodeSignal>`.
|
||
- Missing definitions return an empty handle.
|
||
- Copying a database shares its snapshot; replacing it installs another snapshot.
|
||
|
||
Frame lookup uses a hash index; signal lookup scans the matched frame's signals.
|
||
Decoding shares the snapshot without copying definitions or allocating ownership
|
||
objects per signal.
|
||
|
||
---
|
||
|
||
## Decoder Layer
|
||
|
||
## `RawCanFrame`
|
||
|
||
Represents a raw CAN frame to decode.
|
||
|
||
Fields:
|
||
|
||
- `canId`
|
||
- `isExtended`
|
||
- `data`
|
||
|
||
This same structure can be used for:
|
||
|
||
- live CAN bus input
|
||
- replayed trace records
|
||
- unit tests
|
||
|
||
---
|
||
|
||
## `DecodedSignalValue`
|
||
|
||
Represents one decoded signal result.
|
||
|
||
Fields:
|
||
|
||
- `definition`
|
||
- `key`
|
||
- `rawValue`
|
||
- `physicalValue`
|
||
- `valid`
|
||
- `status`
|
||
|
||
---
|
||
|
||
### 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 physical value. Raw data is also available when
|
||
`status == PhysicalConversionFailed`; other invalid results have no decoded numeric data.
|
||
|
||
Use `std::get` when the numeric type is known, or `std::get_if` to handle both:
|
||
|
||
```cpp
|
||
if (const std::int64_t *value = std::get_if<std::int64_t>(&decodedSignal.rawValue)) {
|
||
std::cout << *value;
|
||
} else {
|
||
std::cout << std::get<std::uint64_t>(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`
|
||
|
||
Represents one decoded frame result.
|
||
|
||
Fields:
|
||
|
||
- `definition`
|
||
- `key`
|
||
- `signals`
|
||
- `valid`
|
||
- `status`
|
||
|
||
`valid` means a matching frame definition was found. Check each signal's
|
||
`valid` separately: insufficient payload data can leave individual signals
|
||
invalid even when the frame is valid. An unknown frame returns `valid == false`,
|
||
a null definition, its requested key, and an empty signal list.
|
||
|
||
`FrameDecodeStatus` summarizes the result:
|
||
|
||
| Status | Meaning |
|
||
| --- | --- |
|
||
| `UnknownFrame` | No definition matches both CAN ID and standard/extended type |
|
||
| `InsufficientPayload` | Actual byte count is less than the declared `dlc` |
|
||
| `SignalErrors` | Payload is long enough, but at least one signal failed |
|
||
| `Valid` | Payload is long enough and every signal is valid or inactive |
|
||
|
||
Short-payload status takes precedence over signal errors; inspect each signal's
|
||
status for the individual cause. `valid` retains its existing meaning of a matched
|
||
definition and does not imply `status == FrameDecodeStatus::Valid`.
|
||
|
||
Payload policy uses `dlc` as a byte count, including for CAN FD; it is not the
|
||
encoded wire DLC. A shorter payload is never padded: all signals whose bytes are
|
||
available still decode, even if other signals fail. A longer payload is accepted;
|
||
bytes beyond `dlc` are ignored. Signal bit layouts must fit the declared `dlc`, so
|
||
extra bytes cannot rescue an invalid definition. Empty payloads and zero-byte
|
||
frame definitions follow the same rules. Results include every signal in definition
|
||
order, preserving successful values and definition handles on partial failures.
|
||
|
||
The decoder applies factor and offset; it does not clamp physical values to the
|
||
declared minimum and maximum. Non-finite conversion results (NaN or positive/
|
||
negative infinity), including overflow with finite factor/offset, produce
|
||
`PhysicalConversionFailed`. The exact raw value and non-finite `physicalValue`
|
||
remain available, while signal `valid` is false. Non-finite numeric parameters are
|
||
rejected during database construction. Finite parameters can still overflow during
|
||
conversion; conversion failure of a selector does not prevent raw branch selection.
|
||
|
||
### Definition ownership and reload
|
||
|
||
`DecodedFrameValue::definition` and `DecodedSignalValue::definition` are owning
|
||
`std::shared_ptr<const ...>` handles. Each handle retains the immutable runtime
|
||
snapshot. Definitions remain valid until the last database copy, lookup handle,
|
||
or decoded result owning that snapshot is released. An independently copied
|
||
signal result retains its definition even after the parent frame result is
|
||
removed. Copying or moving results preserves ownership.
|
||
|
||
Reload by assigning a newly built database. Stored results continue to describe
|
||
the old DBC, including its units, scaling, comments, and `VAL_` descriptions;
|
||
subsequent decoding uses the new DBC. Removed definitions cannot be found in the
|
||
new database, but old results safely retain their old definitions. Keeping even
|
||
one signal handle retains the whole old snapshot; release stored results when
|
||
that metadata is no longer needed.
|
||
|
||
`GetFrames()` is a borrowed view. References, iterators, and raw pointers obtained
|
||
from it or from a handle's `.get()` do not retain ownership. Keep an owning handle
|
||
or an unchanged database copy while using them. Constructor input is copied and
|
||
may be changed independently afterward. The raw string pointer from
|
||
`GetValueDescription()` follows the same rule: keep the signal definition handle
|
||
alive while using the string.
|
||
|
||
Parsed `DbcDatabase`/`FrameInfo`/`SignalInfo` remain ordinary mutable value
|
||
containers: borrowed pointers follow standard vector invalidation rules. Builders
|
||
copy their metadata, so parsed input may be changed or destroyed after building.
|
||
Tree nodes own metadata copies; the Qt model owns its tree and invalidates model
|
||
indexes on reset. Runtime handles do not depend on the parsed database or tree.
|
||
Synchronize replacement of a shared database variable with decoding; immutable
|
||
snapshots do not make concurrent assignment to the same object safe.
|
||
|
||
### Stable frame and signal identifiers
|
||
|
||
`DecodedFrameValue::key` is a `FrameKey` (normalized `canId`, `isExtended`), also
|
||
available through `DecodeFrame::GetKey()`. `DecodedSignalValue::key` is a
|
||
`SignalKey` (frame key, exact case-sensitive signal name). Both are value types
|
||
with equality and hashers (`FrameKeyHasher`, `SignalKeyHasher`) for application
|
||
maps and saved selections. Construct the same keys from parsed metadata when
|
||
needed; do not use addresses or vector positions as identifiers.
|
||
|
||
Keys stay equal across reloads and reordering when their components stay equal.
|
||
Frame renaming and changes to units, bit layout, scaling, or descriptions do not
|
||
change a key. Changing a CAN ID/frame type or renaming a signal produces a new
|
||
key. The same name in different frames and standard/extended frames with the
|
||
same numeric CAN ID remain distinct. Keys identify definitions within one logical
|
||
DBC database; applications combining databases must add their own database ID.
|
||
A key is not a version identifier: matching keys in different snapshots may have
|
||
different metadata. Use `valid`/the definition handle to distinguish a matched
|
||
result; default keys alone do not indicate successful decoding.
|
||
|
||
```cpp
|
||
DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
|
||
DecodedFrameValue stored = DbcDecoder().Decode(runtime, rawFrame);
|
||
if (stored.valid && !stored.signals.empty()) {
|
||
DecodedSignalValue saved = stored.signals.front();
|
||
SignalKey selection = saved.key;
|
||
runtime = DbcDecodeBuilder().Build(parser.ParseFile("updated.dbc"));
|
||
std::shared_ptr<const DecodeSignal> current = runtime.FindSignal(selection);
|
||
// saved.definition retains old metadata; current holds new metadata or is empty.
|
||
}
|
||
```
|
||
|
||
**API migration:** replace reads of `runtime.frames` with `runtime.GetFrames()`.
|
||
To edit runtime definitions, prepare a vector and construct a new database;
|
||
`frameIndexByKey` is now private and built automatically. `DbcDecoder::FindFrame`
|
||
and result `definition` fields now use owning handles instead of raw pointers;
|
||
`definition->name`, boolean/null checks, and existing decode calls remain usable.
|
||
Use `.get()` only for temporary raw-pointer interoperation with an owner alive.
|
||
|
||
---
|
||
|
||
## `DbcDecoder`
|
||
|
||
Main runtime decoder class.
|
||
|
||
Responsibilities:
|
||
|
||
- find a frame definition by CAN ID
|
||
- decode unconditional signals and the active multiplexed branch
|
||
- extract raw values
|
||
- sign-extend signed values
|
||
- convert raw values into physical values
|
||
|
||
Main methods:
|
||
|
||
- `FindFrame(...)`
|
||
- `Decode(...)`
|
||
|
||
---
|
||
|
||
## Parser Support
|
||
|
||
The current parser supports the following DBC elements:
|
||
|
||
- `BO_`
|
||
- `SG_`
|
||
- `CM_ BO_`
|
||
- `CM_ SG_`
|
||
- `VAL_` signal value descriptions
|
||
- basic `M` / `mN` multiplexing
|
||
|
||
### Structured diagnostics
|
||
|
||
`ParseFileWithDiagnostics(path)` returns `DbcParseResult` containing `database`
|
||
and `diagnostics`. Each `DbcDiagnostic` has `severity`, `code`, `filePath`,
|
||
`lineNumber`, and `message`. Paths preserve the caller's supplied spelling.
|
||
Lines are one-based physical lines, including blank lines; file-level errors use zero.
|
||
|
||
```cpp
|
||
const DbcParser parser;
|
||
const DbcParseResult result = parser.ParseFileWithDiagnostics("example.dbc");
|
||
for (const DbcDiagnostic &diagnostic : result.diagnostics) {
|
||
std::cerr << diagnostic.filePath << ":" << diagnostic.lineNumber
|
||
<< ": " << diagnostic.message << '\n';
|
||
}
|
||
if (!result.HasErrors()) {
|
||
const DecodeDatabase runtime = DbcDecodeBuilder().Build(result.database);
|
||
// Inspect warnings before relying on decoding results.
|
||
}
|
||
```
|
||
|
||
| Severity / code | Meaning |
|
||
| --- | --- |
|
||
| `Error` / `FileOpenError` | Input cannot be opened |
|
||
| `Error` / `FileReadError` | Input cannot be read completely |
|
||
| `Error` / `InvalidStatement` | Invalid frame/signal definition, duplicate, or payload violation |
|
||
| `Error` / `InvalidValueDescription` | Invalid `VAL_` syntax, duplicate value, or unresolved reference |
|
||
| `Error` / `InvalidMultiplexing` | Selector, branch range, or signal overlap validation failed |
|
||
| `Warning` / `UnsupportedStatement` | Unregistered statement skipped; decoding or metadata may be incomplete |
|
||
| `Error` / `UnsupportedStatement` | Extended multiplexing (`SG_MUL_VAL_`) rejected |
|
||
|
||
Parsing stops at the first error and returns an empty database, together with
|
||
any preceding warnings and the error. Deferred `VAL_` and multiplexing errors
|
||
retain their original source lines. Invalid signal markers use `InvalidStatement`.
|
||
`HasErrors()` checks severity; warnings alone do not make a parse fail.
|
||
|
||
Unknown statements are reported conservatively, including `SIG_VALTYPE_`,
|
||
attributes (`BA_`, `BA_DEF_`, defaults and relation forms), signal types/references,
|
||
environment variables, named `VAL_TABLE_` tables, and vendor extensions. For
|
||
example, ignoring `SIG_VALTYPE_` may decode a floating-point signal as an integer.
|
||
The parser does not implement the warned features; consumers should review warnings.
|
||
|
||
Common `VERSION`, `NS_`, `BS_`, and `BU_` headers produce no diagnostics.
|
||
Single-token symbols inside the `NS_` declaration list are quiet regardless of
|
||
indentation, even when they name unsupported syntax. Frame, signal, bus, and
|
||
node definitions or a line with arguments end the list. Actual unsupported
|
||
statements outside that list are reported.
|
||
Node and bus metadata are not stored. Other comment forms (`CM_`, `CM_ BU_`,
|
||
`CM_ EV_`) are ignored by the comment handler; comments only attach to already
|
||
parsed frames/signals. Supported statements currently require one physical line.
|
||
|
||
`ParseFile(path)` remains compatible: it returns the database, discards warnings,
|
||
and throws `std::runtime_error` on the first error with the existing `path:line:`
|
||
format. Use `ParseFileWithDiagnostics` to see skipped syntax. Resource failures
|
||
such as allocation errors may still throw.
|
||
|
||
---
|
||
|
||
## Supported DBC Syntax
|
||
|
||
## Frame definition
|
||
|
||
Example:
|
||
|
||
```text
|
||
BO_ 256 EngineData: 8 EEC1
|
||
```
|
||
|
||
Parsed fields:
|
||
|
||
- frame CAN ID
|
||
- frame name
|
||
- DLC
|
||
- transmitter ECU
|
||
|
||
---
|
||
|
||
### 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
|
||
```
|
||
|
||
Frame and signal definitions are validated as described here; comprehensive
|
||
DBC grammar coverage is still planned. File-open exceptions contain the path but
|
||
no line number. The structured diagnostic API uses line zero for file-level errors.
|
||
|
||
---
|
||
|
||
## Signal definition
|
||
|
||
Example:
|
||
|
||
```text
|
||
SG_ EngineSpeed : 0|16@1+ (0.125,0) [0|8000] "rpm" ECU1,ECU2
|
||
```
|
||
|
||
Parsed fields:
|
||
|
||
- signal name
|
||
- start bit
|
||
- signal length
|
||
- byte order
|
||
- signedness
|
||
- factor
|
||
- offset
|
||
- minimum
|
||
- maximum
|
||
- unit
|
||
- receivers
|
||
|
||
---
|
||
|
||
### 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.
|
||
|
||
Basic multiplexing markers `M` and `mN` are supported as described below.
|
||
Overlapping signals are rejected unless their branch values differ.
|
||
Duplicate frame keys and signal names within a frame are rejected.
|
||
|
||
Invalid signal definitions stop parsing with `path:line: description`, including
|
||
when the signal exceeds the declared payload length.
|
||
|
||
---
|
||
|
||
## Basic multiplexing
|
||
|
||
```dbc
|
||
BO_ 256 Status: 3 ECU
|
||
SG_ Mode M : 0|8@1+ (1,0) [0|255] "" ECU
|
||
SG_ Temperature m1 : 8|8@1- (1,0) [-128|127] "C" ECU
|
||
SG_ Pressure m2 : 8|8@1+ (2,0) [0|510] "kPa" ECU
|
||
SG_ Counter : 16|8@1+ (1,0) [0|255] "" ECU
|
||
```
|
||
|
||
`SignalInfo` and `DecodeSignal` contain `multiplexing`: a `MultiplexRole`
|
||
(`Unconditional`, `Selector`, or `Branch`) and an exact `uint64_t` branch `value`.
|
||
Each frame may have one unsigned selector (`M`); a branch (`mN`) references that
|
||
selector implicitly. The selector may appear after its branches. `N` must contain
|
||
only decimal digits, fit `uint64_t`, and fit the selector's bit width. Leading
|
||
zeros are accepted. A selector without branches is allowed. Signed selectors,
|
||
missing selectors, multiple selectors, nested/combined markers, and extended
|
||
`SG_MUL_VAL_` statements are rejected with the originating `path:line`.
|
||
The dispatcher registers the unsupported statement handler through `TypeFactory`.
|
||
|
||
Signal bits may overlap only between branches with different selector values.
|
||
This validation uses Intel or Motorola bit traversal, including mixed byte orders.
|
||
It also rejects overlapping unconditional signals. Metadata survives the runtime
|
||
and tree builders; the optional Qt adapter does not add multiplexing roles yet.
|
||
|
||
The shared decoder extracts the selector before processing branches, compares its
|
||
**raw unsigned value**, and leaves result order equal to definition order. Factor,
|
||
offset, and `VAL_` descriptions do not influence branch selection. Unconditional
|
||
signals continue to decode. An unmapped selector value leaves all branches inactive.
|
||
|
||
Each result has `SignalDecodeStatus status`:
|
||
|
||
| Status | Meaning |
|
||
| --- | --- |
|
||
| `Valid` | Raw and physical values are available; `valid == true` |
|
||
| `Inactive` | Selector is available and chooses another branch |
|
||
| `SelectorUnavailable` | Branch activation is unknown because selector bytes are missing |
|
||
| `InsufficientPayload` | Selected or unconditional signal bytes are missing |
|
||
| `InvalidDefinition` | Defensive decoder check failed; invalid definitions are rejected during database construction |
|
||
| `PhysicalConversionFailed` | Raw value is available, but the converted physical value is NaN or infinity |
|
||
|
||
All statuses except `Valid` have `valid == false` and `GetValueDescription()`
|
||
returns nullptr. `PhysicalConversionFailed` preserves extracted raw data and the
|
||
non-finite conversion result; other invalid statuses have no decoded numeric data. Check `status` to
|
||
distinguish inactive branches from errors. Consumers constructing runtime metadata
|
||
manually receive the same selector and non-overlap validation as parsed files. Extended and nested
|
||
multiplexing are outside this stage and are explicitly rejected, rather than
|
||
silently decoded as unconditional signals.
|
||
|
||
---
|
||
|
||
## Comments
|
||
|
||
Frame comment example:
|
||
|
||
```text
|
||
CM_ BO_ 256 "Engine data frame";
|
||
```
|
||
|
||
Signal comment example:
|
||
|
||
```text
|
||
CM_ SG_ 256 EngineSpeed "Actual engine speed";
|
||
```
|
||
|
||
Stored in:
|
||
|
||
- `FrameInfo::comment`
|
||
- `SignalInfo::comment`
|
||
|
||
---
|
||
|
||
## CAN ID Normalization
|
||
|
||
The parser normalizes frame identifiers.
|
||
|
||
Common DBC behavior:
|
||
|
||
- extended identifiers may be stored with bit 31 set
|
||
- the actual 29-bit identifier must be extracted from that value
|
||
|
||
The parser therefore stores:
|
||
|
||
- normalized `canId`
|
||
- separate `isExtended` flag
|
||
|
||
This is important both for correct lookup and for future interoperability with live CAN APIs.
|
||
|
||
---
|
||
|
||
## PGN Extraction
|
||
|
||
PGN is derived only when the frame is treated as extended.
|
||
|
||
The current logic is simplified J1939 extraction:
|
||
|
||
- `pf`
|
||
- `ps`
|
||
- `dp`
|
||
|
||
This is enough for a practical start but should not be treated as full J1939 validation.
|
||
|
||
---
|
||
|
||
## Decode Flow
|
||
|
||
Typical runtime decode flow:
|
||
|
||
```text
|
||
RawCanFrame
|
||
↓
|
||
Find frame in DecodeDatabase
|
||
↓
|
||
Extract multiplexor selector first (when present)
|
||
For each unconditional or active signal:
|
||
extract raw bits
|
||
apply sign extension if needed
|
||
convert to physical value
|
||
↓
|
||
DecodedFrameValue
|
||
```
|
||
|
||
---
|
||
|
||
## Intel and Motorola Extraction
|
||
|
||
The decoder currently has separate extraction paths:
|
||
|
||
- `ExtractIntel(...)`
|
||
- `ExtractMotorola(...)`
|
||
|
||
This is important because byte order is not just metadata once decoding starts.
|
||
|
||
Intel and Motorola require different bit extraction logic.
|
||
|
||
This is one of the main reasons why the runtime decode layer should be explicit and prepared in advance.
|
||
|
||
---
|
||
|
||
## Example Usage
|
||
|
||
Include the public headers from the `core/` include directory supplied by
|
||
`core/core.pri`:
|
||
|
||
```cpp
|
||
#include "dbc_parser.h"
|
||
#include "dbc_tree_builder.h"
|
||
#include "dbc_decode_builder.h"
|
||
#include "dbc_decoder.h"
|
||
```
|
||
|
||
### Parse DBC
|
||
|
||
```cpp
|
||
DbcParser parser;
|
||
DbcDatabase database = parser.ParseFile("example.dbc");
|
||
```
|
||
|
||
### Build UI tree
|
||
|
||
```cpp
|
||
DbcTreeBuilder treeBuilder;
|
||
std::unique_ptr<TreeNode> root = treeBuilder.Build(database);
|
||
```
|
||
|
||
### Build runtime decode database
|
||
|
||
```cpp
|
||
DbcDecodeBuilder decodeBuilder;
|
||
DecodeDatabase decodeDatabase = decodeBuilder.Build(database);
|
||
```
|
||
|
||
### Decode a raw frame
|
||
|
||
```cpp
|
||
RawCanFrame rawFrame;
|
||
rawFrame.canId = 256; // EngineData from the DBC example above
|
||
rawFrame.isExtended = false;
|
||
rawFrame.data = {0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||
|
||
DbcDecoder decoder;
|
||
DecodedFrameValue decoded = decoder.Decode(decodeDatabase, rawFrame);
|
||
```
|
||
|
||
---
|
||
|
||
## Unified Decode Strategy
|
||
|
||
A key design goal is that the same decoder should work for both:
|
||
|
||
- live CAN frames
|
||
- trace replay frames
|
||
|
||
That means this architecture supports:
|
||
|
||
### live path
|
||
|
||
```text
|
||
live CAN input
|
||
↓
|
||
RawCanFrame
|
||
↓
|
||
DbcDecoder
|
||
↓
|
||
decoded signal values
|
||
```
|
||
|
||
### trace path
|
||
|
||
```text
|
||
trace reader
|
||
↓
|
||
RawCanFrame
|
||
↓
|
||
DbcDecoder
|
||
↓
|
||
decoded signal values
|
||
```
|
||
|
||
This avoids duplicating decode logic in two separate parts of the application.
|
||
|
||
---
|
||
|
||
## Intended Use in FrameTap
|
||
|
||
This module is meant to support at least the following FrameTap workflows:
|
||
|
||
- load a DBC file
|
||
- browse frames and signals
|
||
- search signals
|
||
- drag a signal into a plot later
|
||
- decode live CAN traffic
|
||
- decode recorded traces
|
||
- convert raw values into physical values
|
||
- show metadata like units, comments, transmitter, receivers, and PGN
|
||
|
||
Example combined workflow:
|
||
|
||
```text
|
||
Load DBC
|
||
↓
|
||
Parse into DbcDatabase
|
||
↓
|
||
Build UI tree
|
||
↓
|
||
Build DecodeDatabase
|
||
↓
|
||
Use same decode engine for:
|
||
- live frames
|
||
- trace replay
|
||
```
|
||
|
||
---
|
||
|
||
## Why the Tree Is Not Enough
|
||
|
||
The tree exists for browsing.
|
||
|
||
However, runtime decode should not rely on tree traversal because that would introduce unnecessary coupling and inefficiency.
|
||
|
||
A runtime decoder needs:
|
||
|
||
- fast key-based access
|
||
- minimal interpretation at decode time
|
||
- direct signal definitions already prepared
|
||
|
||
That is why `DecodeDatabase` is a separate layer.
|
||
|
||
---
|
||
|
||
## Statement Handler Factory
|
||
|
||
`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 dispatcher uses `TypeFactory<std::string, DbcStatementHandler>` to create
|
||
registered handlers once per parsing session, then reuses them for all matching
|
||
lines:
|
||
|
||
- `BO_` → `DbcFrameHandler`
|
||
- `SG_` → `DbcSignalHandler`
|
||
- `CM_` → `DbcCommentHandler`
|
||
- `VAL_` → `DbcValueDescriptionHandler`
|
||
- `SG_MUL_VAL_` → `DbcUnsupportedMultiplexingHandler`
|
||
- `VERSION`, `NS_`, `BS_`, `BU_` → `DbcHeaderHandler` (also accepts attached colons)
|
||
|
||
Grammar implementations live in `core/dbc_statement_handler.cpp`; their interface,
|
||
context, and dispatcher live in `core/dbc_statement_handler.h`. `core/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;
|
||
```
|
||
|
||
Register it in the dispatcher constructor:
|
||
|
||
```cpp
|
||
RegisterType<MyCustomHandler>("CUSTOM_"); // after implementing MyCustomHandler
|
||
```
|
||
|
||
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 produce `UnsupportedStatement` warnings through
|
||
`ParseFileWithDiagnostics`. `SG_` and `CM_` accept spaces and tabs after their 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.
|
||
|
||
---
|
||
|
||
## Signal value descriptions (`VAL_`)
|
||
|
||
Signal descriptions attach text to exact raw integer values:
|
||
|
||
```dbc
|
||
BO_ 256 Status: 8 ECU
|
||
SG_ State : 0|8@1+ (1,0) [0|255] "" ECU
|
||
VAL_ 256 State 0 "Off" 1 "On" 2 "Error";
|
||
```
|
||
|
||
Each statement must occupy one physical line, contain at least one integer/text
|
||
pair, and end with a semicolon. Spaces and tabs are accepted between fields;
|
||
descriptions may contain whitespace, semicolons, and empty text. Inside quoted
|
||
text, `\"` and `\\` represent a quote and a backslash; other escapes are rejected.
|
||
Keys are decimal integers in `INT64_MIN..UINT64_MAX`, with an optional minus
|
||
sign; plus signs, fractions, hexadecimal numbers, exponents, and overflow are
|
||
rejected. Keys never pass through `double`.
|
||
|
||
`VAL_` may appear before or after its frame and signal declarations. References
|
||
are resolved after reading the file using the same CAN ID normalization as
|
||
`BO_`, including bit 31 for extended identifiers. Standard and extended frames
|
||
with the same normalized ID remain distinct. Missing or ambiguous references,
|
||
malformed statements, and duplicate integer keys produce `path:line: description`
|
||
errors at the originating `VAL_` line. Distinct keys from multiple statements
|
||
are merged; `0`, `00`, and `-0` are the same key. Keys need not fit the referenced
|
||
signal's bit width or signedness; unreachable keys simply cannot match a decode.
|
||
|
||
Mappings are available as `SignalInfo::valueDescriptions` and
|
||
`DecodeSignal::valueDescriptions`, and survive both database builders, including
|
||
`TreeNode::GetSignal()`. Consumers can look up a decoded result directly:
|
||
|
||
```cpp
|
||
const DecodedSignalValue &value = decodedFrame.signals.at(0);
|
||
if (const std::string *description = value.GetValueDescription()) {
|
||
std::cout << *description;
|
||
}
|
||
```
|
||
|
||
`GetValueDescription()` uses the exact raw integer before factor/offset scaling.
|
||
It returns `nullptr` for an invalid decode or an unknown value; numeric decoding
|
||
and `physicalValue` are unchanged. A mapped empty string returns a non-null
|
||
pointer. The pointer belongs to the signal definition and stays valid while that
|
||
definition remains alive. A decoded signal owns its immutable definition across reload.
|
||
|
||
For metadata or standalone raw values, use
|
||
`FindValueDescription(signal.valueDescriptions, rawValue)`, where `rawValue` is
|
||
`RawSignalValue` (`std::variant<std::int64_t, std::uint64_t>`). The helper matches
|
||
nonnegative signed and unsigned values identically and keeps negative values
|
||
distinct from large unsigned values. Stored keys use negative `int64_t` and
|
||
nonnegative `uint64_t`; when constructing mappings manually, normalize keys
|
||
with `NormalizeValueKey()` before insertion.
|
||
|
||
Named `VAL_TABLE_` tables, environment-variable mappings, multiline statements,
|
||
are outside this implementation; the optional Qt adapter exposes signal mappings.
|
||
|
||
---
|
||
|
||
## Current Limitations
|
||
|
||
This is still a minimal implementation.
|
||
|
||
Not supported yet:
|
||
|
||
- extended and nested multiplexing (`SG_MUL_VAL_`, `mNM`)
|
||
- named `VAL_TABLE_` tables and environment-variable value descriptions
|
||
- `BA_` attributes
|
||
- `BA_DEF_` definitions and attribute defaults
|
||
- `SIG_VALTYPE_` floating-point signal types
|
||
- signal types/references (`SGTYPE_`, `SIG_TYPE_REF_`) and environment variables
|
||
- advanced comment handling
|
||
- full DBC grammar coverage
|
||
- full J1939 validation
|
||
- extensive edge-case handling for unusual DBC formatting
|
||
|
||
Additional behavior to account for:
|
||
|
||
- unregistered DBC keywords are skipped with warnings in the diagnostic API
|
||
- an `SG_` statement before the first frame is rejected
|
||
- duplicate frame keys (normalized CAN ID and frame type) and repeated signal
|
||
names within a frame are rejected at their definition lines; manually created
|
||
databases are also checked for ambiguous `VAL_` references during resolution
|
||
- payload lengths are checked against storage bounds (`0..255`), rather than
|
||
CAN or CAN FD protocol limits
|
||
- comment handling is limited to the supported forms and already parsed targets
|
||
|
||
Motorola extraction uses DBC sawtooth numbering: a start bit of 7 traverses
|
||
`7..0, 15..8, 23..16`. Regression tests cover aligned and unaligned signals,
|
||
byte boundaries, and 64-bit values.
|
||
|
||
---
|
||
|
||
## Recommended Next Steps
|
||
|
||
A practical development order would be:
|
||
|
||
### Stage 1 - already implemented
|
||
|
||
- `BO_`
|
||
- `SG_`
|
||
- `CM_`
|
||
- `VAL_` signal value descriptions
|
||
- normalized CAN ID
|
||
- `isExtended`
|
||
- transmitter
|
||
- receivers
|
||
- comments
|
||
- tree representation
|
||
- runtime decode database
|
||
- runtime decoder
|
||
|
||
### Stage 2
|
||
|
||
Recommended additions:
|
||
|
||
- selective decoding of only chosen signals
|
||
|
||
### Stage 3
|
||
|
||
Recommended additions:
|
||
|
||
- basic multiplexing (implemented)
|
||
- attribute parsing
|
||
- richer J1939 support
|
||
- CSV export of decoded traces
|
||
- optimized filtering and signal selection
|
||
|
||
### Stage 4
|
||
|
||
Advanced functionality:
|
||
|
||
- live plot integration
|
||
- signal subscriptions
|
||
- per-signal trace decode pipelines
|
||
- decoder-assisted export formats
|
||
|
||
---
|
||
|
||
## Build Integration
|
||
|
||
The repository supplies qmake projects. A C++17 compiler is required; the root
|
||
build also requires Qt Core and Qt Test development packages. Other build
|
||
systems can compile the sources listed in `core/core_files.pri`; no CMake
|
||
configuration is currently provided.
|
||
|
||
Qt-dependent qmake targets share `qt/qt_version.pri`, which rejects Qt versions
|
||
other than 6.8.2. The core library remains independent of Qt libraries.
|
||
|
||
Open `dbc.pro` in Qt Creator to see two subprojects: `core` and `tests`.
|
||
`core/core.pro` builds the C++17 static library `dbc_core` without a Qt dependency.
|
||
`tests/tests.pro` builds the QtTest executable `dbc_tests` and links that library.
|
||
The root project declares `tests.depends = core` for parallel builds.
|
||
|
||
Build out of source with Qt 6.8.2:
|
||
|
||
```sh
|
||
mkdir build
|
||
cd build
|
||
qmake6 ../dbc.pro
|
||
make -j
|
||
make check
|
||
```
|
||
|
||
`core/core_files.pri` lists the library sources and headers.
|
||
Consumers include `core/core.pri` for include paths and library linkage; the test
|
||
source lists live in `tests/parser_tests.pri` and `tests/decoder_tests.pri`.
|
||
When building a consumer separately, pass
|
||
`DBC_CORE_BUILD_DIR=/absolute/path/to/build/core` to qmake.
|
||
|
||
### Build core without Qt libraries
|
||
|
||
From the repository root:
|
||
|
||
```sh
|
||
mkdir -p build/core
|
||
cd build/core
|
||
qmake6 ../../core/core.pro
|
||
make -j
|
||
```
|
||
|
||
qmake is still the build tool; this target does not link Qt libraries.
|
||
|
||
### Optional Qt model adapter
|
||
|
||
The adapter and its `QAbstractItemModelTester` suite use Qt 6.8.2.
|
||
The adapter itself links only Qt Core.
|
||
|
||
The model test executable additionally links QtGui and uses QGuiApplication so
|
||
QAbstractItemModelTester can resolve graphical metatypes such as QPixmap. It
|
||
defaults to the `offscreen` platform when `QT_QPA_PLATFORM` is unset or empty;
|
||
no display server is required. An explicit platform setting is preserved.
|
||
|
||
Enable its library and focused QtTest suite:
|
||
|
||
```sh
|
||
mkdir -p build-qt
|
||
cd build-qt
|
||
qmake6 ../dbc.pro CONFIG+=dbc_qt
|
||
make -j
|
||
make check
|
||
```
|
||
|
||
Consumers include `qt/qt.pri` instead of `core/core.pri`, and set
|
||
`DBC_CORE_BUILD_DIR` and `DBC_QT_BUILD_DIR` to the corresponding library build
|
||
directories when building separately. A widget application also needs `QT += widgets`. The adapter integration enables
|
||
`no_keywords`, because core metadata has a `signals` member; use `Q_SIGNALS`,
|
||
`Q_SLOTS`, and `Q_EMIT` in consuming Qt classes.
|
||
|
||
```cpp
|
||
#include "dbc_parser.h"
|
||
#include "dbc_tree_builder.h"
|
||
#include "dbc_tree_model.h"
|
||
#include <QTreeView>
|
||
|
||
// Run after constructing QApplication; view owns the model through QObject.
|
||
QTreeView view;
|
||
DbcTreeModel *model = new DbcTreeModel(&view);
|
||
model->SetTree(DbcTreeBuilder().Build(DbcParser().ParseFile("example.dbc")));
|
||
view.setModel(model);
|
||
view.expandAll();
|
||
view.show();
|
||
// On reload, pass a newly built tree to SetTree(); to clear:
|
||
// model->SetTree(nullptr);
|
||
```
|
||
|
||
The root is invisible: frames are top-level rows, signals are their children.
|
||
Only column zero has children. `parent()` always returns column zero.
|
||
The model takes exclusive ownership of the tree; do not retain mutable aliases.
|
||
Metadata is copied by the tree builder, so the parsed database may be destroyed.
|
||
`TreeNode::GetParent()` is non-owning and `GetRow()` gives the zero-based sibling
|
||
position (zero for a detached node/root). Nodes cannot be copied or moved;
|
||
transferring their `unique_ptr` ownership preserves addresses and navigation.
|
||
|
||
Call `SetTree()` on the model's thread. It accepts a detached Root or nullptr,
|
||
uses `beginResetModel()`/`endResetModel()`, and destroys the previous tree during
|
||
reset. All ordinary QModelIndex values expire on reset and must not be reused;
|
||
QPersistentModelIndex values are invalidated by Qt. Destroying the model also
|
||
invalidates indexes. Invalid and foreign indexes return no data/flags/children.
|
||
Valid items are enabled and selectable, with no editing support.
|
||
|
||
| Column (zero-based enum) | Frame display | Signal display |
|
||
| --- | --- | --- |
|
||
| 0 `Name` | Name | Name |
|
||
| 1 `CanId` | Normalized hexadecimal ID | Empty |
|
||
| 2 `PayloadLength` | Bytes | Empty |
|
||
| 3 `StartBit` | Empty | DBC start bit |
|
||
| 4 `BitLength` | Empty | Bit count |
|
||
| 5 `ByteOrder` | Empty | Intel / Motorola |
|
||
| 6 `Signed` | Empty | Signed / Unsigned |
|
||
| 7 `Unit` | Empty | Unit |
|
||
| 8 `Comment` | Comment | Comment |
|
||
|
||
Horizontal DisplayRole headers describe these columns; other header requests
|
||
return invalid QVariant. ToolTipRole returns the comment on every column.
|
||
Custom roles also apply on every column; inapplicable roles return invalid QVariant:
|
||
|
||
| Role | Node types | QVariant content |
|
||
| --- | --- | --- |
|
||
| `NodeTypeRole` | Frame, signal | int corresponding to NodeType |
|
||
| `CanIdRole`, `PayloadLengthRole`, `ExtendedRole` | Frame | uint, int, bool |
|
||
| `StartBitRole`, `BitLengthRole` | Signal | uint |
|
||
| `LittleEndianRole`, `SignedRole` | Signal | bool |
|
||
| `UnitRole` | Signal | QString |
|
||
| `CommentRole` | Frame, signal | QString |
|
||
| `ValueDescriptionsRole` | Signal | QVariantList of QVariantMap entries |
|
||
|
||
Each value-description entry contains `value` (qlonglong for negative keys,
|
||
qulonglong for nonnegative keys) and `description` (QString). This preserves the
|
||
full signed/unsigned 64-bit range without converting through double. An unmapped
|
||
signal returns an empty list. Data comes from `SignalInfo::valueDescriptions`.
|
||
Unsupported roles return invalid QVariant.
|
||
|
||
### Standalone tree tests without Qt
|
||
|
||
From the repository root, use only a C++17 compiler:
|
||
|
||
```sh
|
||
c++ -std=c++17 -Icore tests/tree_node_test.cpp core/tree_node.cpp \
|
||
core/dbc_tree_builder.cpp -o /tmp/dbc_tree_node_test
|
||
/tmp/dbc_tree_node_test
|
||
```
|
||
|
||
### Build and run the demo
|
||
|
||
After building core, run these commands from the repository root:
|
||
|
||
```sh
|
||
mkdir -p build/examples
|
||
cd build/examples
|
||
qmake6 ../../examples/demo.pro DBC_CORE_BUILD_DIR=../core
|
||
make -j
|
||
./dbc_demo /absolute/path/to/example.dbc
|
||
```
|
||
|
||
The demo prints the parsed tree and decodes eight zero bytes using the first
|
||
frame definition. Replace that sample payload in `examples/main.cpp` to feed
|
||
live CAN data or trace records.
|
||
|
||
### Generate API documentation
|
||
|
||
With Doxygen installed, run from the repository root:
|
||
|
||
```sh
|
||
doxygen Doxyfile
|
||
```
|
||
|
||
Open `docs/html/index.html` to view the generated reference.
|
||
|
||
---
|
||
|
||
## Regression tests
|
||
|
||
See [tests/README.md](tests/README.md) for QtTest build and execution commands.
|
||
Tests cover frame validation and error locations, Intel and Motorola extraction,
|
||
signal syntax and payload bounds, `VAL_` syntax and reference validation,
|
||
raw description lookup and preservation through both builders, statement dispatch, signed and unsigned
|
||
64-bit boundaries, physical conversion, numeric output, and invalid signal
|
||
payloads.
|
||
|
||
## Measuring code coverage
|
||
|
||
Coverage instrumentation is separate from the QtTest suite named `CoverageTest`:
|
||
that suite runs extra regression checks, but coverage counters require a specially
|
||
instrumented build. Use a separate build directory for each coverage tool.
|
||
|
||
### GCC, gcov, and LCOV on Debian/Ubuntu
|
||
|
||
Install the build and report tools:
|
||
|
||
```sh
|
||
sudo apt install build-essential qt6-base-dev qt6-base-dev-tools qmake6 lcov
|
||
# Optional IDE:
|
||
sudo apt install qtcreator
|
||
```
|
||
|
||
GCC supplies `gcov`; the `lcov` package supplies `lcov` and `genhtml`. Use the
|
||
`gcov` version matching the GCC used by the selected Qt Creator kit. These
|
||
instructions use GCC and Qt 6.8.2.
|
||
|
||
From the repository root, create a fresh coverage build. `--coverage` must be
|
||
passed to both compilation and linking; Debug avoids optimization that can make
|
||
source-level results harder to interpret. `-fprofile-abs-path` makes source paths
|
||
in the coverage data independent of the compilation working directory.
|
||
|
||
```sh
|
||
DBC_SOURCE_DIR="$(pwd)"
|
||
mkdir -p build-coverage
|
||
cd build-coverage
|
||
|
||
qmake6 "$DBC_SOURCE_DIR/dbc.pro" \
|
||
CONFIG+=dbc_qt CONFIG+=debug CONFIG-=release \
|
||
'QMAKE_CXXFLAGS+=--coverage -fprofile-abs-path' \
|
||
'QMAKE_LFLAGS+=--coverage'
|
||
make -j4
|
||
|
||
# The standalone tree test is not a qmake target; instrument it separately.
|
||
mkdir -p standalone
|
||
c++ -std=c++17 -O0 -g --coverage -fprofile-abs-path \
|
||
-I"$DBC_SOURCE_DIR/core" \
|
||
"$DBC_SOURCE_DIR/tests/tree_node_test.cpp" \
|
||
"$DBC_SOURCE_DIR/core/tree_node.cpp" \
|
||
"$DBC_SOURCE_DIR/core/dbc_tree_builder.cpp" \
|
||
-o standalone/tree_node_tests
|
||
|
||
# Capture a zero-count baseline so unexecuted compiled code remains in the report.
|
||
lcov --capture --initial --directory . --branch-coverage \
|
||
--include "$DBC_SOURCE_DIR/core/*" --include "$DBC_SOURCE_DIR/qt/*" \
|
||
--output-file baseline.info
|
||
|
||
# Reset counts before each new measurement; otherwise executions accumulate.
|
||
lcov --zerocounters --directory .
|
||
make check
|
||
./standalone/tree_node_tests
|
||
|
||
lcov --capture --directory . --branch-coverage \
|
||
--include "$DBC_SOURCE_DIR/core/*" --include "$DBC_SOURCE_DIR/qt/*" \
|
||
--output-file executed.info
|
||
lcov --add-tracefile baseline.info --add-tracefile executed.info \
|
||
--branch-coverage --output-file coverage.info
|
||
|
||
genhtml coverage.info --branch-coverage --demangle-cpp --output-directory html
|
||
lcov --summary coverage.info --branch-coverage
|
||
```
|
||
|
||
Open `build-coverage/html/index.html` in a browser. The report includes the
|
||
library's `core/` and `qt/` sources and instantiated header code; test sources,
|
||
generated moc files, and Qt/STL headers are excluded. Click a filename to inspect
|
||
execution counts, uncovered lines, and branch outcomes. Branch coverage is a
|
||
separate metric: executing a line does not necessarily exercise every outcome.
|
||
C++ branch data can also include compiler-generated exception paths.
|
||
|
||
For another run with unchanged sources, repeat the reset, test execution,
|
||
capture, merge, and HTML generation steps. After changing sources or compiler
|
||
flags, rebuild and regenerate `baseline.info` before measuring again. A fresh
|
||
build directory avoids mixing old profiling data with a different build.
|
||
|
||
An initial measurement with GCC 14.2 and Qt 6.8.2 on 2026-10-07, including both
|
||
QtTest executables and the standalone tree test, yielded 97.8% line coverage
|
||
(724/740) and 70.7% branch coverage (833/1178). These are a snapshot, not fixed
|
||
expected values for later changes or other toolchains.
|
||
|
||
After adding direct comment-handler and ambiguous `VAL_` reference tests, the
|
||
same configuration measured 98.4% line coverage (728/740) and 71.4% branch
|
||
coverage (841/1178).
|
||
|
||
See [GCC instrumentation options](https://gcc.gnu.org/onlinedocs/gcc/Instrumentation-Options.html)
|
||
and the [LCOV project documentation](https://github.com/linux-test-project/lcov).
|
||
|
||
### Configure the GCC coverage build in Qt Creator
|
||
|
||
1. Open `dbc.pro` and select a GCC kit with Qt 6.8.2.
|
||
2. In **Projects > Build Settings**, clone **Debug** as **Coverage**.
|
||
3. Choose a separate build directory, such as `build-coverage`.
|
||
4. Expand the **qmake** build step and add these **Additional arguments**:
|
||
|
||
```text
|
||
CONFIG+=dbc_qt CONFIG+=debug CONFIG-=release "QMAKE_CXXFLAGS+=--coverage -fprofile-abs-path" "QMAKE_LFLAGS+=--coverage"
|
||
```
|
||
|
||
5. Run qmake and build the project. Both `dbc_tests` and `dbc_model_tests` must
|
||
run to include coverage from the core and optional model adapter.
|
||
6. In a terminal in the coverage build directory, compile the standalone test
|
||
and run the LCOV/report commands above. Set `DBC_SOURCE_DIR` to the absolute
|
||
repository path if using a new terminal. `make check` runs both QtTest
|
||
executables; the standalone tree executable must be run separately.
|
||
7. View `html/index.html` in a browser. This workflow produces HTML reports;
|
||
it does not require a licensed coverage toolchain.
|
||
|
||
### Configure all build variants in Qt Creator
|
||
|
||
The local `dbc.pro.user` stores machine-specific kit IDs and build/run settings.
|
||
It is ignored by Git (`*.user`) and should not be committed or copied between
|
||
machines. Recreate the configurations below through the UI when setting up a
|
||
new checkout. The `Qt4ProjectManager` identifiers inside `.user` are historical
|
||
Qt Creator setting names; the project still requires Qt 6.8.2.
|
||
|
||
The local configuration set uses these independent build directories:
|
||
|
||
| Configuration | Build directory example | Purpose |
|
||
| --- | --- | --- |
|
||
| Debug | `/tmp/build/dbc/Desktop-Debug` | Core library and parser/decoder/coverage regression tests |
|
||
| Release | `/tmp/build/dbc/Desktop-Release` | The same targets with release settings |
|
||
| Profile | `/tmp/build/dbc/Desktop-Profile` | Existing Qt Creator profiling configuration; not code coverage |
|
||
| Debug Qt Models | `/tmp/build/dbc/Desktop-Debug-Qt-Models` | Debug targets plus optional adapter and model tests |
|
||
| Release Qt Models | `/tmp/build/dbc/Desktop-Release-Qt-Models` | Release targets plus optional adapter and model tests |
|
||
| Debug Qt Models Strict | `/tmp/build/dbc/Desktop-Debug-Qt-Models-Strict` | Model build with `QT_FATAL_WARNINGS=1` for builds and runs |
|
||
| Coverage | `/tmp/build/dbc/Desktop-Coverage` | Instrumented core/model build, test execution, and LCOV HTML report |
|
||
| Core Only | `/tmp/build/dbc/Desktop-Core-Only` | The Qt-free core static library |
|
||
| Tree Tests | `/tmp/build/dbc/Desktop-Tree-Tests` | Standalone C++17 tree test without Qt |
|
||
| Demo | `/tmp/build/dbc/Desktop-Demo` | Core library and command-line demo |
|
||
|
||
The temporary paths are examples; choose permanent build directories if desired.
|
||
Do not share a build directory between these configurations.
|
||
|
||
To add the ordinary qmake configurations:
|
||
|
||
1. Open `dbc.pro` with a GCC/Qt 6.8.2 kit.
|
||
2. Go to **Projects > Build Settings**. Keep the existing Debug, Release, and
|
||
Profile configurations. Remove or disable a Coco build step if one remains.
|
||
3. Select Debug, press **Clone**, and name the clone **Debug Qt Models**. Set
|
||
its build directory from the table and add `CONFIG+=dbc_qt` to the qmake
|
||
step's **Additional arguments**.
|
||
4. Clone Release as **Release Qt Models** with the same qmake argument and its
|
||
own build directory.
|
||
5. Clone Debug Qt Models as **Debug Qt Models Strict**. In **Build Environment**,
|
||
add `QT_FATAL_WARNINGS=1`. Run configurations should use the build environment.
|
||
6. Clone Debug Qt Models as **Coverage**. In qmake **Additional arguments**, use:
|
||
|
||
```text
|
||
CONFIG+=dbc_qt CONFIG+=debug CONFIG-=release "QMAKE_CXXFLAGS+=--coverage -fprofile-abs-path" "QMAKE_LFLAGS+=--coverage"
|
||
```
|
||
|
||
7. For Coverage, select **Add Build Step > Custom Process Step** after qmake
|
||
and Make, then set:
|
||
|
||
| Field | Value |
|
||
| --- | --- |
|
||
| Command | `/bin/sh` |
|
||
| Arguments | `"%{sourceDir}/tests/coverage_report.sh" "%{sourceDir}" "%{buildDir}"` |
|
||
| Working directory | `%{buildDir}` |
|
||
|
||
This script compiles the standalone tree test, resets counters, runs both
|
||
QtTest executables and the tree test, and generates `%{buildDir}/html/index.html`.
|
||
It stops if any test fails. Therefore building Coverage also executes tests.
|
||
`lcov` and `genhtml` must be installed. Use this configuration with the GCC kit;
|
||
`gcov` and the compiler used for the standalone test must match that kit.
|
||
8. Run qmake and build each new configuration once. The default root builds
|
||
expose `dbc_tests`; model configurations also expose `dbc_model_tests`.
|
||
|
||
Qt Creator expands `%{sourceDir}` and `%{buildDir}` in custom steps to the source
|
||
and active build directories. Use the field's variable chooser if needed.
|
||
|
||
For the three independent builds, clone Debug, assign the name and build
|
||
directory from the table, and replace its qmake/Make build steps with the custom
|
||
process steps below. These steps are local IDE settings and do not change
|
||
`dbc.pro`. Give each step a descriptive name and retain the listed order.
|
||
|
||
For **Core Only**:
|
||
|
||
| Command | Arguments | Working directory |
|
||
| --- | --- | --- |
|
||
| `/usr/bin/mkdir` | `-p "%{buildDir}"` | `%{sourceDir}` |
|
||
| `/usr/bin/qmake6` | `"%{sourceDir}/core/core.pro" CONFIG+=debug CONFIG-=release` | `%{buildDir}` |
|
||
| `/usr/bin/make` | `-j4` | `%{buildDir}` |
|
||
|
||
Keep a clean step with `make clean` in `%{buildDir}`. Core Only creates a static
|
||
library, so there is no executable to run.
|
||
|
||
For **Tree Tests**:
|
||
|
||
1. Add a custom step with command `/usr/bin/mkdir`, arguments `-p "%{buildDir}"`,
|
||
and working directory `%{sourceDir}`.
|
||
2. Add a custom step with command `/usr/bin/c++`, working directory `%{buildDir}`,
|
||
and these arguments:
|
||
|
||
```text
|
||
-std=c++17 -O0 -g -I"%{sourceDir}/core" "%{sourceDir}/tests/tree_node_test.cpp" "%{sourceDir}/core/tree_node.cpp" "%{sourceDir}/core/dbc_tree_builder.cpp" -o "%{buildDir}/tree_node_tests"
|
||
```
|
||
|
||
3. Remove the default Make clean step: this direct compiler build creates no
|
||
Makefile. Run the resulting executable as described below.
|
||
|
||
For **Demo**:
|
||
|
||
| Command | Arguments | Working directory |
|
||
| --- | --- | --- |
|
||
| `/usr/bin/mkdir` | `-p "%{buildDir}/core" "%{buildDir}/examples"` | `%{sourceDir}` |
|
||
| `/usr/bin/qmake6` | `"%{sourceDir}/core/core.pro" CONFIG+=debug CONFIG-=release` | `%{buildDir}/core` |
|
||
| `/usr/bin/make` | `-j4` | `%{buildDir}/core` |
|
||
| `/usr/bin/qmake6` | `"%{sourceDir}/examples/demo.pro" "DBC_CORE_BUILD_DIR=%{buildDir}/core" CONFIG+=debug CONFIG-=release` | `%{buildDir}/examples` |
|
||
| `/usr/bin/make` | `-j4` | `%{buildDir}/examples` |
|
||
|
||
Replace the default clean step with two custom steps: `/usr/bin/make` with
|
||
arguments `clean`, first in `%{buildDir}/examples`, then in `%{buildDir}/core`.
|
||
|
||
To configure execution and switch variants:
|
||
|
||
1. Go to **Projects > Run Settings** and select the appropriate qmake target:
|
||
`dbc_tests` for ordinary builds or `dbc_model_tests` for model builds.
|
||
2. To run all QtTest suites together, add a **Custom Executable** named
|
||
**All QtTest suites**, with executable `/usr/bin/make`, arguments `check`,
|
||
and working directory `%{buildDir}`. Use it with an ordinary or model qmake
|
||
configuration. Coverage already runs every test in its final build step.
|
||
3. For Tree Tests, add a **Custom Executable** named **Standalone tree test**,
|
||
executable `%{buildDir}/tree_node_tests`, working directory `%{buildDir}`,
|
||
and no arguments.
|
||
4. For Demo, add a **Custom Executable** named **Demo**, executable
|
||
`%{buildDir}/examples/dbc_demo`, working directory `%{buildDir}/examples`.
|
||
Set **Command line arguments** to the quoted absolute path of your DBC file.
|
||
5. For Coverage on Linux, optionally add a **Custom Executable** named
|
||
**Coverage HTML report**, executable `/usr/bin/xdg-open`, arguments
|
||
`"%{buildDir}/html/index.html"`, working directory `%{buildDir}`. Running it
|
||
opens the generated report in the default browser.
|
||
6. Use the kit/build/run selector above the Run button to choose the build
|
||
configuration and its corresponding executable. Build with **Ctrl+B**,
|
||
then run with **Ctrl+R**. The Build Settings configuration list can also
|
||
switch the active build.
|
||
7. If `.user` was edited externally, close and reopen the project so Qt Creator
|
||
reads the new settings. Do not edit it while the project is open, because
|
||
Qt Creator can overwrite external changes when saving its session.
|
||
|
||
See Qt Creator's [build configuration guide](https://doc.qt.io/qtcreator/creator-build-settings.html)
|
||
and [run configuration guide](https://doc.qt.io/qtcreator/creator-run-settings.html).
|