|
DBC Framework
DBC parsing and CAN signal decoding framework
|
This module provides a minimal but extensible DBC parser and runtime decode foundation for the FrameTap project.
Its purpose is to:
The implementation follows a simple and practical design:
This is not a full production-grade DBC implementation yet, but it is a strong architectural base.
The module is divided into two main paths.
Used for:
Model/ViewPipeline:
Used for:
Pipeline:
This separation is intentional.
The tree is useful for UI, but it is not the main data structure for runtime decoding.
A tree structure is convenient for browsing, but a runtime decoder needs something different:
Because of that, the design uses a dedicated runtime-ready structure:
This avoids forcing UI-oriented structures into a decode role they were not meant for.
Files:
These store a readable representation of the parsed DBC file.
Files:
These convert parsed DBC content into a tree hierarchy suitable for UI and model/view usage later.
Files:
These convert parsed DBC content into a structure optimized for decoding.
Files:
These perform actual decoding of raw CAN frames using DecodeDatabase.
Files:
The parser reads files and reports errors; registered statement handlers parse DBC constructs.
File:
Used as a small integration example.
Represents one signal as parsed from the DBC file.
Fields:
namestartBitlengthisLittleEndianisSignedfactoroffsetminimummaximumunitreceiverscommentNotes:
receivers is a list because a signal may have more than one receiver ECUfactor and offset define physical conversionPhysical value rule:
Represents one frame as parsed from the DBC file.
Fields:
namecanIdisExtendedpgnhasPgndlctransmittercommentsignalsNotes:
signals is a list of SignalInfoisExtended is determined during CAN ID normalizationpgn is derived using simplified J1939 logic when applicableTop-level parsed DBC container.
Conceptually:
This is the central structure produced by DbcParser.
The UI tree contains three node types:
RootFrameSignalExample hierarchy:
Each node stores either:
The tree is intended for browsing and later Qt model integration.
It is not the primary runtime decode structure.
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.
Runtime byte order enum:
IntelMotorolaThis is better for decode code than passing around raw DBC characters.
Numeric type enum:
UnsignedSignedThis is clearer than combining multiple boolean flags during runtime logic.
Represents one runtime-ready signal definition.
Fields:
namestartBitlengthbyteOrdervalueTypefactoroffsetminimummaximumunitreceiverscommentThis structure contains all information required for extracting and converting a signal value from raw frame data.
Represents one runtime-ready frame definition.
Fields:
namecanIdisExtendeddlcpgnhasPgntransmittercommentsignalsThis structure is used directly by the decoder.
Fast lookup key for runtime frame matching.
Fields:
canIdisExtendedThis matters because the same numeric identifier must not be confused between standard and extended frames.
Top-level runtime decode container.
Fields:
framesframeIndexByKeyConceptually:
This gives the decoder fast access to a frame definition using CAN ID and frame type.
Represents a raw CAN frame to decode.
Fields:
canIdisExtendeddataThis same structure can be used for:
Represents one decoded signal result.
Fields:
definitionrawValuephysicalValuevalidDecodedSignalValue::rawValue is a C++17 std::variant<std::int64_t, std::uint64_t>. For a valid signal, signed definitions produce the int64_t alternative and unsigned definitions produce the uint64_t alternative. Raw values remain exact across their full 64-bit ranges. Check valid before using the value; an invalid result's default value is not decoded data.
Use std::get when the numeric type is known, or std::visit to handle both:
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.
Represents one decoded frame result.
Fields:
definitionsignalsvalidThis is the decoder output for one raw frame.
Main runtime decoder class.
Responsibilities:
Main methods:
FindFrame(...)Decode(...)The current parser supports the following DBC elements:
BO_SG_CM_ BO_CM_ SG_Example:
Parsed fields:
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:
This change validates frame definitions. Signal definitions are also validated as described below; comprehensive DBC grammar coverage is still planned. File-open errors contain the path but have no line number.
Example:
Parsed fields:
SG_ statements are matched completely. Signal names must be nonempty and contain no whitespace. Spaces and tabs are accepted between syntax elements; byte order and signedness remain adjacent (@0+, @0-, @1+, or @1-). A quoted unit (which may be empty) and a nonempty comma-separated receiver list are required. Unexpected trailing fields are rejected.
Start bits must be unsigned decimal integers fitting uint32_t; lengths must be within 1..64 bits. Signs, fractional values, numeric suffixes, and overflow are rejected for these fields. Every signal bit must fit within the frame's declared payload length, using Intel increasing bit positions or Motorola sawtooth traversal. This uses the stored payload length without introducing an additional CAN or CAN FD payload limit.
Factor, offset, minimum, and maximum accept signed decimal numbers, fractions, and scientific notation. The entire numeric token must be valid; hexadecimal numbers, NaN, infinity, conversion overflow, and conversion underflow are rejected. Minimum must not exceed maximum. Zero and negative factors are allowed.
Multiplexing markers (M, mN, and combinations) are currently rejected rather than interpreted as part of a signal name. Multiplexing support and duplicate frame/signal detection remain separate work.
Invalid signal definitions stop parsing with path:line: description, including when the signal exceeds the declared payload length.
Frame comment example:
Signal comment example:
Stored in:
The parser normalizes frame identifiers.
Common DBC behavior:
The parser therefore stores:
canIdisExtended flagThis is important both for correct lookup and for future interoperability with live CAN APIs.
PGN is derived only when the frame is treated as extended.
The current logic is simplified J1939 extraction:
pfpsdpThis is enough for a practical start but should not be treated as full J1939 validation.
Typical runtime decode flow:
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.
A key design goal is that the same decoder should work for both:
That means this architecture supports:
This avoids duplicating decode logic in two separate parts of the application.
This module is meant to support at least the following FrameTap workflows:
Example combined workflow:
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:
That is why DecodeDatabase is a separate layer.
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_ → DbcCommentHandlerGrammar implementations live in dbc_statement_handler.cpp; their interface, context, and dispatcher live in dbc_statement_handler.h. typefactory.h provides the generic registry and object creation.
To add a construct, implement a default-constructible handler deriving from DbcStatementHandler with:
Register it in the dispatcher constructor:
Registration creates one handler; replacing a registration replaces that instance. Register during initialization before dispatching. A line is routed using its full first whitespace-delimited keyword, so BO_TX_BU_ cannot match BO_. Unregistered keywords remain ignored. SG_ accepts spaces and tabs after its keyword; CM_ retains its existing policy requiring a literal space.
This factory handles statements inside DBC files. A separate format-level factory can be introduced when another file format is implemented. The decoder and database builders remain independent of statement dispatch.
This is still a minimal implementation.
Not supported yet:
VAL_ tablesBA_ attributesBA_DEF_ definitionsMotorola extraction is implemented, but it should still be verified carefully against real-world DBC files and expected values.
A practical development order would be:
BO_SG_CM_isExtendedRecommended additions:
TreeNodeVAL_ support for enum-style signalsRecommended additions:
Advanced functionality:
The module does not depend on any specific build system.
It can be integrated with:
Just add the source files to the project.
This module is now split into two intentionally separate layers:
Used for:
Used for:
That separation is the main architectural improvement.
In short, the system now looks like this:
This gives FrameTap a much better foundation for real use, because both browsing and decoding are supported without forcing one representation to do the other's job.
See tests/README.md for compiler and qmake commands. Tests cover frame validation and error locations, Intel and Motorola extraction, signed and unsigned 64-bit boundaries, physical conversion, numeric output, and invalid signal payloads.