diff options
author | Ivan Solovev <ivan.solovev@qt.io> | 2022-11-01 12:34:47 +0100 |
---|---|---|
committer | Ivan Solovev <ivan.solovev@qt.io> | 2022-12-01 11:01:55 +0100 |
commit | 186f34bec49bcdf69817dec016317dd1edcbfa22 (patch) | |
tree | 3b2f290c7e2d05b13d897856981c0eae7b4f026c /src | |
parent | 8c6b497e81f2b5cbaa2c2741fd65057d6e31f1cd (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.txt | 1 | ||||
-rw-r--r-- | src/serialbus/doc/src/external-resources.qdoc | 12 | ||||
-rw-r--r-- | src/serialbus/qcandbcfileparser.cpp | 877 | ||||
-rw-r--r-- | src/serialbus/qcandbcfileparser.h | 51 | ||||
-rw-r--r-- | src/serialbus/qcandbcfileparser_p.h | 60 |
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 |