Compare commits
2
Commits
v1.0
...
69074e3321
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69074e3321 | ||
|
|
2c057b631d |
No files matched your search
@@ -414,6 +414,34 @@ Fields:
|
||||
|
||||
---
|
||||
|
||||
### Raw value representation and precision
|
||||
|
||||
`DecodedSignalValue::rawValue` is a C++17
|
||||
`std::variant<std::int64_t, std::uint64_t>`. For a valid signal, signed definitions
|
||||
produce the `int64_t` alternative and unsigned definitions produce the `uint64_t`
|
||||
alternative. Raw values remain exact across their full 64-bit ranges. Check
|
||||
`valid` before using the value; an invalid result's default value is not decoded data.
|
||||
|
||||
Use `std::get` when the numeric type is known, or `std::visit` to handle both:
|
||||
|
||||
```cpp
|
||||
std::visit([] (auto raw) { std::cout << raw; }, decodedSignal.rawValue);
|
||||
```
|
||||
|
||||
This replaces the previous `int64_t` field: consumers that read or print
|
||||
`rawValue` directly must handle the variant.
|
||||
|
||||
Physical conversion uses the active numeric type:
|
||||
`physicalValue = static_cast<double>(raw) * factor + offset`.
|
||||
`physicalValue` remains a `double` and is approximate for large integers.
|
||||
On usual IEEE 754 binary64 implementations, integers are exactly representable
|
||||
through 2^53; above that, some integers round to the same floating-point value.
|
||||
For example, unsigned `UINT64_MAX` remains exact in `rawValue`, but converts to
|
||||
approximately 18446744073709551616 in binary64 before scaling.
|
||||
Use `rawValue` whenever exact integer values are required.
|
||||
|
||||
---
|
||||
|
||||
## `DecodedFrameValue`
|
||||
|
||||
Represents one decoded frame result.
|
||||
@@ -839,3 +867,9 @@ DBC parser → DbcDatabase → DecodeDatabase → DbcDecoder
|
||||
```
|
||||
|
||||
This gives FrameTap a much better foundation for real use, because both browsing and decoding are supported without forcing one representation to do the other's job.
|
||||
|
||||
## Regression tests
|
||||
|
||||
See [tests/README.md](tests/README.md) for compiler and qmake commands.
|
||||
Tests cover Intel and Motorola extraction, signed and unsigned 64-bit boundaries,
|
||||
physical conversion, numeric output, and invalid signal payloads.
|
||||
+19
-21
@@ -55,10 +55,12 @@ DecodedFrameValue DbcDecoder::Decode (const DecodeDatabase &database,
|
||||
if (signal.valueType == ValueType::Signed)
|
||||
decoded.rawValue = SignExtend (unsignedValue, signal.length);
|
||||
else
|
||||
decoded.rawValue = static_cast<std::int64_t> (unsignedValue);
|
||||
decoded.rawValue = unsignedValue;
|
||||
|
||||
decoded.physicalValue =
|
||||
(static_cast<double> (decoded.rawValue) * signal.factor) + signal.offset;
|
||||
std::visit ([&signal] (auto rawValue) {
|
||||
return (static_cast<double> (rawValue) * signal.factor) + signal.offset;
|
||||
}, decoded.rawValue);
|
||||
decoded.valid = true;
|
||||
|
||||
result.signals.push_back (decoded);
|
||||
@@ -111,47 +113,43 @@ bool DbcDecoder::ExtractMotorola (const std::vector<std::uint8_t> &data,
|
||||
* - startBit points to the most significant bit of the signal
|
||||
* - inside a byte, bit numbering goes 7..0
|
||||
* - crossing byte boundary moves to the next byte, bit 7
|
||||
* Example: a signal starting at 7 follows 7..0, 15..8, 23..16.
|
||||
*/
|
||||
|
||||
value = 0U;
|
||||
|
||||
std::int32_t currentBit = static_cast<std::int32_t> (startBit);
|
||||
std::size_t byteIndex = startBit / 8U;
|
||||
std::uint32_t bitInByte = startBit % 8U;
|
||||
|
||||
for (std::uint32_t bitIndex = 0U; bitIndex < length; ++bitIndex) {
|
||||
if (currentBit < 0)
|
||||
return false;
|
||||
|
||||
const std::uint32_t absoluteBit = static_cast<std::uint32_t> (currentBit);
|
||||
const std::uint32_t byteIndex = absoluteBit / 8U;
|
||||
const std::uint32_t bitFromMsb = absoluteBit % 8U;
|
||||
const std::uint32_t bitInByte = 7U - bitFromMsb;
|
||||
|
||||
if (byteIndex >= data.size())
|
||||
return false;
|
||||
|
||||
value <<= 1U;
|
||||
value |= static_cast<std::uint64_t> ((data[byteIndex] >> bitInByte) & 0x01U);
|
||||
|
||||
if ((absoluteBit % 8U) == 7U)
|
||||
currentBit = static_cast<std::int32_t> ((byteIndex + 1U) * 8U);
|
||||
else
|
||||
--currentBit;
|
||||
if (bitInByte == 0U) {
|
||||
++byteIndex;
|
||||
bitInByte = 7U;
|
||||
} else
|
||||
--bitInByte;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
std::int64_t DbcDecoder::SignExtend (std::uint64_t value, std::uint32_t bitLength) {
|
||||
if ((bitLength == 0U) || (bitLength >= 64U))
|
||||
return static_cast<std::int64_t> (value);
|
||||
|
||||
const std::uint64_t signMask = (static_cast<std::uint64_t> (1U) << (bitLength - 1U));
|
||||
const std::uint64_t valueMask = (static_cast<std::uint64_t> (1U) << bitLength) - 1U;
|
||||
if ((bitLength == 0U) || (bitLength > 64U))
|
||||
return 0;
|
||||
|
||||
const std::uint64_t signMask = std::uint64_t (1U) << (bitLength - 1U);
|
||||
const std::uint64_t valueMask = (bitLength == 64U)
|
||||
? ~std::uint64_t (0U) : (std::uint64_t (1U) << bitLength) - 1U;
|
||||
value &= valueMask;
|
||||
|
||||
if ((value & signMask) == 0U)
|
||||
return static_cast<std::int64_t> (value);
|
||||
|
||||
return static_cast<std::int64_t> (value | (~valueMask));
|
||||
// The complemented magnitude fits int64_t, including for INT64_MIN.
|
||||
return -1 - static_cast<std::int64_t> ((~value) & valueMask);
|
||||
}
|
||||
+3
-2
@@ -14,6 +14,7 @@
|
||||
|
||||
#include <vector>
|
||||
#include <cstdint>
|
||||
#include <variant>
|
||||
|
||||
#include "decode_database.h"
|
||||
|
||||
@@ -37,13 +38,13 @@ struct RawCanFrame {
|
||||
*/
|
||||
struct DecodedSignalValue {
|
||||
const DecodeSignal *definition; /**< Signal definition. */
|
||||
std::int64_t rawValue; /**< Extracted raw integer value. */
|
||||
std::variant<std::int64_t, std::uint64_t> rawValue; /**< Exact signed or unsigned raw value. */
|
||||
double physicalValue; /**< Converted physical value. */
|
||||
bool valid; /**< true if decoding succeeded. */
|
||||
|
||||
DecodedSignalValue()
|
||||
: definition (nullptr)
|
||||
, rawValue (0)
|
||||
, rawValue (std::uint64_t (0U))
|
||||
, physicalValue (0.0)
|
||||
, valid (false) {
|
||||
}
|
||||
|
||||
@@ -87,8 +87,9 @@ static void PrintDecodedFrame (const DecodedFrameValue &decoded) {
|
||||
continue;
|
||||
|
||||
std::cout << " " << signal.definition->name
|
||||
<< " raw=" << signal.rawValue
|
||||
<< " physical=" << signal.physicalValue
|
||||
<< " raw=";
|
||||
std::visit ([] (auto rawValue) { std::cout << rawValue; }, signal.rawValue);
|
||||
std::cout << " physical=" << signal.physicalValue
|
||||
<< " " << signal.definition->unit
|
||||
<< "\n";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Decoder regression tests
|
||||
|
||||
Run from the repository root with a C++17 compiler:
|
||||
|
||||
```sh
|
||||
g++ -std=c++17 -Wall -Wextra -Wpedantic -I. \
|
||||
tests/dbc_decoder_test.cpp dbc_decoder.cpp -o /tmp/dbc_decoder_test
|
||||
/tmp/dbc_decoder_test
|
||||
```
|
||||
|
||||
Alternatively, build the standalone qmake project out of source:
|
||||
|
||||
```sh
|
||||
mkdir -p /tmp/dbc_decoder_test_build
|
||||
cd /tmp/dbc_decoder_test_build
|
||||
qmake /absolute/path/to/dbc/tests/dbc_decoder_test.pro
|
||||
make
|
||||
./dbc_decoder_test
|
||||
```
|
||||
|
||||
The executable returns a nonzero exit code if any case fails. Checks remain
|
||||
active in release builds and do not depend on `assert` or an external test framework.
|
||||
|
||||
Coverage includes Motorola bit numbering within and across bytes, unaligned
|
||||
signals, signed values, scaling, 64-bit extraction, signed/unsigned boundary values
|
||||
(including `INT64_MIN`, `INT64_MAX`, and `UINT64_MAX`), numeric output, invalid lengths, insufficient
|
||||
payloads, and Intel regression cases. Frame validity means the definition was
|
||||
found; insufficient payloads are checked through signal validity.
|
||||
@@ -0,0 +1,154 @@
|
||||
#include "dbc_decoder.h"
|
||||
|
||||
#include <iostream>
|
||||
#include <limits>
|
||||
#include <sstream>
|
||||
|
||||
struct DecodeCase {
|
||||
const char *name;
|
||||
ByteOrder byteOrder;
|
||||
std::uint32_t startBit;
|
||||
std::uint32_t length;
|
||||
ValueType valueType;
|
||||
std::vector<std::uint8_t> data;
|
||||
bool valid;
|
||||
std::int64_t rawValue;
|
||||
double factor;
|
||||
double offset;
|
||||
double physicalValue;
|
||||
};
|
||||
|
||||
int main() {
|
||||
const DecodeCase cases[] = {
|
||||
{"Motorola reproduction", ByteOrder::Motorola, 7U, 16U, ValueType::Unsigned,
|
||||
{0x12, 0x34}, true, 0x1234, 1.0, 0.0, 4660.0},
|
||||
{"Motorola within byte", ByteOrder::Motorola, 5U, 4U, ValueType::Unsigned,
|
||||
{0xAC}, true, 11, 1.0, 0.0, 11.0},
|
||||
{"Motorola unaligned crossing", ByteOrder::Motorola, 3U, 12U, ValueType::Unsigned,
|
||||
{0xAB, 0xCD}, true, 0xBCD, 1.0, 0.0, 3021.0},
|
||||
{"Motorola starts at bit zero", ByteOrder::Motorola, 0U, 2U, ValueType::Unsigned,
|
||||
{0x01, 0x80}, true, 3, 1.0, 0.0, 3.0},
|
||||
{"Motorola second byte", ByteOrder::Motorola, 15U, 16U, ValueType::Unsigned,
|
||||
{0xFF, 0x12, 0x34}, true, 0x1234, 1.0, 0.0, 4660.0},
|
||||
{"Motorola signed scaled", ByteOrder::Motorola, 7U, 16U, ValueType::Signed,
|
||||
{0xFF, 0xFE}, true, -2, 0.5, 10.0, 9.0},
|
||||
{"Motorola 64 bits", ByteOrder::Motorola, 7U, 64U, ValueType::Unsigned,
|
||||
{0, 0, 0, 0, 0, 0, 0x12, 0x34}, true, 0x1234, 1.0, 0.0, 4660.0},
|
||||
{"Motorola 64 bits high bit", ByteOrder::Motorola, 7U, 64U, ValueType::Unsigned,
|
||||
{0x40, 0, 0, 0, 0, 0, 0, 0}, true, 0x4000000000000000LL,
|
||||
1.0, 0.0, 4611686018427387904.0},
|
||||
{"Motorola short payload", ByteOrder::Motorola, 7U, 16U, ValueType::Unsigned,
|
||||
{0x12}, false, 0, 1.0, 0.0, 0.0},
|
||||
{"Motorola empty payload", ByteOrder::Motorola, 7U, 8U, ValueType::Unsigned,
|
||||
{}, false, 0, 1.0, 0.0, 0.0},
|
||||
{"Motorola large start", ByteOrder::Motorola,
|
||||
std::numeric_limits<std::uint32_t>::max(), 8U, ValueType::Unsigned,
|
||||
{0xFF}, false, 0, 1.0, 0.0, 0.0},
|
||||
{"Motorola zero length", ByteOrder::Motorola, 7U, 0U, ValueType::Unsigned,
|
||||
{0xFF}, false, 0, 1.0, 0.0, 0.0},
|
||||
{"Motorola excessive length", ByteOrder::Motorola, 7U, 65U, ValueType::Unsigned,
|
||||
std::vector<std::uint8_t> (9U, 0xFF), false, 0, 1.0, 0.0, 0.0},
|
||||
{"Intel regression", ByteOrder::Intel, 0U, 16U, ValueType::Unsigned,
|
||||
{0x12, 0x34}, true, 0x3412, 1.0, 0.0, 13330.0},
|
||||
{"Intel signed regression", ByteOrder::Intel, 0U, 16U, ValueType::Signed,
|
||||
{0xFE, 0xFF}, true, -2, 0.5, 10.0, 9.0},
|
||||
{"Intel short payload", ByteOrder::Intel, 0U, 16U, ValueType::Unsigned,
|
||||
{0x12}, false, 0, 1.0, 0.0, 0.0}
|
||||
};
|
||||
|
||||
int failures = 0;
|
||||
for (const DecodeCase &test : cases) {
|
||||
DecodeSignal signal;
|
||||
signal.name = test.name;
|
||||
signal.byteOrder = test.byteOrder;
|
||||
signal.startBit = test.startBit;
|
||||
signal.length = test.length;
|
||||
signal.valueType = test.valueType;
|
||||
signal.factor = test.factor;
|
||||
signal.offset = test.offset;
|
||||
|
||||
DecodeFrame definition;
|
||||
definition.canId = 0x123U;
|
||||
definition.dlc = static_cast<std::uint8_t> (test.data.size());
|
||||
definition.signals.push_back (signal);
|
||||
DecodeDatabase database;
|
||||
database.frames.push_back (definition);
|
||||
database.frameIndexByKey.emplace (FrameKey (definition.canId, false), 0U);
|
||||
|
||||
RawCanFrame frame;
|
||||
frame.canId = definition.canId;
|
||||
frame.data = test.data;
|
||||
const DecodedFrameValue result = DbcDecoder().Decode (database, frame);
|
||||
const bool passed = result.valid &&
|
||||
(result.definition == &database.frames[0]) &&
|
||||
(result.signals.size() == 1U) &&
|
||||
(result.signals[0].definition == &database.frames[0].signals[0]) &&
|
||||
(result.signals[0].valid == test.valid) &&
|
||||
(!test.valid || (((test.valueType == ValueType::Signed)
|
||||
? (std::get_if<std::int64_t> (&result.signals[0].rawValue) &&
|
||||
std::get<std::int64_t> (result.signals[0].rawValue) == test.rawValue)
|
||||
: (std::get_if<std::uint64_t> (&result.signals[0].rawValue) &&
|
||||
std::get<std::uint64_t> (result.signals[0].rawValue) ==
|
||||
static_cast<std::uint64_t> (test.rawValue))) &&
|
||||
(result.signals[0].physicalValue == test.physicalValue)));
|
||||
if (!passed) {
|
||||
std::cerr << "FAIL: " << test.name << '\n';
|
||||
++failures;
|
||||
}
|
||||
}
|
||||
std::size_t boundaryCases = 0U;
|
||||
for (ByteOrder order : {ByteOrder::Intel, ByteOrder::Motorola}) {
|
||||
for (ValueType type : {ValueType::Unsigned, ValueType::Signed}) {
|
||||
for (std::uint64_t bits : {std::uint64_t (0U), std::uint64_t (0x7FFFFFFFFFFFFFFFULL),
|
||||
std::uint64_t (0x8000000000000000ULL),
|
||||
std::uint64_t (0x8000000000000001ULL),
|
||||
std::uint64_t (0xFFFFFFFFFFFFFFFFULL)}) {
|
||||
DecodeDatabase database;
|
||||
DecodeFrame definition;
|
||||
DecodeSignal signal;
|
||||
signal.startBit = order == ByteOrder::Intel ? 0U : 7U;
|
||||
signal.length = 64U;
|
||||
signal.byteOrder = order;
|
||||
signal.valueType = type;
|
||||
signal.factor = 0.5;
|
||||
signal.offset = 10.0;
|
||||
definition.signals.push_back (signal);
|
||||
database.frames.push_back (definition);
|
||||
database.frameIndexByKey.emplace (FrameKey (0U, false), 0U);
|
||||
RawCanFrame frame;
|
||||
for (unsigned byte = 0U; byte < 8U; ++byte) {
|
||||
const unsigned shift = (order == ByteOrder::Intel ? byte : 7U - byte) * 8U;
|
||||
frame.data.push_back (static_cast<std::uint8_t> (bits >> shift));
|
||||
}
|
||||
const auto result = DbcDecoder().Decode (database, frame);
|
||||
const std::int64_t signedExpected = bits <= 0x7FFFFFFFFFFFFFFFULL
|
||||
? static_cast<std::int64_t> (bits)
|
||||
: std::numeric_limits<std::int64_t>::min() +
|
||||
static_cast<std::int64_t> (bits - 0x8000000000000000ULL);
|
||||
const auto &decoded = result.signals.at (0U);
|
||||
const auto expected = type == ValueType::Unsigned
|
||||
? std::variant<std::int64_t, std::uint64_t> (bits)
|
||||
: std::variant<std::int64_t, std::uint64_t> (signedExpected);
|
||||
const double physical = (type == ValueType::Unsigned
|
||||
? static_cast<double> (bits) : static_cast<double> (signedExpected)) * 0.5 + 10.0;
|
||||
std::ostringstream actualText;
|
||||
std::ostringstream expectedText;
|
||||
std::visit ([&actualText] (auto raw) { actualText << raw; }, decoded.rawValue);
|
||||
if (type == ValueType::Unsigned)
|
||||
expectedText << bits;
|
||||
else
|
||||
expectedText << signedExpected;
|
||||
if (!decoded.valid || decoded.rawValue != expected ||
|
||||
decoded.physicalValue != physical || actualText.str() != expectedText.str()) {
|
||||
std::cerr << "FAIL: 64-bit boundary " << bits << '\n';
|
||||
++failures;
|
||||
}
|
||||
++boundaryCases;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (failures != 0)
|
||||
return 1;
|
||||
std::cout << "All " << sizeof (cases) / sizeof (cases[0]) + boundaryCases << " decoder cases passed.\n";
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
TEMPLATE = app
|
||||
TARGET = dbc_decoder_test
|
||||
CONFIG += console c++17
|
||||
CONFIG -= app_bundle qt
|
||||
INCLUDEPATH += ..
|
||||
SOURCES += dbc_decoder_test.cpp ../dbc_decoder.cpp
|
||||
HEADERS += ../dbc_decoder.h ../decode_database.h
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
// Copyright (c) 2026 Dim Himro
|
||||
|
||||
#ifndef TYPEFACTORY_H
|
||||
#define TYPEFACTORY_H
|
||||
|
||||
#include <memory>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <utility> // std::move
|
||||
|
||||
/**
|
||||
* @brief Registry-based factory for creating objects by runtime identifier.
|
||||
*
|
||||
* Allows registering derived types and instantiating them using a ClassId key.
|
||||
*
|
||||
* Notes:
|
||||
* - Not thread-safe. Register types during initialization phase only.
|
||||
* - Re-registering the same id overwrites the previous entry ("last wins").
|
||||
* - ClassId must be hashable (std::hash<ClassId> specialization must exist).
|
||||
*
|
||||
* @tparam ClassId -Unique class identifier type (e.g. std::string, int, enum).
|
||||
* @tparam BaseClass -Base type for all created objects.
|
||||
* @tparam Arg - Constructor arguments forwarded to registered derived types (by value).
|
||||
*
|
||||
* Usage example:
|
||||
* @code
|
||||
* #include <string>
|
||||
*
|
||||
* struct Base {
|
||||
* virtual ~Base() = default;
|
||||
* virtual int get() = 0;
|
||||
* };
|
||||
*
|
||||
* struct Derived1 : Base {
|
||||
* explicit Derived1(int start) : m_start(start) {}
|
||||
* int get() override { return m_start + 1; }
|
||||
* int m_start = 0;
|
||||
* };
|
||||
*
|
||||
* struct Derived2 : Base {
|
||||
* explicit Derived2(int start) : m_start(start) {}
|
||||
* int get() override { return m_start + 2; }
|
||||
* int m_start = 0;
|
||||
* };
|
||||
*
|
||||
* TypeFactory<std::string, Base, int> factory;
|
||||
* factory.registerType<Derived1>("one");
|
||||
* factory.registerType<Derived2>("two");
|
||||
*
|
||||
* auto a = factory.create("one", 10);
|
||||
* auto b = factory.creator("two")(10); // advanced API: get creator function
|
||||
*
|
||||
* (void)a->get();
|
||||
* (void)b->get();
|
||||
* @endcode
|
||||
*/
|
||||
template <class ClassId, class BaseClass, class... Args>
|
||||
class TypeFactory {
|
||||
public:
|
||||
using BasePtr = std::shared_ptr<BaseClass>;
|
||||
using CreatorFn = BasePtr (*)(Args...);
|
||||
|
||||
TypeFactory() = default;
|
||||
|
||||
/**
|
||||
* @brief Registers a Derived type under the given id.
|
||||
*
|
||||
* Requirements (checked at compile-time):
|
||||
* - Derived must inherit from BaseClass
|
||||
* - Derived must be constructible from Args...
|
||||
*
|
||||
* Re-registering the same id overwrites the previous entry ("last wins").
|
||||
*/
|
||||
template <class Derived>
|
||||
void registerType(const ClassId& id) {
|
||||
static_assert(std::is_base_of_v<BaseClass, Derived>,
|
||||
"Derived must inherit from BaseClass");
|
||||
static_assert(std::is_constructible_v<Derived, Args...>,
|
||||
"Derived must be constructible from Args...");
|
||||
|
||||
classes.insert_or_assign(id, &instantiate<Derived>);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Returns creator function for the given id.
|
||||
* @throws std::out_of_range when id is not registered.
|
||||
*/
|
||||
[[nodiscard]] CreatorFn creator(const ClassId& id) const {
|
||||
return classes.at(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Creates an instance for the given id using provided arguments.
|
||||
* @throws std::out_of_range when id is not registered.
|
||||
*/
|
||||
[[nodiscard]] BasePtr create(const ClassId& id, Args... args) const {
|
||||
return creator(id)(std::move(args)...);
|
||||
}
|
||||
|
||||
/**
|
||||
* @brief Checks whether an id is registered.
|
||||
*/
|
||||
[[nodiscard]] bool contains(const ClassId& id) const {
|
||||
return classes.find(id) != classes.end();
|
||||
}
|
||||
|
||||
protected:
|
||||
// Protected by design: derived factories may extend/inspect the registry.
|
||||
std::unordered_map<ClassId, CreatorFn> classes;
|
||||
|
||||
private:
|
||||
template <class Derived>
|
||||
static BasePtr instantiate(Args... args) {
|
||||
return std::make_shared<Derived>(std::move(args)...);
|
||||
}
|
||||
};
|
||||
#endif // TYPEFACTORY_H
|
||||
Reference in new issue
Block a user