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 for the release and development history, tests/README.md for test instructions, and generated API documentation for the API reference.
AGENTS.md contains project instructions for coding agents, including code style, build and test commands, and documentation requirements.
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:
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:
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:
DecodeDatabaseDecodeFrameDecodeSignal
This avoids forcing UI-oriented structures into a decode role they were not meant for.
Module Layout
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:
namestartBitlengthisLittleEndianisSignedfactoroffsetminimummaximumunitreceiverscomment
Notes:
receiversis a list because a signal may have more than one receiver ECUfactorandoffsetdefine physical conversion- this structure is close to DBC content and easy to inspect
Physical value rule:
physical = raw * factor + offset
FrameInfo
Represents one frame as parsed from the DBC file.
Fields:
namecanIdisExtendedpgnhasPgndlctransmittercommentsignals
Notes:
signalsis a list ofSignalInfoisExtendedis determined during CAN ID normalizationpgnis derived for extended frames only with explicit J1939 opt-in (see below)
DbcDatabase
Top-level parsed DBC container.
Conceptually:
DbcDatabase
└── vector<FrameInfo>
This is the central structure produced by DbcParser.
UI Tree Layer
TreeNode
The UI tree contains three node types:
RootFrameSignal
Example hierarchy:
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:
FrameInfoSignalInfo
The tree is independent of Qt and supports browsing through the optional
DbcTreeModel adapter. GetParent() returns a non-owning parent pointer;
GetRow() returns the zero-based sibling position (zero for a detached node or
root). GetChild() returns nullptr outside the child range, and AddChild()
ignores null children. Appending children preserves existing addresses and rows.
Nodes cannot be copied or moved. Transfer their std::unique_ptr ownership to
preserve parent links and addresses. AddChild() accepts an rvalue ownership
handle and rejects self/ancestor insertion and already-attached children with
std::invalid_argument before consuming ownership. There is no detach or
reparent operation; callers must supply exclusive ownership.
GetFrameKey() returns std::optional<FrameKey> for frame nodes and signals
directly attached to a frame. GetSignalKey() returns std::optional<SignalKey>
for those signals. Roots, detached signals, and signals without a frame parent
have no key. Keys use normalized CAN ID plus standard/extended type, and signal
keys also include the signal name. They match runtime definition keys, remain
independent of row order and frame display names, and survive source-database
changes or destruction. Renaming a signal changes its logical identity. Returned
keys are values and may be retained after destroying the tree.
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:
IntelMotorola
This is better for decode code than passing around raw DBC characters.
ValueType
Numeric type enum:
UnsignedSigned
This is clearer than combining multiple boolean flags during runtime logic.
DecodeSignal
Represents one runtime-ready signal definition.
Fields:
namestartBitlengthbyteOrdervalueTypefactoroffsetminimummaximumunitreceiverscomment
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:
namecanIdisExtendeddlcpgnhasPgntransmittercommentsignals
This structure is used directly by the decoder.
FrameKey
Stable identifier for runtime frame matching and application state.
Fields:
canIdisExtended
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 borrowedconst std::vector<DecodeFrame>&.FindFrame(FrameKey)returnsstd::shared_ptr<const DecodeFrame>.FindSignal(SignalKey)returnsstd::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:
canIdisExtendeddata
This same structure can be used for:
- live CAN bus input
- replayed trace records
- unit tests
DecodedSignalValue
Represents one decoded signal result.
Fields:
definitionkeyrawValuephysicalValuevalidstatus
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:
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:
definitionkeysignalsvalidstatus
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 or ambiguous frame returns valid == false,
a null definition, its requested key, and an empty signal list. A successful PGN
match uses the DBC definition key for the frame and its signals.
FrameDecodeStatus summarizes the result:
| Status | Meaning |
|---|---|
UnknownFrame |
No exact CAN ID/type match or eligible PGN match |
AmbiguousFrame |
No exact match and multiple J1939 definitions share the PGN |
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.
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/mNmultiplexing
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.
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 or comment, unresolved comment reference, 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 comment, 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_,
attribute relation forms (BA_REL_, BA_DEF_REL_), 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.
Attributes and frame metadata
BA_DEF_, BA_DEF_DEF_, and BA_ support database, BO_, SG_,
BU_, and EV_ scopes with INT, HEX, FLOAT, STRING, and ENUM
definitions. Integer/HEX values use decimal signed 64-bit numbers; floating
values are finite and locale independent. ENUM assignments use zero-based indices;
defaults accept an index or a quoted label. Strings support escaped quotes and
backslashes. Each statement occupies one physical line and ends with ;.
BA_DEF_ BO_ "VFrameFormat" ENUM "StandardCAN","ExtendedCAN","StandardCAN_FD";
BA_DEF_DEF_ "VFrameFormat" "StandardCAN";
BA_DEF_ BO_ "GenMsgCycleTime" INT 0 10000;
BA_DEF_DEF_ "GenMsgCycleTime" 100;
BA_ "GenMsgCycleTime" BO_ 256 20;
Definitions, defaults, assignments, and frame/signal declarations may appear in
any order. Defaults apply to every object of the defined scope; explicit values
override them, with the last repeated assignment winning. Duplicate definitions
or defaults, undefined names, wrong types/scopes, out-of-range values, and missing
frame/signal targets produce Error / InvalidAttribute at the source line.
DbcDatabase::attributeDefinitions retains types, ranges, enum labels, and defaults;
attributeAssignments retains explicit values and targets. Effective values are
available in attributes on the database, frames and signals. Node/environment
assignments are retained by name without checking declarations; their defaults
remain on the definitions. Frame/signal attributes are copied into runtime
snapshots and tree payloads. Other valid attributes remain available as metadata
without changing decoding.
FrameInfo and DecodeFrame expose optional frameFormat (the enum label) and
cycleTimeMs (GenMsgCycleTime, unsigned 32-bit milliseconds). Missing values
remain unset; zero cycle time means unspecified. StandardCAN and ExtendedCAN
are recognized. Other format labels, including CAN FD and J1939PG, are preserved
with Warning / UnsupportedAttribute when effective on a frame. A supported
format conflicting with the encoded BO_ ID also warns. Warnings identify the
assignment or default line. Attributes do not change CAN ID normalization or
automatically enable CAN FD or J1939 decoding; J1939 still requires parser opt-in.
Use ParseFileWithDiagnostics() to review these 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. Frame and signal comments may span
physical lines and resolve after definitions are read; other supported statements
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:
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:
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:
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. Conversion is independent of the application's C and
C++ locales: the decimal separator is always a dot (.), and comma decimals
and digit grouping are rejected. The entire numeric token must be valid; hexadecimal
numbers, NaN, infinity, conversion overflow, and conversion underflow (including
subnormal values) 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
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:
CM_ BO_ 256 "Engine data frame";
Signal comment example:
CM_ SG_ 256 EngineSpeed "Actual engine speed";
Stored in:
FrameInfo::commentSignalInfo::comment
Comments accept spaces and tabs between fields, quotes and backslashes escaped
with a preceding backslash, and text spanning multiple physical lines. Newlines, blank
lines, and indentation inside quoted text are preserved; CRLF becomes LF.
Comments may appear before their frame or signal definitions. References use the
same decimal CAN ID validation and normalization as BO_, including reserved-bit
and overflow checks and distinct standard/extended targets. Repeated comments
apply in statement order, so the last comment wins, including empty text.
Malformed supported comments (including invalid escapes, missing quotes or
semicolon, and trailing text) and missing frame/signal references produce an
InvalidStatement error at the comment's starting line. Failed parses return an
empty database; ParseFile throws with the same source location.
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
isExtendedflag
This is important both for correct lookup and for future interoperability with live CAN APIs.
J1939 Opt-in and PGN Lookup
Extended CAN identifiers do not imply J1939. The default parser leaves hasPgn
false and pgn zero, and decoding uses exact normalized CAN ID and frame type.
Enable J1939 interpretation explicitly for a file:
DbcParserOptions options;
options.enableJ1939 = true;
const DbcDatabase parsed = DbcParser(options).ParseFile("vehicle.dbc");
const DecodeDatabase runtime = DbcDecodeBuilder().Build(parsed);
Both parser entry points honor this option. Standard frames remain ordinary CAN.
For mixed-protocol files, parse with the default options, then set hasPgn = true
and pgn = ExtractJ1939Pgn(frame.canId) (from j1939.h) only on known J1939
extended definitions before building the runtime snapshot. The same explicit
metadata enables J1939 for manually constructed DecodeFrame definitions.
Runtime construction rejects hasPgn on standard frames or an inconsistent PGN.
No DBC attribute automatically enables J1939 interpretation.
PGN extraction retains both data-page bits (CAN ID bits 25–24) and PF:
- PDU1 (
PF < 240): PS is the destination address and is zeroed in the PGN. - PDU2 (
PF >= 240): PS is the group extension and remains part of the PGN. - Priority and source address are excluded in both cases. This follows the Linux J1939 PGN layout.
DecodeDatabase::FindFrame(FrameKey) stays an exact lookup.
FindFrameByPgn(pgn) returns a PgnLookupResult with an owning definition
handle for one eligible definition. No match returns a null handle with
ambiguous == false; multiple definitions return a null handle with
ambiguous == true. Copies and returned handles retain the immutable snapshot.
DbcDecoder::FindFrame and both Decode overloads first try the exact ID/type.
Only when that fails for a valid normalized extended ID do they try PGN lookup
among definitions with hasPgn == true. A unique PGN can therefore match a
changed source address or priority, and for PDU1 a changed destination address.
A changed PDU2 group extension does not match. Exact matches win even when the
PGN is ambiguous, including exact matches to ordinary extended CAN definitions.
Multiple PGN candidates are never resolved by order, source, or payload:
FindFrame returns null and Decode returns AmbiguousFrame without decoding
signals or validating the selection. Missing candidates return UnknownFrame.
On success, decoded frame/signal keys belong to the matched DBC definition;
selected-signal decoding also expects those definition keys. Keep the input
RawCanFrame if the received address/ID is needed separately. This feature does
not implement transport-protocol reassembly, address claiming, or full J1939
validation.
Decode Flow
Typical runtime decode flow:
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:
#include "dbc_parser.h"
#include "dbc_tree_builder.h"
#include "dbc_decode_builder.h"
#include "dbc_decoder.h"
Parse DBC
DbcParser parser;
DbcDatabase database = parser.ParseFile("example.dbc");
Build UI tree
DbcTreeBuilder treeBuilder;
std::unique_ptr<TreeNode> root = treeBuilder.Build(database);
Build runtime decode database
DbcDecodeBuilder decodeBuilder;
DecodeDatabase decodeDatabase = decodeBuilder.Build(database);
Decode a raw frame
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
live CAN input
↓
RawCanFrame
↓
DbcDecoder
↓
decoded signal values
trace path
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:
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_→DbcFrameHandlerSG_→DbcSignalHandlerCM_→DbcCommentHandlerVAL_→DbcValueDescriptionHandlerSG_MUL_VAL_→DbcUnsupportedMultiplexingHandlerVERSION,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:
void Handle(const std::string &line, DbcParseContext &context) const override;
Register it in the dispatcher constructor:
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:
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, escape a quote or a backslash with a preceding 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:
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 - attribute relation forms (
BA_REL_,BA_DEF_REL_) 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 frame and signal comments; references resolve after definitions are read
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 (implemented for definitions, defaults and values)
- 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:
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:
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:
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.
#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:
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:
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:
doxygen Doxyfile
Open docs/html/index.html to view the generated reference.
Regression tests
See 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.
dbc_pipeline_test.cpp follows temporary DBC fixtures through parsed metadata,
runtime frame/signal lookups, decoded raw/physical values and descriptions, and
tree nodes. It verifies all supported metadata in both builders, standard and
extended frames sharing a numeric ID, empty databases, comments, and replacement
with another fixture or an empty database.
Code review and additional checks
Attribute numeric parsing shares one locale-independent converter for integer and
floating-point values. Default application skips unrelated scopes, CAN ID
normalization uses the validated identifier mask, and signal layout validation
owns the length checks. Statement handlers continue to use TypeFactory.
Core, adapter, example, and test C++ sources use explicit return types and contain
no lambdas. Public metadata and parser-state comments use Doxygen descriptions.
See tests/README.md for attribute boundary regressions, sanitizer commands, and the latest validation results.
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:
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.
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 and the LCOV project documentation.
Configure the GCC coverage build in Qt Creator
-
Open
dbc.proand select a GCC kit with Qt 6.8.2. -
In Projects > Build Settings, clone Debug as Coverage.
-
Choose a separate build directory, such as
build-coverage. -
Expand the qmake build step and add these Additional arguments:
CONFIG+=dbc_qt CONFIG+=debug CONFIG-=release "QMAKE_CXXFLAGS+=--coverage -fprofile-abs-path" "QMAKE_LFLAGS+=--coverage" -
Run qmake and build the project. Both
dbc_testsanddbc_model_testsmust run to include coverage from the core and optional model adapter. -
In a terminal in the coverage build directory, compile the standalone test and run the LCOV/report commands above. Set
DBC_SOURCE_DIRto the absolute repository path if using a new terminal.make checkruns both QtTest executables; the standalone tree executable must be run separately. -
View
html/index.htmlin 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:
-
Open
dbc.prowith a GCC/Qt 6.8.2 kit. -
Go to Projects > Build Settings. Keep the existing Debug, Release, and Profile configurations. Remove or disable a Coco build step if one remains.
-
Select Debug, press Clone, and name the clone Debug Qt Models. Set its build directory from the table and add
CONFIG+=dbc_qtto the qmake step's Additional arguments. -
Clone Release as Release Qt Models with the same qmake argument and its own build directory.
-
Clone Debug Qt Models as Debug Qt Models Strict. In Build Environment, add
QT_FATAL_WARNINGS=1. Run configurations should use the build environment. -
Clone Debug Qt Models as Coverage. In qmake Additional arguments, use:
CONFIG+=dbc_qt CONFIG+=debug CONFIG-=release "QMAKE_CXXFLAGS+=--coverage -fprofile-abs-path" "QMAKE_LFLAGS+=--coverage" -
For Coverage, select Add Build Step > Custom Process Step after qmake and Make, then set:
Field Value Command /bin/shArguments "%{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.lcovandgenhtmlmust be installed. Use this configuration with the GCC kit;gcovand the compiler used for the standalone test must match that kit. -
Run qmake and build each new configuration once. The default root builds expose
dbc_tests; model configurations also exposedbc_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:
-
Add a custom step with command
/usr/bin/mkdir, arguments-p "%{buildDir}", and working directory%{sourceDir}. -
Add a custom step with command
/usr/bin/c++, working directory%{buildDir}, and these arguments:-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" -
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:
- Go to Projects > Run Settings and select the appropriate qmake target:
dbc_testsfor ordinary builds ordbc_model_testsfor model builds. - To run all QtTest suites together, add a Custom Executable named
All QtTest suites, with executable
/usr/bin/make, argumentscheck, and working directory%{buildDir}. Use it with an ordinary or model qmake configuration. Coverage already runs every test in its final build step. - For Tree Tests, add a Custom Executable named Standalone tree test,
executable
%{buildDir}/tree_node_tests, working directory%{buildDir}, and no arguments. - 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. - 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. - 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.
- If
.userwas 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 and run configuration guide.