1// Copyright (C) 2022 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qcandbcfileparser.h"
5#include "qcandbcfileparser_p.h"
6#include "qcanmessagedescription.h"
7#include "qcansignaldescription.h"
8#include "qcanuniqueiddescription.h"
9#include "private/qcanmessagedescription_p.h"
10#include "private/qcansignaldescription_p.h"
11
12#include <QtCore/QFile>
13#include <QtCore/QRegularExpression>
14
15#include <optional>
16
17QT_BEGIN_NAMESPACE
18
19/*!
20 \class QCanDbcFileParser
21 \inmodule QtSerialBus
22 \since 6.5
23 \preliminary
24
25 \brief The QCanDbcFileParser class can be used to parse DBC files.
26
27 A CAN database or CAN DBC file is an ASCII text file that contains
28 information on how to decode and interpret raw CAN bus data. Some more
29 details about the format can be found \l {CSSElectronics DBC Intro}{here}
30 or \l {OpenVehicles DBC Intro}{here}.
31
32 The QCanDbcFileParser class takes the input DBC file, parses it, and
33 provides a list of \l {QCanMessageDescription}s as an output. These message
34 descriptions can be forwarded to \l QCanFrameProcessor, and later used
35 as rules to encode or decode \l {QCanBusFrame}s.
36
37 Use one of \l parse() overloads to specify a file or a list of files that
38 will be processed. Both overloads return \c true if the parsing completes
39 successfully and \c false otherwise.
40
41 Call the \l error() method to get the error which occurred during the
42 parsing. If the parsing completes successfully, this method will return
43 \l {QCanDbcFileParser::}{None}. Otherwise, you can use an
44 \l errorString() method to get the string representation of an error.
45
46 During the parsing some non-critical problems may occur as well. Such
47 problems will be logged, but the parsing process will not be aborted. You
48 can use the \l warnings() method to get the full list of such problems
49 after the parsing is completed.
50
51 If the parsing completes successfully, call \l messageDescriptions() to get
52 a list of the message descriptions that were extracted during the last
53 \l parse() call. Call \l messageValueDescriptions() to get the textual
54 descriptions of signal raw values, if they are available.
55
56 Use the static \l uniqueIdDescription() function to get a
57 \l QCanUniqueIdDescription for the DBC format.
58
59 \code
60 QCanDbcFileParser fileParser;
61 const bool result = fileParser.parse(u"path/to/file.dbc"_s);
62 // Check result, call error() and warnings() if needed
63
64 // Prepare a QCanFrameProcessor to decode or encode DBC frames
65 QCanFrameProcessor frameProcessor;
66 frameProcessor.setUniqueIdDescription(QCanDbcFileParser::uniqueIdDescription());
67 frameProcessor.setMessageDescriptions(fileParser.messageDescriptions());
68 \endcode
69
70 \note The parser is stateful, which means that all the results (like
71 extracted message descriptions, error code, or warnings) are reset once the
72 next parsing starts.
73
74 \section2 Supported Keywords
75
76 The current implementation supports only a subset of keywords that you can
77 find in a DBC file:
78
79 \list
80 \li \c {BO_} - message description.
81 \li \c {SG_} - signal description.
82 \li \c {SIG_VALTYPE_} - signal type description.
83 \li \c {SG_MUL_VAL_} - extended multiplexing description.
84 \li \c {CM_} - comments (only for message and signal descriptions).
85 \li \c {VAL_} - textual descriptions for raw signal values.
86 \endlist
87
88 Lines starting from other keywords are simply ignored.
89
90 \sa QCanMessageDescription, QCanFrameProcessor
91*/
92
93/*!
94 \typealias QCanDbcFileParser::ValueDescriptions
95
96 This is a type alias for \c {QHash<quint32, QString>}.
97
98 The keys of the hash represent raw signal values, and the values of the
99 hash represent corresponding string descriptions.
100*/
101
102/*!
103 \typealias QCanDbcFileParser::SignalValueDescriptions
104
105 This is a type alias for \c {QHash<QString, ValueDescriptions>}.
106
107 The keys of the hash represent signal names, and the values of the
108 hash contain the corresponding \l QCanDbcFileParser::ValueDescriptions
109 entries.
110
111 \sa QCanDbcFileParser::ValueDescriptions
112*/
113
114/*!
115 \typealias QCanDbcFileParser::MessageValueDescriptions
116
117 This is a type alias for
118 \c {QHash<QtCanBus::UniqueId, SignalValueDescriptions>}.
119
120 The keys of the hash represent message unique ids, and the values of the
121 hash contain the corresponding \l QCanDbcFileParser::SignalValueDescriptions
122 entries.
123
124 \sa QCanDbcFileParser::SignalValueDescriptions
125*/
126
127/*!
128 \enum QCanDbcFileParser::Error
129
130 This enum represents the possible errors that can happen during the parsing
131 of a DBC file.
132
133 \value None No error occurred.
134 \value FileReading An error occurred while opening or reading the file.
135 \value Parsing An error occurred while parsing the content of the file.
136*/
137
138/*!
139 Constructs a DBC file parser.
140*/
141QCanDbcFileParser::QCanDbcFileParser()
142 : d(std::make_unique<QCanDbcFileParserPrivate>())
143{
144}
145
146/*!
147 Destroys this DBC file parser.
148*/
149QCanDbcFileParser::~QCanDbcFileParser() = default;
150
151/*!
152 Parses the file \a fileName. Returns \c true if the parsing completed
153 successfully or \c false otherwise.
154
155 If the parsing completed successfully, call the \l messageDescriptions()
156 method to get the list of all extracted message descriptions.
157
158 If the parsing failed, call the \l error() and \l errorString() methods
159 to get the information about the error.
160
161 Call the \l warnings() method to get the list of warnings that were
162 logged during the parsing.
163
164 \note This method expects the file contents to be encoded in UTF-8. If the
165 file has a different encoding, decode it first, and use \l parseData()
166 to extract the DBC information.
167
168 \sa messageDescriptions(), error(), warnings(), parseData()
169*/
170bool QCanDbcFileParser::parse(const QString &fileName)
171{
172 d->reset();
173 return d->parseFile(fileName);
174}
175
176/*!
177 \overload
178
179 Parses a list of files \a fileNames. Returns \c true if the parsing
180 completed successfully or \c false otherwise.
181
182 If the parsing completed successfully, call the \l messageDescriptions()
183 method to get the list of all extracted message descriptions.
184
185 The parsing stops at the first error. Call the \l error() and
186 \l errorString() methods to get the information about the error.
187
188 Call the \l warnings() method to get the list of warnings that were
189 logged during the parsing.
190
191 \note This method expects the file contents to be encoded in UTF-8. If the
192 file has a different encoding, decode it first, and use \l parseData()
193 to extract the DBC information.
194
195 \sa messageDescriptions(), error(), warnings(), parseData()
196*/
197bool QCanDbcFileParser::parse(const QStringList &fileNames)
198{
199 d->reset();
200 for (const auto &fileName : fileNames) {
201 if (!d->parseFile(fileName))
202 return false;
203 }
204 return true;
205}
206
207/*!
208 \since 6.7
209
210 Parses the input data \a data and returns \c true if the parsing completed
211 successfully or \c false otherwise.
212
213 If the parsing completed successfully, call the \l messageDescriptions()
214 method to get the list of all extracted message descriptions.
215
216 If the parsing failed, call the \l error() and \l errorString() methods
217 to get the information about the error.
218
219 Call the \l warnings() method to get the list of warnings that were
220 logged during the parsing.
221
222 The method expects that \a data is the content of a valid DBC file,
223 properly converted to QStringView.
224
225 Use this method when the input file has an encoding different from UTF-8.
226
227 \code
228 // Read the data from a DBC file with custom encoding
229 const QByteArray initialData = ...;
230 // Convert to UTF-16 using QStringDecoder or some other way
231 const QString decodedData = ...;
232 QCanDbcFileParser parser;
233 const bool result = parser.parseData(decodedData);
234 \endcode
235
236 \sa messageDescriptions(), error(), warnings(), parse()
237*/
238bool QCanDbcFileParser::parseData(QStringView data)
239{
240 d->reset();
241 return d->parseData(data);
242}
243
244/*!
245 Returns the list of message descriptions that were extracted during the
246 last \l parse() call.
247
248 \sa parse(), error()
249*/
250QList<QCanMessageDescription> QCanDbcFileParser::messageDescriptions() const
251{
252 return d->getMessages();
253}
254
255/*!
256 Returns the textual descriptions for signal raw values.
257
258 DBC supports the possibility to provide textual descriptions to signal raw
259 values. If such data exists in the parsed DBC file(s), it can be accessed
260 using this function.
261
262 The textual descriptions are unique for a certain signal within a specific
263 message, so the returned structure contains the information about the
264 message unique id and the signal name, as well as the actual value
265 descriptions.
266
267 \sa QCanDbcFileParser::MessageValueDescriptions,
268 QCanDbcFileParser::SignalValueDescriptions,
269 QCanDbcFileParser::ValueDescriptions
270*/
271QCanDbcFileParser::MessageValueDescriptions QCanDbcFileParser::messageValueDescriptions() const
272{
273 return d->m_valueDescriptions;
274}
275
276/*!
277 Returns the last error which occurred during the parsing.
278
279 \sa errorString(), parse()
280*/
281QCanDbcFileParser::Error QCanDbcFileParser::error() const
282{
283 return d->m_error;
284}
285
286/*!
287 Returns the text representation of the last error which occurred during the
288 parsing or an empty string if there was no error.
289
290 \sa error()
291*/
292QString QCanDbcFileParser::errorString() const
293{
294 return d->m_errorString;
295}
296
297/*!
298 Returns the list of non-critical problems which occurred during the parsing.
299
300 A typical problem can be a malformed message or signal description. In such
301 cases the malformed message or signal is skipped, but the rest of the file
302 can be processed as usual.
303
304 \sa error(), parse()
305*/
306QStringList QCanDbcFileParser::warnings() const
307{
308 return d->m_warnings;
309}
310
311/*!
312 Returns a unique identifier description. DBC protocol always uses the
313 Frame Id as an identifier, and therefore the unique identifier description
314 is always the same.
315
316 Use this method to get an instance of \l QCanUniqueIdDescription and pass
317 it to \l QCanFrameProcessor.
318
319 \sa QCanFrameProcessor::setUniqueIdDescription()
320*/
321QCanUniqueIdDescription QCanDbcFileParser::uniqueIdDescription()
322{
323 QCanUniqueIdDescription desc;
324 desc.setSource(QtCanBus::DataSource::FrameId);
325 desc.setEndian(QSysInfo::Endian::LittleEndian);
326 desc.setStartBit(0);
327 desc.setBitLength(29); // for both extended and normal frame id
328 return desc;
329}
330
331/* QCanDbcFileParserPrivate implementation */
332
333using namespace Qt::StringLiterals;
334
335// signal name with whitespaces is invalid in DBC, so we can safely use it
336// for internal purposes
337static const auto kQtDummySignal = u"Qt Dummy Signal"_s;
338
339static constexpr auto kMessageDef = "BO_ "_L1;
340static constexpr auto kSignalDef = "SG_ "_L1;
341static constexpr auto kSigValTypeDef = "SIG_VALTYPE_ "_L1;
342static constexpr auto kCommentDef = "CM_ "_L1;
343static constexpr auto kExtendedMuxDef = "SG_MUL_VAL_ "_L1;
344static constexpr auto kValDef = "VAL_ "_L1;
345
346static constexpr auto kUnsignedIntRegExp = "\\d+"_L1;
347static constexpr auto kDoubleRegExp = "[+-]?\\d+((.\\d*)?([eE][+-]?\\d+)?)?"_L1;
348static constexpr auto kDbcIdentRegExp = "[_[:alpha:]][_[:alnum:]]+"_L1;
349static constexpr auto kOneOrMoreSpaceRegExp = "[ ]+"_L1;
350static constexpr auto kMaybeSpaceRegExp = "[ ]*"_L1;
351static constexpr auto kMuxIndicatorRegExp = "M|m\\d+M?"_L1;
352static constexpr auto kByteOrderRegExp = "0|1"_L1;
353static constexpr auto kValueTypeRegExp = "\\+|\\-"_L1;
354// The pattern matches all printable characters, except double-quote (") and backslash (\).
355static constexpr auto kCharStrRegExp = "((?![\\\"\\\\])\\P{Cc})*"_L1;
356
357void QCanDbcFileParserPrivate::reset()
358{
359 m_fileName.clear();
360 m_error = QCanDbcFileParser::Error::None;
361 m_errorString.clear();
362 m_warnings.clear();
363 m_lineOffset = 0;
364 m_isProcessingMessage = false;
365 m_seenExtraData = false;
366 m_currentMessage = {};
367 m_messageDescriptions.clear();
368 m_valueDescriptions.clear();
369}
370
371/*!
372 \internal
373 Returns \c false only in case of hard error. Returns \c true even if some
374 warnings occurred during parsing.
375*/
376bool QCanDbcFileParserPrivate::parseFile(const QString &fileName)
377{
378 QFile f(fileName);
379 if (!f.open(flags: QIODevice::ReadOnly)) {
380 m_error = QCanDbcFileParser::Error::FileReading;
381 m_errorString = f.errorString();
382 return false;
383 }
384 m_fileName = fileName;
385 m_seenExtraData = false;
386
387 while (!f.atEnd()) {
388 const QString str = QString::fromUtf8(ba: f.readLine().trimmed());
389 if (!processLine(line: {str.constData(), str.size()})) // also sets the error properly
390 return false;
391 }
392 addCurrentMessage(); // check if we need to add the message
393 // now when we parsed the whole file, we can verify the signal multiplexing
394 postProcessSignalMultiplexing();
395
396 return true;
397}
398
399struct ReadData
400{
401 qsizetype index;
402 QStringView result;
403};
404
405static ReadData readUntilNewline(QStringView in, qsizetype from)
406{
407 const qsizetype idx = in.indexOf(c: '\n'_L1, from);
408
409 return (idx == -1) ? ReadData{.index: idx, .result: in.sliced(pos: from).trimmed()}
410 : ReadData{.index: idx, .result: in.sliced(pos: from, n: idx - from).trimmed()};
411}
412
413/*!
414 \internal
415 The implementation basically copies parseFile(), including all
416 post-processing. The only difference is in extracting the data.
417*/
418bool QCanDbcFileParserPrivate::parseData(QStringView data)
419{
420 if (data.isEmpty()) {
421 m_error = QCanDbcFileParser::Error::Parsing;
422 m_errorString = QObject::tr(s: "Empty input data.");
423 return false;
424 }
425 m_seenExtraData = false;
426 qsizetype from = 0;
427 while (true) {
428 const auto [idx, sv] = readUntilNewline(in: data, from);
429 if (!processLine(line: sv))
430 return false;
431 if (idx == -1) // reached the end of the string
432 break;
433 from = idx + 1;
434 }
435 addCurrentMessage();
436 postProcessSignalMultiplexing();
437 return true;
438}
439
440/*!
441 \internal
442 Returns \c false only in case of hard error. Returns \c true even if some
443 warnings occurred during parsing.
444*/
445bool QCanDbcFileParserPrivate::processLine(const QStringView line)
446{
447 QStringView data = line;
448 m_lineOffset = 0;
449
450 auto handleParsingError = [this](QLatin1StringView section) {
451 m_error = QCanDbcFileParser::Error::Parsing;
452 m_errorString = !m_fileName.isEmpty()
453 ? QObject::tr(s: "Failed to parse file %1. Unexpected position "
454 "of %2 section.").arg(args&: m_fileName, args&: section)
455 : QObject::tr(s: "Failed to parse input data. Unexpected position "
456 "of %1 section.").arg(a: section);
457 };
458
459 if (data.startsWith(s: kMessageDef)) {
460 if (m_seenExtraData) {
461 // Unexpected position of message description
462 handleParsingError(kMessageDef);
463 return false;
464 }
465 addCurrentMessage();
466 if (!parseMessage(data))
467 return false;
468 }
469 // signal definitions can be on the same line as message definition,
470 // or on a separate line
471 data = data.sliced(pos: m_lineOffset).trimmed();
472 while (data.startsWith(s: kSignalDef)) {
473 if (!m_isProcessingMessage || m_seenExtraData) {
474 // Unexpected position of signal description
475 handleParsingError(kSignalDef);
476 return false;
477 }
478 if (!parseSignal(data))
479 return false;
480 data = data.sliced(pos: m_lineOffset).trimmed();
481 }
482 // If we detect one of the following lines, then message description is
483 // finished. We also assume that we can have only one key at each line.
484 if (data.startsWith(s: kSigValTypeDef)) {
485 m_seenExtraData = true;
486 addCurrentMessage();
487 parseSignalType(data);
488 } else if (data.startsWith(s: kCommentDef)) {
489 m_seenExtraData = true;
490 addCurrentMessage();
491 parseComment(data);
492 } else if (data.startsWith(s: kExtendedMuxDef)) {
493 m_seenExtraData = true;
494 addCurrentMessage();
495 parseExtendedMux(data);
496 } else if (data.startsWith(s: kValDef)) {
497 m_seenExtraData = true;
498 addCurrentMessage();
499 parseValueDescriptions(data);
500 }
501 return true;
502}
503
504static std::optional<QtCanBus::UniqueId> extractUniqueId(QStringView view)
505{
506 bool ok = false;
507 const uint value = view.toUInt(ok: &ok);
508 if (ok)
509 return QtCanBus::UniqueId{value & 0x1FFFFFFF};
510 return std::nullopt;
511}
512
513/*!
514 \internal
515 Returns \c false only in case of hard error. Returns \c true even if some
516 warnings occurred during parsing.
517*/
518bool QCanDbcFileParserPrivate::parseMessage(const QStringView data)
519{
520 // The regexp matches the following definition:
521 // BO_ message_id message_name ':' message_size transmitter
522 // also considering the fact that spaces around ':' seem to be optional, and
523 // allowing more than one space between parts.
524
525 // %1 - messageDef
526 // %2 - maybeSpace
527 // %3 - unsignedInt
528 // %4 - oneOrMoreSpace
529 // %5 - DbcIdentifier
530 static const QString regExStr =
531 "%1%2(?<messageId>%3)%4(?<name>%5)%2:%2(?<size>%3)%4(?<transmitter>%5)"_L1.
532 arg(args: kMessageDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
533 args: kDbcIdentRegExp);
534 static const QRegularExpression messageRegExp(regExStr);
535
536 m_isProcessingMessage = false;
537 const auto match = messageRegExp.matchView(subjectView: data);
538 if (match.hasMatch()) {
539 m_currentMessage = extractMessage(match);
540 // can't check for isValid() here, because demands signal descriptions
541 if (!m_currentMessage.name().isEmpty()) {
542 m_isProcessingMessage = true;
543 } else {
544 addWarning(warning: QObject::tr(s: "Failed to parse message description from "
545 "string %1").arg(a: data));
546 }
547 m_lineOffset = match.capturedEnd(nth: 0);
548 } else {
549 addWarning(warning: QObject::tr(s: "Failed to find message description in string %1").arg(a: data));
550 m_lineOffset = data.size(); // skip this string
551 }
552 return true;
553}
554
555QCanMessageDescription
556QCanDbcFileParserPrivate::extractMessage(const QRegularExpressionMatch &match)
557{
558 Q_ASSERT(match.hasMatch());
559 QCanMessageDescription desc;
560 desc.setName(match.captured(name: u"name"_s));
561
562 const auto id = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
563 if (id.has_value()) {
564 desc.setUniqueId(id.value());
565 } else {
566 addWarning(warning: QObject::tr(s: "Failed to parse frame id for message %1").arg(a: desc.name()));
567 return {};
568 }
569
570 bool ok = false;
571 const auto size = match.capturedView(name: u"size"_s).toUInt(ok: &ok);
572 if (ok) {
573 desc.setSize(size);
574 } else {
575 addWarning(warning: QObject::tr(s: "Failed to parse size for message %1").arg(a: desc.name()));
576 return {};
577 }
578
579 desc.setTransmitter(match.captured(name: u"transmitter"_s));
580
581 return desc;
582}
583
584/*!
585 \internal
586 Returns \c false only in case of hard error. Returns \c true even if some
587 warnings occurred during parsing.
588*/
589bool QCanDbcFileParserPrivate::parseSignal(const QStringView data)
590{
591 // The regexp should match the following pattern:
592 // SG_ signal_name multiplexer_indicator : start_bit |
593 // signal_size @ byte_order value_type ( factor , offset )
594 // [ minimum | maximum ] unit receiver {, receiver}
595 // We also need to consider the fact that some of the spaces might be
596 // optional, and we can potentially allow more spaces between parts.
597 // Note that the end of the signal description can contain multiple
598 // receivers. The regexp is supposed to extract all of them, but we use
599 // only the first one for now.
600
601 // %1 - SignalDef
602 // %2 - MaybeSpace
603 // %3 - DbcIdentifier
604 // %4 - OneOrMoreSpace
605 // %5 - MuxIndicator
606 // %6 - unsignedInt
607 // %7 - byteOrder
608 // %8 - valueType
609 // %9 - double
610 // %10 - charStr
611 static const QString regExStr =
612 "%1%2(?<name>%3)(%4(?<mux>%5))?%2:%2(?<startBit>%6)%2\\|%2(?<sigSize>%6)%2@%2"
613 "(?<byteOrder>%7)%2(?<valueType>%8)%4\\(%2(?<factor>%9)%2,%2(?<offset>%9)%2\\)"
614 "%4\\[%2(?<min>%9)%2\\|%2(?<max>%9)%2\\]%4\"(?<unit>%10)\""
615 "%4(?<receiver>%3)(%2,%2%3)*"_L1.
616 arg(args: kSignalDef, args: kMaybeSpaceRegExp, args: kDbcIdentRegExp, args: kOneOrMoreSpaceRegExp,
617 args: kMuxIndicatorRegExp, args: kUnsignedIntRegExp, args: kByteOrderRegExp, args: kValueTypeRegExp,
618 args: kDoubleRegExp, args: kCharStrRegExp);
619 static const QRegularExpression signalRegExp(regExStr);
620
621 const auto match = signalRegExp.matchView(subjectView: data);
622 if (match.hasMatch()) {
623 QCanSignalDescription desc = extractSignal(match);
624
625 if (desc.isValid())
626 m_currentMessage.addSignalDescription(description: desc);
627 else
628 addWarning(warning: QObject::tr(s: "Failed to parse signal description from string %1").arg(a: data));
629
630 m_lineOffset = match.capturedEnd(nth: 0);
631 } else {
632 addWarning(warning: QObject::tr(s: "Failed to find signal description in string %1").arg(a: data));
633 m_lineOffset = data.size(); // skip this string
634 }
635 return true;
636}
637
638QCanSignalDescription QCanDbcFileParserPrivate::extractSignal(const QRegularExpressionMatch &match)
639{
640 Q_ASSERT(match.hasMatch());
641 QCanSignalDescription desc;
642 desc.setName(match.captured(name: u"name"_s));
643
644 bool ok = false;
645
646 if (match.hasCaptured(name: u"mux"_s)) {
647 const auto muxStr = match.capturedView(name: u"mux"_s);
648 if (muxStr == u"M"_s) {
649 desc.setMultiplexState(QtCanBus::MultiplexState::MultiplexorSwitch);
650 } else if (muxStr.endsWith(s: u"M"_s, cs: Qt::CaseSensitive)) {
651 desc.setMultiplexState(QtCanBus::MultiplexState::SwitchAndSignal);
652 const auto val = muxStr.sliced(pos: 1, n: muxStr.size() - 2).toUInt(ok: &ok);
653 if (!ok) {
654 addWarning(warning: QObject::tr(s: "Failed to parse multiplexor value for signal %1").
655 arg(a: desc.name()));
656 return {};
657 }
658 // We have the value, but we do not really know the multiplexor
659 // switch name. To know it, we potentially need to parse all signals
660 // for the message. So for now we just create a dummy entry, and
661 // the actual signal name will be updated later;
662 desc.addMultiplexSignal(name: kQtDummySignal, value: val);
663 } else {
664 desc.setMultiplexState(QtCanBus::MultiplexState::MultiplexedSignal);
665 const auto val = muxStr.sliced(pos: 1).toUInt(ok: &ok);
666 if (!ok) {
667 addWarning(warning: QObject::tr(s: "Failed to parse multiplexor value for signal %1").
668 arg(a: desc.name()));
669 return {};
670 }
671 // Same as above
672 desc.addMultiplexSignal(name: kQtDummySignal, value: val);
673 }
674 }
675
676 const uint startBit = match.capturedView(name: u"startBit"_s).toUInt(ok: &ok);
677 if (ok) {
678 desc.setStartBit(startBit);
679 } else {
680 addWarning(warning: QObject::tr(s: "Failed to parse start bit for signal %1").arg(a: desc.name()));
681 return {};
682 }
683
684 const uint bitLength = match.capturedView(name: u"sigSize"_s).toUInt(ok: &ok);
685 if (ok) {
686 desc.setBitLength(bitLength);
687 } else {
688 addWarning(warning: QObject::tr(s: "Failed to parse bit length for signal %1").arg(a: desc.name()));
689 return {};
690 }
691
692 // 0 = BE; 1 = LE
693 const auto endian = match.capturedView(name: u"byteOrder"_s) == u"0"_s
694 ? QSysInfo::Endian::BigEndian : QSysInfo::Endian::LittleEndian;
695 desc.setDataEndian(endian);
696
697 // + = unsigned; - = signed
698 const auto dataFormat = match.capturedView(name: u"valueType"_s) == u"+"_s
699 ? QtCanBus::DataFormat::UnsignedInteger : QtCanBus::DataFormat::SignedInteger;
700 desc.setDataFormat(dataFormat);
701
702 const double factor = match.capturedView(name: u"factor"_s).toDouble(ok: &ok);
703 if (ok) {
704 desc.setFactor(factor);
705 } else {
706 addWarning(warning: QObject::tr(s: "Failed to parse factor for signal %1").arg(a: desc.name()));
707 return {};
708 }
709
710 const double offset = match.capturedView(name: u"offset"_s).toDouble(ok: &ok);
711 if (ok) {
712 desc.setOffset(offset);
713 } else {
714 addWarning(warning: QObject::tr(s: "Failed to parse offset for signal %1").arg(a: desc.name()));
715 return {};
716 }
717
718 const double min = match.capturedView(name: u"min"_s).toDouble(ok: &ok);
719 if (ok) {
720 const double max = match.capturedView(name: u"max"_s).toDouble(ok: &ok);
721 if (ok)
722 desc.setRange(minimum: min, maximum: max);
723 }
724 if (!ok) {
725 addWarning(warning: QObject::tr(s: "Failed to parse value range from signal %1").arg(a: desc.name()));
726 return {};
727 }
728
729 desc.setPhysicalUnit(match.captured(name: u"unit"_s));
730 desc.setReceiver(match.captured(name: u"receiver"_s));
731
732 return desc;
733}
734
735void QCanDbcFileParserPrivate::parseSignalType(const QStringView data)
736{
737 // The regexp should match the following pattern:
738 // SIG_VALTYPE_ message_id signal_name signal_extended_value_type ;
739 // We also need to consider the fact that we can potentially allow more
740 // spaces between parts.
741
742 // %1 sigValTypeDef
743 // %2 maybeSpace
744 // %3 unsignedInt
745 // %4 oneOrMoreSpace
746 // %5 DbcIdentifier
747 const QString regExStr =
748 "%1%2(?<messageId>%3)%4(?<sigName>%5)%2:%2(?<type>%3)%2;"_L1.
749 arg(args: kSigValTypeDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp,
750 args: kOneOrMoreSpaceRegExp, args: kDbcIdentRegExp);
751 const QRegularExpression sigValTypeRegEx(regExStr);
752
753 const auto match = sigValTypeRegEx.matchView(subjectView: data);
754 if (!match.hasMatch()) {
755 m_lineOffset = data.size();
756 addWarning(warning: QObject::tr(s: "Failed to find signal value type description in string %1").
757 arg(a: data));
758 return;
759 }
760
761 m_lineOffset = match.capturedEnd(nth: 0);
762
763 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
764 if (!uidOptional) {
765 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
766 return;
767 }
768
769 const QtCanBus::UniqueId uid = uidOptional.value();
770 auto msgDesc = m_messageDescriptions.value(key: uid);
771 if (msgDesc.isValid()) {
772 const QString sigName = match.captured(name: u"sigName"_s);
773 auto sigDesc = msgDesc.signalDescriptionForName(name: sigName);
774 if (sigDesc.isValid()) {
775 bool ok = false;
776 const auto type = match.capturedView(name: u"type").toUInt(ok: &ok);
777 if (ok) {
778 bool sigDescChanged = false;
779 switch (type) {
780 case 0: /* signed or unsigned integer */
781 // do nothing, as we already have signed/unsinged integer
782 // based on "SG_ " string
783 break;
784 case 1: /* 32-bit IEEE-float */
785 sigDesc.setDataFormat(QtCanBus::DataFormat::Float);
786 sigDesc.setBitLength(32);
787 sigDescChanged = true;
788 break;
789 case 2: /* 64-bit IEEE-double */
790 sigDesc.setDataFormat(QtCanBus::DataFormat::Double);
791 sigDesc.setBitLength(64);
792 sigDescChanged = true;
793 break;
794 default:
795 // invalid value
796 break;
797 }
798 if (sigDescChanged) {
799 msgDesc.addSignalDescription(description: sigDesc);
800 m_messageDescriptions.insert(key: msgDesc.uniqueId(), value: msgDesc);
801 }
802 } else {
803 addWarning(warning: QObject::tr(s: "Failed to parse data type from string %1").arg(a: data));
804 }
805 } else {
806 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
807 "Skipping string %2").arg(args: sigName, args: data));
808 }
809 } else {
810 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
811 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
812 }
813}
814
815void QCanDbcFileParserPrivate::parseComment(const QStringView data)
816{
817 // The comment for message or signal description is represented by the
818 // following pattern:
819 // CM_ (BO_ message_id char_string | SG_ message_id signal_name char_string);
820
821 // %1 commentDef
822 // %2 maybeSpace
823 // %3 messageDef
824 // %4 signalDef
825 // %5 oneOrMoreSpace
826 // %6 unsignedInt
827 // %7 DbcIdentifier
828 // %8 charStr
829 const QString regExStr =
830 "%1%2(?<type>(%3|%4))%2(?<messageId>%6)%5((?<sigName>%7)%5)?\"(?<comment>%8)\"%2;"_L1.
831 arg(args: kCommentDef, args: kMaybeSpaceRegExp, args: kMessageDef, args: kSignalDef, args: kOneOrMoreSpaceRegExp,
832 args: kUnsignedIntRegExp, args: kDbcIdentRegExp, args: kCharStrRegExp);
833 const QRegularExpression commentRegExp(regExStr);
834
835 const auto match = commentRegExp.matchView(subjectView: data);
836 if (!match.hasMatch()) {
837 // no warning here, as we ignore some "general" comments, and parse only
838 // comments related to messages and signals
839 m_lineOffset = data.size();
840 return;
841 }
842
843 m_lineOffset = match.capturedEnd(nth: 0);
844
845 const auto type = match.capturedView(name: u"type"_s);
846
847 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
848 if (!uidOptional) {
849 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
850 return;
851 }
852
853 const QtCanBus::UniqueId uid = uidOptional.value();
854 auto messageDesc = m_messageDescriptions.value(key: uid);
855 if (!messageDesc.isValid()) {
856 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
857 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
858 return;
859 }
860
861 if (type == kMessageDef) {
862 const QString comment = match.captured(name: u"comment"_s);
863 messageDesc.setComment(comment);
864 m_messageDescriptions.insert(key: uid, value: messageDesc);
865 } else if (type == kSignalDef) {
866 const QString sigName = match.captured(name: u"sigName"_s);
867 auto signalDesc = messageDesc.signalDescriptionForName(name: sigName);
868 if (signalDesc.isValid()) {
869 const QString comment = match.captured(name: u"comment"_s);
870 signalDesc.setComment(comment);
871 messageDesc.addSignalDescription(description: signalDesc);
872 m_messageDescriptions.insert(key: uid, value: messageDesc);
873 } else {
874 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
875 "Skipping string %2").arg(args: sigName, args: data));
876 }
877 }
878}
879
880void QCanDbcFileParserPrivate::parseExtendedMux(const QStringView data)
881{
882 // The extended multiplexing is defined by the following pattern:
883 // SG_MUL_VAL_ message_id multiplexed_signal_name
884 // multiplexor_switch_name multiplexor_value_ranges ;
885 // Here multiplexor_value_ranges consists of multiple ranges, separated
886 // by a whitespace, and one range is defined as follows:
887 // multiplexor_value_range = unsigned_integer - unsigned_integer
888
889 // %1 extendedMuxDef
890 // %2 maybeSpace
891 // %3 unsignedInt
892 // %4 oneOrMoreSpace
893 // %5 DbcIdentifier
894 const QString regExStr =
895 "%1%2(?<messageId>%3)%4(?<multiplexedSignal>%5)%4(?<multiplexorSwitch>%5)%4"
896 "(?<firstRange>%3%2-%2%3)(%2,%2%3%2-%2%3)*%2;"_L1.
897 arg(args: kExtendedMuxDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
898 args: kDbcIdentRegExp);
899 const QRegularExpression extendedMuxRegExp(regExStr);
900
901 const auto match = extendedMuxRegExp.matchView(subjectView: data);
902 if (!match.hasMatch()) {
903 m_lineOffset = data.size();
904 addWarning(warning: QObject::tr(s: "Failed to find extended multiplexing description in string %1").
905 arg(a: data));
906 return;
907 }
908
909 m_lineOffset = match.capturedEnd(nth: 0);
910
911 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
912 if (!uidOptional) {
913 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
914 return;
915 }
916
917 const QtCanBus::UniqueId uid = uidOptional.value();
918 auto messageDesc = m_messageDescriptions.value(key: uid);
919 if (!messageDesc.isValid()) {
920 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
921 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
922 return;
923 }
924
925 const QString multiplexedSignalName = match.captured(name: u"multiplexedSignal"_s);
926 const QString multiplexorSwitchName = match.captured(name: u"multiplexorSwitch"_s);
927
928 auto multiplexedSignal = messageDesc.signalDescriptionForName(name: multiplexedSignalName);
929 auto multiplexorSwitch = messageDesc.signalDescriptionForName(name: multiplexorSwitchName);
930
931 if (!multiplexedSignal.isValid() || !multiplexorSwitch.isValid()) {
932 const QString invalidName = multiplexedSignal.isValid() ? multiplexorSwitchName
933 : multiplexedSignalName;
934 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
935 "Skipping string %2").arg(args: invalidName, args: data));
936 return;
937 }
938
939 auto signalRanges = multiplexedSignal.multiplexSignals();
940 signalRanges.remove(key: kQtDummySignal); // dummy signal not needed anymore
941
942 QCanSignalDescription::MultiplexValues rangeValues;
943 auto rangeView = match.capturedView(name: u"firstRange"_s);
944 const auto sepIdx = rangeView.indexOf(c: u'-');
945 if (sepIdx != -1) {
946 const auto min = rangeView.first(n: sepIdx).trimmed().toUInt();
947 const auto max = rangeView.sliced(pos: sepIdx + 1).trimmed().toUInt();
948 rangeValues.push_back(t: {.minimum: min, .maximum: max});
949 }
950
951 // We can have an arbitrary amount of ranges, so we can't use capture groups
952 // to capture them. But we know that they follow a specific pattern (because
953 // the full string matched the regexp). So we need to parse the rest of the
954 // matched string manually
955 const auto totalEnd = match.capturedEnd(nth: 0); // including the ';'
956 const auto firstRangeEnd = match.capturedEnd(name: u"firstRange"_s);
957 const auto len = totalEnd - firstRangeEnd - 1;
958 if (len > 0) {
959 const auto otherRangesView = data.sliced(pos: firstRangeEnd, n: len).trimmed();
960 const QStringTokenizer parts = otherRangesView.tokenize(needle: u',', flags: Qt::SkipEmptyParts);
961 for (const QStringView range : parts) {
962 const auto sepIdx = range.indexOf(c: u'-');
963 if (sepIdx != -1) {
964 const auto min = range.first(n: sepIdx).trimmed().toUInt();
965 const auto max = range.sliced(pos: sepIdx + 1).trimmed().toUInt();
966 rangeValues.push_back(t: {.minimum: min, .maximum: max});
967 }
968 }
969 }
970
971 if (!rangeValues.isEmpty())
972 signalRanges.insert(key: multiplexorSwitchName, value: rangeValues);
973 else
974 signalRanges.remove(key: multiplexorSwitchName);
975
976 // update the value
977 multiplexedSignal.setMultiplexSignals(signalRanges);
978 messageDesc.addSignalDescription(description: multiplexedSignal);
979 m_messageDescriptions.insert(key: uid, value: messageDesc);
980}
981
982void QCanDbcFileParserPrivate::parseValueDescriptions(const QStringView data)
983{
984 // The regexp should match the following pattern:
985 // VAL_ message_id signal_name { value_description };
986 // Here the value_description is defined as follows
987 // value_description = unsigned_int char_string
988
989 // %1 valDef
990 // %2 maybeSpace
991 // %3 unsignedInt
992 // %4 oneOrMoreSpace
993 // %5 DbcIdentifier
994 // %6 charStr
995 const QString regExStr =
996 "%1%2(?<messageId>%3)%4(?<signalName>%5)(%4%3%4\"(%6)\")+%2;"_L1.
997 arg(args: kValDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
998 args: kDbcIdentRegExp, args: kCharStrRegExp);
999
1000 const QRegularExpression valueDescRegExp(regExStr);
1001
1002 const auto match = valueDescRegExp.matchView(subjectView: data);
1003 if (!match.hasMatch()) {
1004 m_lineOffset = data.size();
1005 addWarning(warning: QObject::tr(s: "Failed to parse value description from string %1").arg(a: data));
1006 return;
1007 }
1008
1009 m_lineOffset = match.capturedEnd(nth: 0);
1010
1011 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
1012 if (!uidOptional) {
1013 addWarning(warning: QObject::tr(s: "Failed to parse value description from string %1").arg(a: data));
1014 return;
1015 }
1016
1017 const QtCanBus::UniqueId uid = uidOptional.value();
1018 // Check if the message exists
1019 const auto messageDesc = m_messageDescriptions.value(key: uid);
1020 if (!messageDesc.isValid()) {
1021 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
1022 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
1023 return;
1024 }
1025
1026 // Check if the signal exists within the message
1027 const QString signalName = match.captured(name: u"signalName"_s);
1028 if (!messageDesc.signalDescriptionForName(name: signalName).isValid()) {
1029 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
1030 "Skipping string %2").arg(args: signalName, args: data));
1031 return;
1032 }
1033
1034 // We can have an arbitrary amount of value descriptions, so we can't use
1035 // capture groups to capture them. But we know that they follow a specific
1036 // pattern (because the full string matched the regexp). So we need to parse
1037 // the rest of the matched string manually
1038 const auto totalEnd = match.capturedEnd(nth: 0); // including the ';'
1039 const auto signalNameEnd = match.capturedEnd(name: u"signalName"_s);
1040 const auto len = totalEnd - signalNameEnd - 1;
1041 if (len > 0) {
1042 auto signalDescriptionsView = data.sliced(pos: signalNameEnd, n: len).trimmed();
1043 while (signalDescriptionsView.size()) {
1044 const auto spacePos = signalDescriptionsView.indexOf(c: u' ');
1045 if (spacePos == -1)
1046 break;
1047 bool ok = false;
1048 const auto value = signalDescriptionsView.sliced(pos: 0, n: spacePos).toUInt(ok: &ok);
1049 if (!ok)
1050 break;
1051 const auto firstQuotePos = signalDescriptionsView.indexOf(c: u'"', from: spacePos + 1);
1052 if (firstQuotePos == -1)
1053 break;
1054 const auto nextQuotePos = signalDescriptionsView.indexOf(c: u'"', from: firstQuotePos + 1);
1055 if (nextQuotePos == -1)
1056 break;
1057 const auto description = signalDescriptionsView.sliced(
1058 pos: firstQuotePos + 1, n: nextQuotePos - firstQuotePos - 1);
1059
1060 m_valueDescriptions[uid][signalName].insert(key: value, value: description.toString());
1061 signalDescriptionsView = signalDescriptionsView.sliced(pos: nextQuotePos + 1).trimmed();
1062 }
1063 }
1064}
1065
1066void QCanDbcFileParserPrivate::postProcessSignalMultiplexing()
1067{
1068 // For the case of simple multiplexing we need to do the following for
1069 // every message description:
1070 // 1. Find the multiplexor signal
1071 // 2. Replace all kQtDummySignal entries with the name of the multiplexor
1072 // 3. While doing so, check if we have any signals with type
1073 // SwitchAndSignal. This will mean that extended multiplexing is used
1074 // 4. Also detect conditions when we have more than one multiplexor signal.
1075 // This is an error as well, and message description should be discarded.
1076
1077 QList<QtCanBus::UniqueId> uidsToRemove;
1078
1079 for (auto &messageDesc : m_messageDescriptions) {
1080 bool useExtendedMux = false;
1081 QString multiplexorSignal;
1082 auto &signalDescriptions = QCanMessageDescriptionPrivate::get(desc: messageDesc)->messageSignals;
1083 for (const auto &signalDesc : std::as_const(t&: signalDescriptions)) {
1084 if (signalDesc.multiplexState() == QtCanBus::MultiplexState::MultiplexorSwitch) {
1085 if (multiplexorSignal.isEmpty()) {
1086 multiplexorSignal = signalDesc.name();
1087 } else {
1088 // invalid config
1089 multiplexorSignal.clear();
1090 uidsToRemove.push_back(t: messageDesc.uniqueId());
1091 break;
1092 }
1093 } else if (signalDesc.multiplexState() == QtCanBus::MultiplexState::SwitchAndSignal) {
1094 // extended multiplexing
1095 useExtendedMux = true;
1096 }
1097 }
1098 if (!useExtendedMux && !multiplexorSignal.isEmpty()) {
1099 // iterate through all signal descriptions and update kQtDummySignal
1100 for (auto &signalDesc : signalDescriptions) {
1101 if (signalDesc.multiplexState() == QtCanBus::MultiplexState::MultiplexedSignal) {
1102 auto &muxValues = QCanSignalDescriptionPrivate::get(desc: signalDesc)->muxSignals;
1103 auto val = muxValues.value(key: kQtDummySignal);
1104 muxValues.remove(key: kQtDummySignal);
1105 muxValues.insert(key: multiplexorSignal, value: val);
1106 }
1107 }
1108 } else if (useExtendedMux) {
1109 // Iterate through all signal descriptions and check that we do
1110 // not have any kQtDummySignal entries. It such entry exists, this
1111 // means that there were errors while parsing extended multiplexing
1112 // table, and this message description is invalid
1113 for (const auto &signalDesc : std::as_const(t&: signalDescriptions)) {
1114 const auto muxSignals = signalDesc.multiplexSignals();
1115 if (muxSignals.contains(key: kQtDummySignal)) {
1116 uidsToRemove.push_back(t: messageDesc.uniqueId());
1117 break;
1118 }
1119 }
1120 }
1121 }
1122
1123 for (const auto &uid : std::as_const(t&: uidsToRemove)) {
1124 m_messageDescriptions.remove(key: uid);
1125 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped because "
1126 "it has invalid multiplexing description.").
1127 arg(a: qToUnderlying(e: uid)));
1128 }
1129}
1130
1131void QCanDbcFileParserPrivate::addWarning(QString &&warning)
1132{
1133 m_warnings.emplace_back(args&: warning);
1134}
1135
1136void QCanDbcFileParserPrivate::addCurrentMessage()
1137{
1138 if (m_isProcessingMessage) {
1139 auto uid = m_currentMessage.uniqueId();
1140 if (!m_currentMessage.isValid()) {
1141 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped "
1142 "because it's not valid.").arg(a: qToUnderlying(e: uid)));
1143 } else if (m_messageDescriptions.contains(key: uid)) {
1144 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped "
1145 "because such unique id is already used.").
1146 arg(a: qToUnderlying(e: uid)));
1147 } else {
1148 m_messageDescriptions.insert(key: uid, value: m_currentMessage);
1149 }
1150 m_currentMessage = {};
1151 m_isProcessingMessage = false;
1152 }
1153}
1154
1155QList<QCanMessageDescription> QCanDbcFileParserPrivate::getMessages() const
1156{
1157 return QList<QCanMessageDescription>(m_messageDescriptions.cbegin(),
1158 m_messageDescriptions.cend());
1159}
1160
1161QT_END_NAMESPACE
1162

source code of qtserialbus/src/serialbus/qcandbcfileparser.cpp