summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorIvan Solovev <ivan.solovev@qt.io>2022-11-01 12:34:47 +0100
committerIvan Solovev <ivan.solovev@qt.io>2022-12-01 11:01:55 +0100
commit186f34bec49bcdf69817dec016317dd1edcbfa22 (patch)
tree3b2f290c7e2d05b13d897856981c0eae7b4f026c /src
parent8c6b497e81f2b5cbaa2c2741fd65057d6e31f1cd (diff)
Long live QCanDbcFileParser!
[ChangeLog][CAN Bus] Introduce a new QCanDbcFileParser class to parse DBC files. The DBC file parser will provide a list of QCanMessageDescriptions as a result. These can later be used in a QCanFrameProcessor to decode or encode QCanBusFrames. The current implementation parses DBC files using regular expressions. Currently the following keywords are supported: * BO_ - message description * SG_ - signal description * SIG_VALTYPE_ - signal type description * SG_MUL_VAL_ - extended multiplexing description * CM_ - comments (only for message and signal descriptions) All the other lines from the DBC files are simply ignored. Task-number: QTBUG-107075 Change-Id: I40091160aa39d399143ab8a5ccd194753cc86e10 Reviewed-by: Alex Blasche <alexander.blasche@qt.io>
Diffstat (limited to 'src')
-rw-r--r--src/serialbus/CMakeLists.txt1
-rw-r--r--src/serialbus/doc/src/external-resources.qdoc12
-rw-r--r--src/serialbus/qcandbcfileparser.cpp877
-rw-r--r--src/serialbus/qcandbcfileparser.h51
-rw-r--r--src/serialbus/qcandbcfileparser_p.h60
5 files changed, 1001 insertions, 0 deletions
diff --git a/src/serialbus/CMakeLists.txt b/src/serialbus/CMakeLists.txt
index a6c153f..1329595 100644
--- a/src/serialbus/CMakeLists.txt
+++ b/src/serialbus/CMakeLists.txt
@@ -14,6 +14,7 @@ qt_internal_add_module(SerialBus
qcanbusfactory.cpp qcanbusfactory.h
qcanbusframe.cpp qcanbusframe.h
qcancommondefinitions.cpp qcancommondefinitions.h
+ qcandbcfileparser.cpp qcandbcfileparser.h qcandbcfileparser_p.h
qcanframeprocessor.cpp qcanframeprocessor.h qcanframeprocessor_p.h
qcanmessagedescription.cpp qcanmessagedescription.h qcanmessagedescription_p.h
qcansignaldescription.cpp qcansignaldescription.h qcansignaldescription_p.h
diff --git a/src/serialbus/doc/src/external-resources.qdoc b/src/serialbus/doc/src/external-resources.qdoc
new file mode 100644
index 0000000..e8d4a90
--- /dev/null
+++ b/src/serialbus/doc/src/external-resources.qdoc
@@ -0,0 +1,12 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only
+
+/*!
+\externalpage https://docs.openvehicles.com/en/stable/components/vehicle_dbc/docs/dbc-primer.html
+\title OpenVehicles DBC Intro
+*/
+
+/*!
+\externalpage https://www.csselectronics.com/pages/can-dbc-file-database-intro
+\title CSSElectronics DBC Intro
+*/
diff --git a/src/serialbus/qcandbcfileparser.cpp b/src/serialbus/qcandbcfileparser.cpp
new file mode 100644
index 0000000..9b90397
--- /dev/null
+++ b/src/serialbus/qcandbcfileparser.cpp
@@ -0,0 +1,877 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#include "qcandbcfileparser.h"
+#include "qcandbcfileparser_p.h"
+#include "qcanmessagedescription.h"
+#include "qcansignaldescription.h"
+#include "qcanuniqueiddescription.h"
+#include "private/qcanmessagedescription_p.h"
+#include "private/qcansignaldescription_p.h"
+
+#include <QtCore/QFile>
+#include <QtCore/QRegularExpression>
+
+QT_BEGIN_NAMESPACE
+
+/*!
+ \class QCanDbcFileParser
+ \inmodule QtSerialBus
+ \since 6.5
+
+ \brief The QCanDbcFileParser class can be used to parse DBC files.
+
+ A CAN database or CAN DBC file is an ASCII text file that contains
+ information on how to decode and interpret raw CAN bus data. Some more
+ details about the format can be found \l {CSSElectronics DBC Intro}{here}
+ or \l {OpenVehicles DBC Intro}{here}.
+
+ The QCanDbcFileParser class takes the input DBC file, parses it, and
+ provides a list of \l {QCanMessageDescription}s as an output. These message
+ descriptions can be forwarded to \l QCanFrameProcessor, and later used
+ as rules to encode or decode \l {QCanBusFrame}s.
+
+ Use one of \l parse() overloads to specify a file or a list of files that
+ will be processed. Both overloads return \c true if the parsing completes
+ successfully and \c false otherwise.
+
+ Call the \l error() method to get the error which occurred during the
+ parsing. If the parsing completes successfully, this method will return
+ \l {QCanDbcFileParser::}{NoError}. Otherwise, you can use an
+ \l errorString() method to get the string representation of an error.
+
+ During the parsing some non-critical problems may occur as well. Such
+ problems will be logged, but the parsing process will not be aborted. You
+ can use the \l warnings() method to get the full list of such problems
+ after the parsing is completed.
+
+ If the parsing completes successfully, call \l messageDescriptions() to get
+ a list of the message descriptions that were extracted during the last
+ \l parse() call.
+
+ Use the static \l uniqueIdDescription() function to get a
+ \l QCanUniqueIdDescription for the DBC format.
+
+ \code
+ QCanDbcFileParser fileParser;
+ const bool result = fileParser.parse(u"path/to/file.dbc"_s);
+ // Check result, call error() and warnings() if needed
+
+ // Prepare a QCanFrameProcessor to decode or encode DBC frames
+ QCanFrameProcessor frameProcessor;
+ frameProcessor.setUniqueIdDescription(QCanDbcFileParser::uniqueIdDescription());
+ frameProcessor.setMessageDescriptions(fileParser.messageDescriptions());
+ \endcode
+
+ \note The parser is stateful, which means that all the results (like
+ extracted message descriptions, error code, or warnings) are reset once the
+ next parsing starts.
+
+ \section2 Supported Keywords
+
+ The current implementation supports only a subset of keywords that you can
+ find in a DBC file:
+
+ \list
+ \li \c {BO_} - message description.
+ \li \c {SG_} - signal description.
+ \li \c {SIG_VALTYPE_} - signal type description.
+ \li \c {SG_MUL_VAL_} - extended multiplexing description.
+ \li \c {CM_} - comments (only for message and signal descriptions).
+ \endlist
+
+ Lines starting from other keywords are simply ignored.
+
+ \sa QCanMessageDescription, QCanFrameProcessor
+*/
+
+/*!
+ \enum QCanDbcFileParser::Error
+
+ This enum represents the possible errors that can happen during the parsing
+ of a DBC file.
+
+ \value NoError No error occurred.
+ \value FileReadError An error occurred while opening or reading the file.
+ \value ParseError An error occurred while parsing the content of the file.
+*/
+
+/*!
+ Constructs a DBC file parser.
+*/
+QCanDbcFileParser::QCanDbcFileParser()
+ : d(std::make_unique<QCanDbcFileParserPrivate>())
+{
+}
+
+/*!
+ Destroys this DBC file parser.
+*/
+QCanDbcFileParser::~QCanDbcFileParser() = default;
+
+/*!
+ Parses the file \a fileName. Returns \c true if the parsing completed
+ successfully or \c false otherwise.
+
+ If the parsing completed successfully, call the \l messageDescriptions()
+ method to get the list of all extracted message descriptions.
+
+ If the parsing failed, call the \l error() and \l errorString() methods
+ to get the information about the error.
+
+ Call the \l warnings() method to get the list of warnings that were
+ logged during the parsing.
+
+ \sa messageDescriptions(), error(), warnings()
+*/
+bool QCanDbcFileParser::parse(const QString &fileName)
+{
+ d->reset();
+ return d->parseFile(fileName);
+}
+
+/*!
+ \overload
+
+ Parses a list of files \a fileNames. Returns \c true if the parsing
+ completed successfully or \c false otherwise.
+
+ If the parsing completed successfully, call the \l messageDescriptions()
+ method to get the list of all extracted message descriptions.
+
+ The parsing stops at the first error. Call the \l error() and
+ \l errorString() methods to get the information about the error.
+
+ Call the \l warnings() method to get the list of warnings that were
+ logged during the parsing.
+
+ \sa messageDescriptions(), error(), warnings()
+*/
+bool QCanDbcFileParser::parse(const QStringList &fileNames)
+{
+ d->reset();
+ for (const auto &fileName : fileNames) {
+ if (!d->parseFile(fileName))
+ return false;
+ }
+ return true;
+}
+
+/*!
+ Returns the list of message descriptions that were extracted during the
+ last \l parse() call.
+
+ \sa parse(), error()
+*/
+QList<QCanMessageDescription> QCanDbcFileParser::messageDescriptions() const
+{
+ return d->getMessages();
+}
+
+/*!
+ Returns the last error which occurred during the parsing.
+
+ \sa errorString(), parse()
+*/
+QCanDbcFileParser::Error QCanDbcFileParser::error() const
+{
+ return d->m_error;
+}
+
+/*!
+ Returns the text representation of the last error which occurred during the
+ parsing or an empty string if there was no error.
+
+ \sa error()
+*/
+QString QCanDbcFileParser::errorString() const
+{
+ return d->m_errorString;
+}
+
+/*!
+ Returns the list of non-critical problems which occurred during the parsing.
+
+ A typical problem can be a malformed message or signal description. In such
+ cases the malformed message or signal is skipped, but the rest of the file
+ can be processed as usual.
+
+ \sa error(), parse()
+*/
+QStringList QCanDbcFileParser::warnings() const
+{
+ return d->m_warnings;
+}
+
+/*!
+ Returns a unique identifier description. DBC protocol always uses the
+ Frame Id as an identifier, and therefore the unique identifier description
+ is always the same.
+
+ Use this method to get an instance of \l QCanUniqueIdDescription and pass
+ it to \l QCanFrameProcessor.
+
+ \sa QCanFrameProcessor::setUniqueIdDescription()
+*/
+QCanUniqueIdDescription QCanDbcFileParser::uniqueIdDescription()
+{
+ QCanUniqueIdDescription desc;
+ desc.setSource(QtCanBus::DataSource::FrameId);
+ desc.setEndian(QtCanBus::DataEndian::LittleEndian);
+ desc.setStartBit(0);
+ desc.setBitLength(29); // for both extended and normal frame id
+ return desc;
+}
+
+/* QCanDbcFileParserPrivate implementation */
+
+using namespace Qt::StringLiterals;
+
+// signal name with whitespaces is invalid in DBC, so we can safely use it
+// for internal purposes
+static const auto kQtDummySignal = u"Qt Dummy Signal"_s;
+
+static constexpr auto kMessageDef = "BO_ "_L1;
+static constexpr auto kSignalDef = "SG_ "_L1;
+static constexpr auto kSigValTypeDef = "SIG_VALTYPE_ "_L1;
+static constexpr auto kCommentDef = "CM_ "_L1;
+static constexpr auto kExtendedMuxDef = "SG_MUL_VAL_ "_L1;
+
+static constexpr auto kUnsignedIntRegExp = "\\d+"_L1;
+static constexpr auto kDoubleRegExp = "[+-]?\\d+(.\\d+([eE][+-]?\\d+)?)?"_L1;
+static constexpr auto kDbcIdentRegExp = "[_[:alpha:]][_[:alnum:]]+"_L1;
+static constexpr auto kOneOrMoreSpaceRegExp = "[ ]+"_L1;
+static constexpr auto kMaybeSpaceRegExp = "[ ]*"_L1;
+static constexpr auto kMuxIndicatorRegExp = "M|m\\d+M?"_L1;
+static constexpr auto kByteOrderRegExp = "0|1"_L1;
+static constexpr auto kValueTypeRegExp = "\\+|\\-"_L1;
+// The pattern matches all ASCII characters in range 0x20 - 0x7E, except
+// double-quote (") and backslash (\).
+static constexpr auto kCharStrRegExp = "((?![\\\"\\\\])[\x20-\x7e])*"_L1;
+
+void QCanDbcFileParserPrivate::reset()
+{
+ m_fileName.clear();
+ m_error = QCanDbcFileParser::Error::NoError;
+ m_errorString.clear();
+ m_warnings.clear();
+ m_lineOffset = 0;
+ m_isProcessingMessage = false;
+ m_seenExtraData = false;
+ m_currentMessage = {};
+ m_messageDescriptions.clear();
+}
+
+/*!
+ \internal
+ Returns \c false only in case of hard error. Returns \c true even if some
+ warnings occurred during parsing.
+*/
+bool QCanDbcFileParserPrivate::parseFile(const QString &fileName)
+{
+ QFile f(fileName);
+ if (!f.open(QIODevice::ReadOnly)) {
+ m_error = QCanDbcFileParser::Error::FileReadError;
+ m_errorString = f.errorString();
+ return false;
+ }
+ m_fileName = fileName;
+ m_seenExtraData = false;
+
+ while (!f.atEnd()) {
+ const QString str = QString::fromLatin1(f.readLine().trimmed());
+ if (!processLine({str.constData(), str.size()})) // also sets the error properly
+ return false;
+ }
+ addCurrentMessage(); // check if we need to add the message
+ // now when we parsed the whole file, we can verify the signal multiplexing
+ postProcessSignalMultiplexing();
+
+ return true;
+}
+
+/*!
+ \internal
+ Returns \c false only in case of hard error. Returns \c true even if some
+ warnings occurred during parsing.
+*/
+bool QCanDbcFileParserPrivate::processLine(const QStringView line)
+{
+ QStringView data = line;
+ m_lineOffset = 0;
+ if (data.startsWith(kMessageDef)) {
+ if (m_seenExtraData) {
+ // Unexpected position of message description
+ m_error = QCanDbcFileParser::Error::ParseError;
+ m_errorString = QObject::tr("Failed to parse file %1. Unexpected position "
+ "of %2 section.").arg(m_fileName, kMessageDef);
+ return false;
+ }
+ addCurrentMessage();
+ if (!parseMessage(data))
+ return false;
+ }
+ // signal definitions can be on the same line as message definition,
+ // or on a separate line
+ data = data.sliced(m_lineOffset).trimmed();
+ while (data.startsWith(kSignalDef)) {
+ if (!m_isProcessingMessage || m_seenExtraData) {
+ // Unexpected position of signal description
+ m_error = QCanDbcFileParser::Error::ParseError;
+ m_errorString = QObject::tr("Failed to parse file %1. Unexpected position "
+ "of %2 section.").arg(m_fileName, kSignalDef);
+ return false;
+ }
+ if (!parseSignal(data))
+ return false;
+ data = data.sliced(m_lineOffset).trimmed();
+ }
+ // If we detect one of the following lines, then message description is
+ // finished. We also assume that we can have only one key at each line.
+ if (data.startsWith(kSigValTypeDef)) {
+ m_seenExtraData = true;
+ addCurrentMessage();
+ parseSignalType(data);
+ } else if (data.startsWith(kCommentDef)) {
+ m_seenExtraData = true;
+ addCurrentMessage();
+ parseComment(data);
+ } else if (data.startsWith(kExtendedMuxDef)) {
+ m_seenExtraData = true;
+ addCurrentMessage();
+ parseExtendedMux(data);
+ }
+ return true;
+}
+
+/*!
+ \internal
+ Returns \c false only in case of hard error. Returns \c true even if some
+ warnings occurred during parsing.
+*/
+bool QCanDbcFileParserPrivate::parseMessage(const QStringView data)
+{
+ // The regexp matches the following definition:
+ // BO_ message_id message_name ':' message_size transmitter
+ // also considering the fact that spaces around ':' seem to be optional, and
+ // allowing more than one space between parts.
+
+ // %1 - messageDef
+ // %2 - maybeSpace
+ // %3 - unsignedInt
+ // %4 - oneOrMoreSpace
+ // %5 - DbcIdentifier
+ static const QString regExStr =
+ "%1%2(?<messageId>%3)%4(?<name>%5)%2:%2(?<size>%3)%4(?<transmitter>%5)"_L1.
+ arg(kMessageDef, kMaybeSpaceRegExp, kUnsignedIntRegExp, kOneOrMoreSpaceRegExp,
+ kDbcIdentRegExp);
+ static const QRegularExpression messageRegExp(regExStr);
+
+ m_isProcessingMessage = false;
+ const auto match = messageRegExp.matchView(data);
+ if (match.hasMatch()) {
+ m_currentMessage = extractMessage(match);
+ // can't check for isValid() here, because demands signal descriptions
+ if (!m_currentMessage.name().isEmpty()) {
+ m_isProcessingMessage = true;
+ } else {
+ addWarning(QObject::tr("Failed to parse message description from "
+ "string %1").arg(data));
+ }
+ m_lineOffset = match.capturedEnd(0);
+ } else {
+ addWarning(QObject::tr("Failed to find message description in string %1").arg(data));
+ m_lineOffset = data.size(); // skip this string
+ }
+ return true;
+}
+
+QCanMessageDescription
+QCanDbcFileParserPrivate::extractMessage(const QRegularExpressionMatch &match)
+{
+ Q_ASSERT(match.hasMatch());
+ QCanMessageDescription desc;
+ desc.setName(match.captured(u"name"_s));
+
+ bool ok = false;
+
+ const auto id = match.capturedView(u"messageId"_s).toUInt(&ok);
+ if (ok) {
+ desc.setUniqueId(id);
+ } else {
+ addWarning(QObject::tr("Failed to parse frame id for message %1").arg(desc.name()));
+ return {};
+ }
+
+ const auto size = match.capturedView(u"size"_s).toUInt(&ok);
+ if (ok) {
+ desc.setSize(size);
+ } else {
+ addWarning(QObject::tr("Failed to parse size for message %1").arg(desc.name()));
+ return {};
+ }
+
+ desc.setTransmitter(match.captured(u"transmitter"_s));
+
+ return desc;
+}
+
+/*!
+ \internal
+ Returns \c false only in case of hard error. Returns \c true even if some
+ warnings occurred during parsing.
+*/
+bool QCanDbcFileParserPrivate::parseSignal(const QStringView data)
+{
+ // The regexp should match the following pattern:
+ // SG_ signal_name multiplexer_indicator : start_bit |
+ // signal_size @ byte_order value_type ( factor , offset )
+ // [ minimum | maximum ] unit receiver {, receiver}
+ // We also need to consider the fact that some of the spaces might be
+ // optional, and we can potentially allow more spaces between parts.
+ // Note that the end of the signal description can contain multiple
+ // receivers. The regexp is supposed to extract all of them, but we use
+ // only the first one for now.
+
+ // %1 - SignalDef
+ // %2 - MaybeSpace
+ // %3 - DbcIdentifier
+ // %4 - OneOrMoreSpace
+ // %5 - MuxIndicator
+ // %6 - unsignedInt
+ // %7 - byteOrder
+ // %8 - valueType
+ // %9 - double
+ // %10 - charStr
+ static const QString regExStr =
+ "%1%2(?<name>%3)(%4(?<mux>%5))?%2:%2(?<startBit>%6)%2\\|%2(?<sigSize>%6)%2@%2"
+ "(?<byteOrder>%7)%2(?<valueType>%8)%4\\(%2(?<factor>%9)%2,%2(?<offset>%9)%2\\)"
+ "%4\\[%2(?<min>%9)%2\\|%2(?<max>%9)%2\\]%4\"(?<unit>%10)\""
+ "%4(?<receiver>%3)(%2,%2%3)*"_L1.
+ arg(kSignalDef, kMaybeSpaceRegExp, kDbcIdentRegExp, kOneOrMoreSpaceRegExp,
+ kMuxIndicatorRegExp, kUnsignedIntRegExp, kByteOrderRegExp, kValueTypeRegExp,
+ kDoubleRegExp, kCharStrRegExp);
+ static const QRegularExpression signalRegExp(regExStr);
+
+ const auto match = signalRegExp.matchView(data);
+ if (match.hasMatch()) {
+ QCanSignalDescription desc = extractSignal(match);
+
+ if (desc.isValid())
+ m_currentMessage.addSignalDescription(desc);
+ else
+ addWarning(QObject::tr("Failed to parse signal description from string %1").arg(data));
+
+ m_lineOffset = match.capturedEnd(0);
+ } else {
+ addWarning(QObject::tr("Failed to find signal description in string %1").arg(data));
+ m_lineOffset = data.size(); // skip this string
+ }
+ return true;
+}
+
+QCanSignalDescription QCanDbcFileParserPrivate::extractSignal(const QRegularExpressionMatch &match)
+{
+ Q_ASSERT(match.hasMatch());
+ QCanSignalDescription desc;
+ desc.setName(match.captured(u"name"_s));
+
+ bool ok = false;
+
+ if (match.hasCaptured(u"mux"_s)) {
+ const auto muxStr = match.capturedView(u"mux"_s);
+ if (muxStr == u"M"_s) {
+ desc.setMultiplexState(QtCanBus::MultiplexState::MultiplexorSwitch);
+ } else if (muxStr.endsWith(u"M"_s, Qt::CaseSensitive)) {
+ desc.setMultiplexState(QtCanBus::MultiplexState::SwitchAndSignal);
+ const auto val = muxStr.sliced(1, muxStr.size() - 2).toUInt(&ok);
+ if (!ok) {
+ addWarning(QObject::tr("Failed to parse multiplexor value for signal %1").
+ arg(desc.name()));
+ return {};
+ }
+ // We have the value, but we do not really know the multiplexor
+ // switch name. To know it, we potentially need to parse all signals
+ // for the message. So for now we just create a dummy entry, and
+ // the actual signal name will be updated later;
+ desc.addMultiplexSignal(kQtDummySignal, val);
+ } else {
+ desc.setMultiplexState(QtCanBus::MultiplexState::MultiplexedSignal);
+ const auto val = muxStr.sliced(1).toUInt(&ok);
+ if (!ok) {
+ addWarning(QObject::tr("Failed to parse multiplexor value for signal %1").
+ arg(desc.name()));
+ return {};
+ }
+ // Same as above
+ desc.addMultiplexSignal(kQtDummySignal, val);
+ }
+ }
+
+ const uint startBit = match.capturedView(u"startBit"_s).toUInt(&ok);
+ if (ok) {
+ desc.setStartBit(startBit);
+ } else {
+ addWarning(QObject::tr("Failed to parse start bit for signal %1").arg(desc.name()));
+ return {};
+ }
+
+ const uint bitLength = match.capturedView(u"sigSize"_s).toUInt(&ok);
+ if (ok) {
+ desc.setBitLength(bitLength);
+ } else {
+ addWarning(QObject::tr("Failed to parse bit length for signal %1").arg(desc.name()));
+ return {};
+ }
+
+ // 0 = BE; 1 = LE
+ const auto endian = match.capturedView(u"byteOrder"_s) == u"0"_s
+ ? QtCanBus::DataEndian::BigEndian : QtCanBus::DataEndian::LittleEndian;
+ desc.setDataEndian(endian);
+
+ // + = unsigned; - = signed
+ const auto dataFormat = match.capturedView(u"valueType"_s) == u"+"_s
+ ? QtCanBus::DataFormat::UnsignedInteger : QtCanBus::DataFormat::SignedInteger;
+ desc.setDataFormat(dataFormat);
+
+ const double factor = match.capturedView(u"factor"_s).toDouble(&ok);
+ if (ok) {
+ desc.setFactor(factor);
+ } else {
+ addWarning(QObject::tr("Failed to parse factor for signal %1").arg(desc.name()));
+ return {};
+ }
+
+ const double offset = match.capturedView(u"offset"_s).toDouble(&ok);
+ if (ok) {
+ desc.setOffset(offset);
+ } else {
+ addWarning(QObject::tr("Failed to parse offset for signal %1").arg(desc.name()));
+ return {};
+ }
+
+ const double min = match.capturedView(u"min"_s).toDouble(&ok);
+ if (ok) {
+ const double max = match.capturedView(u"max"_s).toDouble(&ok);
+ if (ok)
+ desc.setRange(min, max);
+ }
+ if (!ok) {
+ addWarning(QObject::tr("Failed to parse value range from signal %1").arg(desc.name()));
+ return {};
+ }
+
+ desc.setPhysicalUnit(match.captured(u"unit"_s));
+ desc.setReceiver(match.captured(u"receiver"_s));
+
+ return desc;
+}
+
+void QCanDbcFileParserPrivate::parseSignalType(const QStringView data)
+{
+ // The regexp should match the following pattern:
+ // SIG_VALTYPE_ message_id signal_name signal_extended_value_type ;
+ // We also need to consider the fact that we can potentially allow more
+ // spaces between parts.
+
+ // %1 sigValTypeDef
+ // %2 maybeSpace
+ // %3 unsignedInt
+ // %4 oneOrMoreSpace
+ // %5 DbcIdentifier
+ const QString regExStr =
+ "%1%2(?<messageId>%3)%4(?<sigName>%5)%2:%2(?<type>%3)%2;"_L1.
+ arg(kSigValTypeDef, kMaybeSpaceRegExp, kUnsignedIntRegExp,
+ kOneOrMoreSpaceRegExp, kDbcIdentRegExp);
+ const QRegularExpression sigValTypeRegEx(regExStr);
+
+ const auto match = sigValTypeRegEx.matchView(data);
+ if (!match.hasMatch()) {
+ m_lineOffset = data.size();
+ return;
+ }
+
+ m_lineOffset = match.capturedEnd(0);
+
+ bool ok = false;
+ const auto uid = match.capturedView(u"messageId"_s).toUInt(&ok);
+ if (!ok)
+ return;
+
+ auto msgDesc = m_messageDescriptions.value(uid);
+ if (msgDesc.isValid()) {
+ const QString sigName = match.captured(u"sigName"_s);
+ auto sigDesc = msgDesc.signalDescriptionForName(sigName);
+ if (sigDesc.isValid()) {
+ const auto type = match.capturedView(u"type").toUInt(&ok);
+ if (ok) {
+ bool sigDescChanged = false;
+ switch (type) {
+ case 0: /* signed or unsigned integer */
+ // do nothing, as we already have signed/unsinged integer
+ // based on "SG_ " string
+ break;
+ case 1: /* 32-bit IEEE-float */
+ sigDesc.setDataFormat(QtCanBus::DataFormat::Float);
+ sigDesc.setBitLength(32);
+ sigDescChanged = true;
+ break;
+ case 2: /* 64-bit IEEE-double */
+ sigDesc.setDataFormat(QtCanBus::DataFormat::Double);
+ sigDesc.setBitLength(64);
+ sigDescChanged = true;
+ break;
+ default:
+ // invalid value
+ break;
+ }
+ if (sigDescChanged) {
+ msgDesc.addSignalDescription(sigDesc);
+ m_messageDescriptions.insert(msgDesc.uniqueId(), msgDesc);
+ }
+ }
+ }
+ }
+}
+
+void QCanDbcFileParserPrivate::parseComment(const QStringView data)
+{
+ // The comment for message or signal description is represented by the
+ // following pattern:
+ // CM_ (BO_ message_id char_string | SG_ message_id signal_name char_string);
+
+ // %1 commentDef
+ // %2 maybeSpace
+ // %3 messageDef
+ // %4 signalDef
+ // %5 oneOrMoreSpace
+ // %6 unsignedInt
+ // %7 DbcIdentifier
+ // %8 charStr
+ const QString regExStr =
+ "%1%2(?<type>(%3|%4))%2(?<messageId>%6)%5((?<sigName>%7)%5)?\"(?<comment>%8)\"%2;"_L1.
+ arg(kCommentDef, kMaybeSpaceRegExp, kMessageDef, kSignalDef, kOneOrMoreSpaceRegExp,
+ kUnsignedIntRegExp, kDbcIdentRegExp, kCharStrRegExp);
+ const QRegularExpression commentRegExp(regExStr);
+
+ const auto match = commentRegExp.matchView(data);
+ if (!match.hasMatch()) {
+ m_lineOffset = data.size();
+ return;
+ }
+
+ m_lineOffset = match.capturedEnd(0);
+
+ const auto type = match.capturedView(u"type"_s);
+
+ bool ok = false;
+ const auto uid = match.capturedView(u"messageId"_s).toUInt(&ok);
+ if (!ok)
+ return;
+
+ auto messageDesc = m_messageDescriptions.value(uid);
+ if (!messageDesc.isValid())
+ return;
+
+ if (type == kMessageDef) {
+ const QString comment = match.captured(u"comment"_s);
+ messageDesc.setComment(comment);
+ m_messageDescriptions.insert(uid, messageDesc);
+ } else if (type == kSignalDef) {
+ const QString sigName = match.captured(u"sigName"_s);
+ if (!sigName.isEmpty()) {
+ auto signalDesc = messageDesc.signalDescriptionForName(sigName);
+ if (signalDesc.isValid()) {
+ const QString comment = match.captured(u"comment"_s);
+ signalDesc.setComment(comment);
+ messageDesc.addSignalDescription(signalDesc);
+ m_messageDescriptions.insert(uid, messageDesc);
+ }
+ }
+ }
+}
+
+void QCanDbcFileParserPrivate::parseExtendedMux(const QStringView data)
+{
+ // The extended multiplexing is defined by the following pattern:
+ // SG_MUL_VAL_ message_id multiplexed_signal_name
+ // multiplexor_switch_name multiplexor_value_ranges ;
+ // Here multiplexor_value_ranges consists of multiple ranges, separated
+ // by a whitespace, and one range is defined as follows:
+ // multiplexor_value_range = unsigned_integer - unsigned_integer
+
+ // %1 extendedMuxDef
+ // %2 maybeSpace
+ // %3 unsignedInt
+ // %4 oneOrMoreSpace
+ // %5 DbcIdentifier
+ const QString regExStr =
+ "%1%2(?<messageId>%3)%4(?<multiplexedSignal>%5)%4(?<multiplexorSwitch>%5)%4"
+ "(?<firstRange>%3%2-%2%3)(%2,%2%3%2-%2%3)*%2;"_L1.
+ arg(kExtendedMuxDef, kMaybeSpaceRegExp, kUnsignedIntRegExp, kOneOrMoreSpaceRegExp,
+ kDbcIdentRegExp);
+ const QRegularExpression extendedMuxRegExp(regExStr);
+
+ const auto match = extendedMuxRegExp.matchView(data);
+ if (!match.hasMatch()) {
+ m_lineOffset = data.size();
+ return;
+ }
+
+ m_lineOffset = match.capturedEnd(0);
+
+ bool ok = false;
+ const auto uid = match.capturedView(u"messageId"_s).toUInt(&ok);
+ if (!ok)
+ return;
+
+ auto messageDesc = m_messageDescriptions.value(uid);
+ if (!messageDesc.isValid())
+ return;
+
+ const QString multiplexedSignalName = match.captured(u"multiplexedSignal"_s);
+ const QString multiplexorSwitchName = match.captured(u"multiplexorSwitch"_s);
+
+ auto multiplexedSignal = messageDesc.signalDescriptionForName(multiplexedSignalName);
+ auto multiplexorSwitch = messageDesc.signalDescriptionForName(multiplexorSwitchName);
+
+ if (!multiplexedSignal.isValid() || !multiplexorSwitch.isValid())
+ return;
+
+ auto signalRanges = multiplexedSignal.multiplexSignals();
+ signalRanges.remove(kQtDummySignal); // dummy signal not needed anymore
+
+ QCanSignalDescription::MultiplexValues rangeValues;
+ auto rangeView = match.capturedView(u"firstRange"_s);
+ const auto sepIdx = rangeView.indexOf(u'-');
+ if (sepIdx != -1) {
+ const auto min = rangeView.first(sepIdx).trimmed().toUInt();
+ const auto max = rangeView.sliced(sepIdx + 1).trimmed().toUInt();
+ rangeValues.emplaceBack(qMakePair(min, max));
+ }
+
+ // We can have an arbitrary amount of ranges, so we can't use capture groups
+ // to capture them. But we know that they follow a specific pattern (because
+ // the full string matched the regexp). So we need to parse the rest of the
+ // matched string manually
+ const auto totalEnd = match.capturedEnd(0); // including the ';'
+ const auto firstRangeEnd = match.capturedEnd(u"firstRange"_s);
+ const auto len = totalEnd - firstRangeEnd - 1;
+ if (len > 0) {
+ const auto otherRangesView = data.sliced(firstRangeEnd, len).trimmed();
+ const QStringTokenizer parts = otherRangesView.tokenize(u',', Qt::SkipEmptyParts);
+ for (const QStringView range : parts) {
+ const auto sepIdx = range.indexOf(u'-');
+ if (sepIdx != -1) {
+ const auto min = range.first(sepIdx).trimmed().toUInt();
+ const auto max = range.sliced(sepIdx + 1).trimmed().toUInt();
+ rangeValues.emplaceBack(qMakePair(min, max));
+ }
+ }
+ }
+
+ if (!rangeValues.isEmpty())
+ signalRanges.insert(multiplexorSwitchName, rangeValues);
+ else
+ signalRanges.remove(multiplexorSwitchName);
+
+ // update the value
+ multiplexedSignal.setMultiplexSignals(signalRanges);
+ messageDesc.addSignalDescription(multiplexedSignal);
+ m_messageDescriptions.insert(uid, messageDesc);
+}
+
+void QCanDbcFileParserPrivate::postProcessSignalMultiplexing()
+{
+ // For the case of simple multiplexing we need to do the following for
+ // every message description:
+ // 1. Find the multiplexor signal
+ // 2. Replace all kQtDummySignal entries with the name of the multiplexor
+ // 3. While doing so, check if we have any signals with type
+ // SwitchAndSignal. This will mean that extended multiplexing is used
+ // 4. Also detect conditions when we have more than one multiplexor signal.
+ // This is an error as well, and message description should be discarded.
+
+ QList<QtCanBus::UniqueId> uidsToRemove;
+
+ for (auto &messageDesc : m_messageDescriptions) {
+ bool useExtendedMux = false;
+ QString multiplexorSignal;
+ auto &signalDescriptions = QCanMessageDescriptionPrivate::get(messageDesc)->messageSignals;
+ for (const auto &signalDesc : std::as_const(signalDescriptions)) {
+ if (signalDesc.multiplexState() == QtCanBus::MultiplexState::MultiplexorSwitch) {
+ if (multiplexorSignal.isEmpty()) {
+ multiplexorSignal = signalDesc.name();
+ } else {
+ // invalid config
+ multiplexorSignal.clear();
+ uidsToRemove.push_back(messageDesc.uniqueId());
+ break;
+ }
+ } else if (signalDesc.multiplexState() == QtCanBus::MultiplexState::SwitchAndSignal) {
+ // extended multiplexing
+ useExtendedMux = true;
+ }
+ }
+ if (!useExtendedMux && !multiplexorSignal.isEmpty()) {
+ // iterate through all signal descriptions and update kQtDummySignal
+ for (auto &signalDesc : signalDescriptions) {
+ if (signalDesc.multiplexState() == QtCanBus::MultiplexState::MultiplexedSignal) {
+ auto &muxValues = QCanSignalDescriptionPrivate::get(signalDesc)->muxSignals;
+ auto val = muxValues.value(kQtDummySignal);
+ muxValues.remove(kQtDummySignal);
+ muxValues.insert(multiplexorSignal, val);
+ }
+ }
+ } else if (useExtendedMux) {
+ // Iterate through all signal descriptions and check that we do
+ // not have any kQtDummySignal entries. It such entry exists, this
+ // means that there were errors while parsing extended multiplexing
+ // table, and this message description is invalid
+ for (const auto &signalDesc : std::as_const(signalDescriptions)) {
+ const auto muxSignals = signalDesc.multiplexSignals();
+ if (muxSignals.contains(kQtDummySignal)) {
+ uidsToRemove.push_back(messageDesc.uniqueId());
+ break;
+ }
+ }
+ }
+ }
+
+ for (const auto &uid : std::as_const(uidsToRemove)) {
+ m_messageDescriptions.remove(uid);
+ addWarning(QObject::tr("Message description with unique id %1 is skipped because "
+ "it has invalid multiplexing description.").arg(uid));
+ }
+}
+
+void QCanDbcFileParserPrivate::addWarning(QString &&warning)
+{
+ m_warnings.emplace_back(warning);
+}
+
+void QCanDbcFileParserPrivate::addCurrentMessage()
+{
+ if (m_isProcessingMessage) {
+ auto uid = m_currentMessage.uniqueId();
+ if (!m_currentMessage.isValid()) {
+ addWarning(QObject::tr("Message description with unique id %1 is skipped "
+ "because it's not valid.").arg(uid));
+ } else if (m_messageDescriptions.contains(uid)) {
+ addWarning(QObject::tr("Message description with unique id %1 is skipped "
+ "because such unique id is already used.").arg(uid));
+ } else {
+ m_messageDescriptions.insert(uid, m_currentMessage);
+ }
+ m_currentMessage = {};
+ m_isProcessingMessage = false;
+ }
+}
+
+QList<QCanMessageDescription> QCanDbcFileParserPrivate::getMessages() const
+{
+ return QList<QCanMessageDescription>(m_messageDescriptions.cbegin(),
+ m_messageDescriptions.cend());
+}
+
+QT_END_NAMESPACE
diff --git a/src/serialbus/qcandbcfileparser.h b/src/serialbus/qcandbcfileparser.h
new file mode 100644
index 0000000..2186c7d
--- /dev/null
+++ b/src/serialbus/qcandbcfileparser.h
@@ -0,0 +1,51 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QCANDBCFILEPARSER_H
+#define QCANDBCFILEPARSER_H
+
+#include <QtCore/QList>
+
+#include <QtSerialBus/qtserialbusglobal.h>
+
+#include <memory>
+
+QT_BEGIN_NAMESPACE
+
+class QCanDbcFileParserPrivate;
+class QCanMessageDescription;
+class QCanUniqueIdDescription;
+
+class Q_SERIALBUS_EXPORT QCanDbcFileParser
+{
+public:
+ enum class Error : quint8 {
+ NoError = 0,
+ FileReadError,
+ ParseError
+ };
+
+ QCanDbcFileParser();
+ ~QCanDbcFileParser();
+
+ bool parse(const QString &fileName);
+ bool parse(const QStringList &fileNames);
+
+ QList<QCanMessageDescription> messageDescriptions() const;
+
+ Error error() const;
+ QString errorString() const;
+ QStringList warnings() const;
+
+ static QCanUniqueIdDescription uniqueIdDescription();
+
+private:
+ std::unique_ptr<QCanDbcFileParserPrivate> d;
+ friend class QCanDbcFileParserPrivate;
+
+ Q_DISABLE_COPY_MOVE(QCanDbcFileParser)
+};
+
+QT_END_NAMESPACE
+
+#endif // QCANDBCFILEPARSER_H
diff --git a/src/serialbus/qcandbcfileparser_p.h b/src/serialbus/qcandbcfileparser_p.h
new file mode 100644
index 0000000..1a698b2
--- /dev/null
+++ b/src/serialbus/qcandbcfileparser_p.h
@@ -0,0 +1,60 @@
+// Copyright (C) 2022 The Qt Company Ltd.
+// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
+
+#ifndef QCANDBCFILEPARSER_P_H
+#define QCANDBCFILEPARSER_P_H
+
+//
+// W A R N I N G
+// -------------
+//
+// This file is not part of the Qt API. It exists purely as an
+// implementation detail. This header file may change from version to
+// version without notice, or even be removed.
+//
+// We mean it.
+//
+
+#include "qcandbcfileparser.h"
+#include "qcanmessagedescription.h"
+
+#include <QtCore/QHash>
+
+QT_BEGIN_NAMESPACE
+
+class QCanSignalDescription;
+
+class QCanDbcFileParserPrivate
+{
+public:
+ void reset();
+ bool parseFile(const QString &fileName);
+ bool processLine(const QStringView line);
+ bool parseMessage(const QStringView data);
+ QCanMessageDescription extractMessage(const QRegularExpressionMatch &match);
+ bool parseSignal(const QStringView data);
+ QCanSignalDescription extractSignal(const QRegularExpressionMatch &match);
+ void parseSignalType(const QStringView data);
+ void parseComment(const QStringView data);
+ void parseExtendedMux(const QStringView data);
+ void postProcessSignalMultiplexing();
+
+ void addWarning(QString &&warning);
+ void addCurrentMessage();
+
+ QList<QCanMessageDescription> getMessages() const;
+
+ QString m_fileName;
+ QCanDbcFileParser::Error m_error = QCanDbcFileParser::Error::NoError;
+ QString m_errorString;
+ QStringList m_warnings;
+ qsizetype m_lineOffset = 0;
+ bool m_isProcessingMessage = false;
+ bool m_seenExtraData = false;
+ QCanMessageDescription m_currentMessage;
+ QHash<QtCanBus::UniqueId, QCanMessageDescription> m_messageDescriptions;
+};
+
+QT_END_NAMESPACE
+
+#endif // QCANDBCFILEPARSER_P_H