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 \sa messageDescriptions(), error(), warnings()
165*/
166bool QCanDbcFileParser::parse(const QString &fileName)
167{
168 d->reset();
169 return d->parseFile(fileName);
170}
171
172/*!
173 \overload
174
175 Parses a list of files \a fileNames. Returns \c true if the parsing
176 completed successfully or \c false otherwise.
177
178 If the parsing completed successfully, call the \l messageDescriptions()
179 method to get the list of all extracted message descriptions.
180
181 The parsing stops at the first error. Call the \l error() and
182 \l errorString() methods to get the information about the error.
183
184 Call the \l warnings() method to get the list of warnings that were
185 logged during the parsing.
186
187 \sa messageDescriptions(), error(), warnings()
188*/
189bool QCanDbcFileParser::parse(const QStringList &fileNames)
190{
191 d->reset();
192 for (const auto &fileName : fileNames) {
193 if (!d->parseFile(fileName))
194 return false;
195 }
196 return true;
197}
198
199/*!
200 Returns the list of message descriptions that were extracted during the
201 last \l parse() call.
202
203 \sa parse(), error()
204*/
205QList<QCanMessageDescription> QCanDbcFileParser::messageDescriptions() const
206{
207 return d->getMessages();
208}
209
210/*!
211 Returns the textual descriptions for signal raw values.
212
213 DBC supports the possibility to provide textual descriptions to signal raw
214 values. If such data exists in the parsed DBC file(s), it can be accessed
215 using this function.
216
217 The textual descriptions are unique for a certain signal within a specific
218 message, so the returned structure contains the information about the
219 message unique id and the signal name, as well as the actual value
220 descriptions.
221
222 \sa QCanDbcFileParser::MessageValueDescriptions,
223 QCanDbcFileParser::SignalValueDescriptions,
224 QCanDbcFileParser::ValueDescriptions
225*/
226QCanDbcFileParser::MessageValueDescriptions QCanDbcFileParser::messageValueDescriptions() const
227{
228 return d->m_valueDescriptions;
229}
230
231/*!
232 Returns the last error which occurred during the parsing.
233
234 \sa errorString(), parse()
235*/
236QCanDbcFileParser::Error QCanDbcFileParser::error() const
237{
238 return d->m_error;
239}
240
241/*!
242 Returns the text representation of the last error which occurred during the
243 parsing or an empty string if there was no error.
244
245 \sa error()
246*/
247QString QCanDbcFileParser::errorString() const
248{
249 return d->m_errorString;
250}
251
252/*!
253 Returns the list of non-critical problems which occurred during the parsing.
254
255 A typical problem can be a malformed message or signal description. In such
256 cases the malformed message or signal is skipped, but the rest of the file
257 can be processed as usual.
258
259 \sa error(), parse()
260*/
261QStringList QCanDbcFileParser::warnings() const
262{
263 return d->m_warnings;
264}
265
266/*!
267 Returns a unique identifier description. DBC protocol always uses the
268 Frame Id as an identifier, and therefore the unique identifier description
269 is always the same.
270
271 Use this method to get an instance of \l QCanUniqueIdDescription and pass
272 it to \l QCanFrameProcessor.
273
274 \sa QCanFrameProcessor::setUniqueIdDescription()
275*/
276QCanUniqueIdDescription QCanDbcFileParser::uniqueIdDescription()
277{
278 QCanUniqueIdDescription desc;
279 desc.setSource(QtCanBus::DataSource::FrameId);
280 desc.setEndian(QSysInfo::Endian::LittleEndian);
281 desc.setStartBit(0);
282 desc.setBitLength(29); // for both extended and normal frame id
283 return desc;
284}
285
286/* QCanDbcFileParserPrivate implementation */
287
288using namespace Qt::StringLiterals;
289
290// signal name with whitespaces is invalid in DBC, so we can safely use it
291// for internal purposes
292static const auto kQtDummySignal = u"Qt Dummy Signal"_s;
293
294static constexpr auto kMessageDef = "BO_ "_L1;
295static constexpr auto kSignalDef = "SG_ "_L1;
296static constexpr auto kSigValTypeDef = "SIG_VALTYPE_ "_L1;
297static constexpr auto kCommentDef = "CM_ "_L1;
298static constexpr auto kExtendedMuxDef = "SG_MUL_VAL_ "_L1;
299static constexpr auto kValDef = "VAL_ "_L1;
300
301static constexpr auto kUnsignedIntRegExp = "\\d+"_L1;
302static constexpr auto kDoubleRegExp = "[+-]?\\d+(.\\d+([eE][+-]?\\d+)?)?"_L1;
303static constexpr auto kDbcIdentRegExp = "[_[:alpha:]][_[:alnum:]]+"_L1;
304static constexpr auto kOneOrMoreSpaceRegExp = "[ ]+"_L1;
305static constexpr auto kMaybeSpaceRegExp = "[ ]*"_L1;
306static constexpr auto kMuxIndicatorRegExp = "M|m\\d+M?"_L1;
307static constexpr auto kByteOrderRegExp = "0|1"_L1;
308static constexpr auto kValueTypeRegExp = "\\+|\\-"_L1;
309// The pattern matches all printable characters, except double-quote (") and backslash (\).
310static constexpr auto kCharStrRegExp = "((?![\\\"\\\\])\\P{Cc})*"_L1;
311
312void QCanDbcFileParserPrivate::reset()
313{
314 m_fileName.clear();
315 m_error = QCanDbcFileParser::Error::None;
316 m_errorString.clear();
317 m_warnings.clear();
318 m_lineOffset = 0;
319 m_isProcessingMessage = false;
320 m_seenExtraData = false;
321 m_currentMessage = {};
322 m_messageDescriptions.clear();
323 m_valueDescriptions.clear();
324}
325
326/*!
327 \internal
328 Returns \c false only in case of hard error. Returns \c true even if some
329 warnings occurred during parsing.
330*/
331bool QCanDbcFileParserPrivate::parseFile(const QString &fileName)
332{
333 QFile f(fileName);
334 if (!f.open(flags: QIODevice::ReadOnly)) {
335 m_error = QCanDbcFileParser::Error::FileReading;
336 m_errorString = f.errorString();
337 return false;
338 }
339 m_fileName = fileName;
340 m_seenExtraData = false;
341
342 while (!f.atEnd()) {
343 const QString str = QString::fromUtf8(ba: f.readLine().trimmed());
344 if (!processLine(line: {str.constData(), str.size()})) // also sets the error properly
345 return false;
346 }
347 addCurrentMessage(); // check if we need to add the message
348 // now when we parsed the whole file, we can verify the signal multiplexing
349 postProcessSignalMultiplexing();
350
351 return true;
352}
353
354/*!
355 \internal
356 Returns \c false only in case of hard error. Returns \c true even if some
357 warnings occurred during parsing.
358*/
359bool QCanDbcFileParserPrivate::processLine(const QStringView line)
360{
361 QStringView data = line;
362 m_lineOffset = 0;
363 if (data.startsWith(s: kMessageDef)) {
364 if (m_seenExtraData) {
365 // Unexpected position of message description
366 m_error = QCanDbcFileParser::Error::Parsing;
367 m_errorString = QObject::tr(s: "Failed to parse file %1. Unexpected position "
368 "of %2 section.").arg(args&: m_fileName, args: kMessageDef);
369 return false;
370 }
371 addCurrentMessage();
372 if (!parseMessage(data))
373 return false;
374 }
375 // signal definitions can be on the same line as message definition,
376 // or on a separate line
377 data = data.sliced(pos: m_lineOffset).trimmed();
378 while (data.startsWith(s: kSignalDef)) {
379 if (!m_isProcessingMessage || m_seenExtraData) {
380 // Unexpected position of signal description
381 m_error = QCanDbcFileParser::Error::Parsing;
382 m_errorString = QObject::tr(s: "Failed to parse file %1. Unexpected position "
383 "of %2 section.").arg(args&: m_fileName, args: kSignalDef);
384 return false;
385 }
386 if (!parseSignal(data))
387 return false;
388 data = data.sliced(pos: m_lineOffset).trimmed();
389 }
390 // If we detect one of the following lines, then message description is
391 // finished. We also assume that we can have only one key at each line.
392 if (data.startsWith(s: kSigValTypeDef)) {
393 m_seenExtraData = true;
394 addCurrentMessage();
395 parseSignalType(data);
396 } else if (data.startsWith(s: kCommentDef)) {
397 m_seenExtraData = true;
398 addCurrentMessage();
399 parseComment(data);
400 } else if (data.startsWith(s: kExtendedMuxDef)) {
401 m_seenExtraData = true;
402 addCurrentMessage();
403 parseExtendedMux(data);
404 } else if (data.startsWith(s: kValDef)) {
405 m_seenExtraData = true;
406 addCurrentMessage();
407 parseValueDescriptions(data);
408 }
409 return true;
410}
411
412static std::optional<QtCanBus::UniqueId> extractUniqueId(QStringView view)
413{
414 bool ok = false;
415 const uint value = view.toUInt(ok: &ok);
416 if (ok)
417 return QtCanBus::UniqueId{value & 0x1FFFFFFF};
418 return std::nullopt;
419}
420
421/*!
422 \internal
423 Returns \c false only in case of hard error. Returns \c true even if some
424 warnings occurred during parsing.
425*/
426bool QCanDbcFileParserPrivate::parseMessage(const QStringView data)
427{
428 // The regexp matches the following definition:
429 // BO_ message_id message_name ':' message_size transmitter
430 // also considering the fact that spaces around ':' seem to be optional, and
431 // allowing more than one space between parts.
432
433 // %1 - messageDef
434 // %2 - maybeSpace
435 // %3 - unsignedInt
436 // %4 - oneOrMoreSpace
437 // %5 - DbcIdentifier
438 static const QString regExStr =
439 "%1%2(?<messageId>%3)%4(?<name>%5)%2:%2(?<size>%3)%4(?<transmitter>%5)"_L1.
440 arg(args: kMessageDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
441 args: kDbcIdentRegExp);
442 static const QRegularExpression messageRegExp(regExStr);
443
444 m_isProcessingMessage = false;
445 const auto match = messageRegExp.matchView(subjectView: data);
446 if (match.hasMatch()) {
447 m_currentMessage = extractMessage(match);
448 // can't check for isValid() here, because demands signal descriptions
449 if (!m_currentMessage.name().isEmpty()) {
450 m_isProcessingMessage = true;
451 } else {
452 addWarning(warning: QObject::tr(s: "Failed to parse message description from "
453 "string %1").arg(a: data));
454 }
455 m_lineOffset = match.capturedEnd(nth: 0);
456 } else {
457 addWarning(warning: QObject::tr(s: "Failed to find message description in string %1").arg(a: data));
458 m_lineOffset = data.size(); // skip this string
459 }
460 return true;
461}
462
463QCanMessageDescription
464QCanDbcFileParserPrivate::extractMessage(const QRegularExpressionMatch &match)
465{
466 Q_ASSERT(match.hasMatch());
467 QCanMessageDescription desc;
468 desc.setName(match.captured(name: u"name"_s));
469
470 const auto id = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
471 if (id.has_value()) {
472 desc.setUniqueId(id.value());
473 } else {
474 addWarning(warning: QObject::tr(s: "Failed to parse frame id for message %1").arg(a: desc.name()));
475 return {};
476 }
477
478 bool ok = false;
479 const auto size = match.capturedView(name: u"size"_s).toUInt(ok: &ok);
480 if (ok) {
481 desc.setSize(size);
482 } else {
483 addWarning(warning: QObject::tr(s: "Failed to parse size for message %1").arg(a: desc.name()));
484 return {};
485 }
486
487 desc.setTransmitter(match.captured(name: u"transmitter"_s));
488
489 return desc;
490}
491
492/*!
493 \internal
494 Returns \c false only in case of hard error. Returns \c true even if some
495 warnings occurred during parsing.
496*/
497bool QCanDbcFileParserPrivate::parseSignal(const QStringView data)
498{
499 // The regexp should match the following pattern:
500 // SG_ signal_name multiplexer_indicator : start_bit |
501 // signal_size @ byte_order value_type ( factor , offset )
502 // [ minimum | maximum ] unit receiver {, receiver}
503 // We also need to consider the fact that some of the spaces might be
504 // optional, and we can potentially allow more spaces between parts.
505 // Note that the end of the signal description can contain multiple
506 // receivers. The regexp is supposed to extract all of them, but we use
507 // only the first one for now.
508
509 // %1 - SignalDef
510 // %2 - MaybeSpace
511 // %3 - DbcIdentifier
512 // %4 - OneOrMoreSpace
513 // %5 - MuxIndicator
514 // %6 - unsignedInt
515 // %7 - byteOrder
516 // %8 - valueType
517 // %9 - double
518 // %10 - charStr
519 static const QString regExStr =
520 "%1%2(?<name>%3)(%4(?<mux>%5))?%2:%2(?<startBit>%6)%2\\|%2(?<sigSize>%6)%2@%2"
521 "(?<byteOrder>%7)%2(?<valueType>%8)%4\\(%2(?<factor>%9)%2,%2(?<offset>%9)%2\\)"
522 "%4\\[%2(?<min>%9)%2\\|%2(?<max>%9)%2\\]%4\"(?<unit>%10)\""
523 "%4(?<receiver>%3)(%2,%2%3)*"_L1.
524 arg(args: kSignalDef, args: kMaybeSpaceRegExp, args: kDbcIdentRegExp, args: kOneOrMoreSpaceRegExp,
525 args: kMuxIndicatorRegExp, args: kUnsignedIntRegExp, args: kByteOrderRegExp, args: kValueTypeRegExp,
526 args: kDoubleRegExp, args: kCharStrRegExp);
527 static const QRegularExpression signalRegExp(regExStr);
528
529 const auto match = signalRegExp.matchView(subjectView: data);
530 if (match.hasMatch()) {
531 QCanSignalDescription desc = extractSignal(match);
532
533 if (desc.isValid())
534 m_currentMessage.addSignalDescription(description: desc);
535 else
536 addWarning(warning: QObject::tr(s: "Failed to parse signal description from string %1").arg(a: data));
537
538 m_lineOffset = match.capturedEnd(nth: 0);
539 } else {
540 addWarning(warning: QObject::tr(s: "Failed to find signal description in string %1").arg(a: data));
541 m_lineOffset = data.size(); // skip this string
542 }
543 return true;
544}
545
546QCanSignalDescription QCanDbcFileParserPrivate::extractSignal(const QRegularExpressionMatch &match)
547{
548 Q_ASSERT(match.hasMatch());
549 QCanSignalDescription desc;
550 desc.setName(match.captured(name: u"name"_s));
551
552 bool ok = false;
553
554 if (match.hasCaptured(name: u"mux"_s)) {
555 const auto muxStr = match.capturedView(name: u"mux"_s);
556 if (muxStr == u"M"_s) {
557 desc.setMultiplexState(QtCanBus::MultiplexState::MultiplexorSwitch);
558 } else if (muxStr.endsWith(s: u"M"_s, cs: Qt::CaseSensitive)) {
559 desc.setMultiplexState(QtCanBus::MultiplexState::SwitchAndSignal);
560 const auto val = muxStr.sliced(pos: 1, n: muxStr.size() - 2).toUInt(ok: &ok);
561 if (!ok) {
562 addWarning(warning: QObject::tr(s: "Failed to parse multiplexor value for signal %1").
563 arg(a: desc.name()));
564 return {};
565 }
566 // We have the value, but we do not really know the multiplexor
567 // switch name. To know it, we potentially need to parse all signals
568 // for the message. So for now we just create a dummy entry, and
569 // the actual signal name will be updated later;
570 desc.addMultiplexSignal(name: kQtDummySignal, value: val);
571 } else {
572 desc.setMultiplexState(QtCanBus::MultiplexState::MultiplexedSignal);
573 const auto val = muxStr.sliced(pos: 1).toUInt(ok: &ok);
574 if (!ok) {
575 addWarning(warning: QObject::tr(s: "Failed to parse multiplexor value for signal %1").
576 arg(a: desc.name()));
577 return {};
578 }
579 // Same as above
580 desc.addMultiplexSignal(name: kQtDummySignal, value: val);
581 }
582 }
583
584 const uint startBit = match.capturedView(name: u"startBit"_s).toUInt(ok: &ok);
585 if (ok) {
586 desc.setStartBit(startBit);
587 } else {
588 addWarning(warning: QObject::tr(s: "Failed to parse start bit for signal %1").arg(a: desc.name()));
589 return {};
590 }
591
592 const uint bitLength = match.capturedView(name: u"sigSize"_s).toUInt(ok: &ok);
593 if (ok) {
594 desc.setBitLength(bitLength);
595 } else {
596 addWarning(warning: QObject::tr(s: "Failed to parse bit length for signal %1").arg(a: desc.name()));
597 return {};
598 }
599
600 // 0 = BE; 1 = LE
601 const auto endian = match.capturedView(name: u"byteOrder"_s) == u"0"_s
602 ? QSysInfo::Endian::BigEndian : QSysInfo::Endian::LittleEndian;
603 desc.setDataEndian(endian);
604
605 // + = unsigned; - = signed
606 const auto dataFormat = match.capturedView(name: u"valueType"_s) == u"+"_s
607 ? QtCanBus::DataFormat::UnsignedInteger : QtCanBus::DataFormat::SignedInteger;
608 desc.setDataFormat(dataFormat);
609
610 const double factor = match.capturedView(name: u"factor"_s).toDouble(ok: &ok);
611 if (ok) {
612 desc.setFactor(factor);
613 } else {
614 addWarning(warning: QObject::tr(s: "Failed to parse factor for signal %1").arg(a: desc.name()));
615 return {};
616 }
617
618 const double offset = match.capturedView(name: u"offset"_s).toDouble(ok: &ok);
619 if (ok) {
620 desc.setOffset(offset);
621 } else {
622 addWarning(warning: QObject::tr(s: "Failed to parse offset for signal %1").arg(a: desc.name()));
623 return {};
624 }
625
626 const double min = match.capturedView(name: u"min"_s).toDouble(ok: &ok);
627 if (ok) {
628 const double max = match.capturedView(name: u"max"_s).toDouble(ok: &ok);
629 if (ok)
630 desc.setRange(minimum: min, maximum: max);
631 }
632 if (!ok) {
633 addWarning(warning: QObject::tr(s: "Failed to parse value range from signal %1").arg(a: desc.name()));
634 return {};
635 }
636
637 desc.setPhysicalUnit(match.captured(name: u"unit"_s));
638 desc.setReceiver(match.captured(name: u"receiver"_s));
639
640 return desc;
641}
642
643void QCanDbcFileParserPrivate::parseSignalType(const QStringView data)
644{
645 // The regexp should match the following pattern:
646 // SIG_VALTYPE_ message_id signal_name signal_extended_value_type ;
647 // We also need to consider the fact that we can potentially allow more
648 // spaces between parts.
649
650 // %1 sigValTypeDef
651 // %2 maybeSpace
652 // %3 unsignedInt
653 // %4 oneOrMoreSpace
654 // %5 DbcIdentifier
655 const QString regExStr =
656 "%1%2(?<messageId>%3)%4(?<sigName>%5)%2:%2(?<type>%3)%2;"_L1.
657 arg(args: kSigValTypeDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp,
658 args: kOneOrMoreSpaceRegExp, args: kDbcIdentRegExp);
659 const QRegularExpression sigValTypeRegEx(regExStr);
660
661 const auto match = sigValTypeRegEx.matchView(subjectView: data);
662 if (!match.hasMatch()) {
663 m_lineOffset = data.size();
664 addWarning(warning: QObject::tr(s: "Failed to find signal value type description in string %1").
665 arg(a: data));
666 return;
667 }
668
669 m_lineOffset = match.capturedEnd(nth: 0);
670
671 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
672 if (!uidOptional) {
673 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
674 return;
675 }
676
677 const QtCanBus::UniqueId uid = uidOptional.value();
678 auto msgDesc = m_messageDescriptions.value(key: uid);
679 if (msgDesc.isValid()) {
680 const QString sigName = match.captured(name: u"sigName"_s);
681 auto sigDesc = msgDesc.signalDescriptionForName(name: sigName);
682 if (sigDesc.isValid()) {
683 bool ok = false;
684 const auto type = match.capturedView(name: u"type").toUInt(ok: &ok);
685 if (ok) {
686 bool sigDescChanged = false;
687 switch (type) {
688 case 0: /* signed or unsigned integer */
689 // do nothing, as we already have signed/unsinged integer
690 // based on "SG_ " string
691 break;
692 case 1: /* 32-bit IEEE-float */
693 sigDesc.setDataFormat(QtCanBus::DataFormat::Float);
694 sigDesc.setBitLength(32);
695 sigDescChanged = true;
696 break;
697 case 2: /* 64-bit IEEE-double */
698 sigDesc.setDataFormat(QtCanBus::DataFormat::Double);
699 sigDesc.setBitLength(64);
700 sigDescChanged = true;
701 break;
702 default:
703 // invalid value
704 break;
705 }
706 if (sigDescChanged) {
707 msgDesc.addSignalDescription(description: sigDesc);
708 m_messageDescriptions.insert(key: msgDesc.uniqueId(), value: msgDesc);
709 }
710 } else {
711 addWarning(warning: QObject::tr(s: "Failed to parse data type from string %1").arg(a: data));
712 }
713 } else {
714 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
715 "Skipping string %2").arg(args: sigName, args: data));
716 }
717 } else {
718 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
719 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
720 }
721}
722
723void QCanDbcFileParserPrivate::parseComment(const QStringView data)
724{
725 // The comment for message or signal description is represented by the
726 // following pattern:
727 // CM_ (BO_ message_id char_string | SG_ message_id signal_name char_string);
728
729 // %1 commentDef
730 // %2 maybeSpace
731 // %3 messageDef
732 // %4 signalDef
733 // %5 oneOrMoreSpace
734 // %6 unsignedInt
735 // %7 DbcIdentifier
736 // %8 charStr
737 const QString regExStr =
738 "%1%2(?<type>(%3|%4))%2(?<messageId>%6)%5((?<sigName>%7)%5)?\"(?<comment>%8)\"%2;"_L1.
739 arg(args: kCommentDef, args: kMaybeSpaceRegExp, args: kMessageDef, args: kSignalDef, args: kOneOrMoreSpaceRegExp,
740 args: kUnsignedIntRegExp, args: kDbcIdentRegExp, args: kCharStrRegExp);
741 const QRegularExpression commentRegExp(regExStr);
742
743 const auto match = commentRegExp.matchView(subjectView: data);
744 if (!match.hasMatch()) {
745 // no warning here, as we ignore some "general" comments, and parse only
746 // comments related to messages and signals
747 m_lineOffset = data.size();
748 return;
749 }
750
751 m_lineOffset = match.capturedEnd(nth: 0);
752
753 const auto type = match.capturedView(name: u"type"_s);
754
755 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
756 if (!uidOptional) {
757 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
758 return;
759 }
760
761 const QtCanBus::UniqueId uid = uidOptional.value();
762 auto messageDesc = m_messageDescriptions.value(key: uid);
763 if (!messageDesc.isValid()) {
764 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
765 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
766 return;
767 }
768
769 if (type == kMessageDef) {
770 const QString comment = match.captured(name: u"comment"_s);
771 messageDesc.setComment(comment);
772 m_messageDescriptions.insert(key: uid, value: messageDesc);
773 } else if (type == kSignalDef) {
774 const QString sigName = match.captured(name: u"sigName"_s);
775 auto signalDesc = messageDesc.signalDescriptionForName(name: sigName);
776 if (signalDesc.isValid()) {
777 const QString comment = match.captured(name: u"comment"_s);
778 signalDesc.setComment(comment);
779 messageDesc.addSignalDescription(description: signalDesc);
780 m_messageDescriptions.insert(key: uid, value: messageDesc);
781 } else {
782 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
783 "Skipping string %2").arg(args: sigName, args: data));
784 }
785 }
786}
787
788void QCanDbcFileParserPrivate::parseExtendedMux(const QStringView data)
789{
790 // The extended multiplexing is defined by the following pattern:
791 // SG_MUL_VAL_ message_id multiplexed_signal_name
792 // multiplexor_switch_name multiplexor_value_ranges ;
793 // Here multiplexor_value_ranges consists of multiple ranges, separated
794 // by a whitespace, and one range is defined as follows:
795 // multiplexor_value_range = unsigned_integer - unsigned_integer
796
797 // %1 extendedMuxDef
798 // %2 maybeSpace
799 // %3 unsignedInt
800 // %4 oneOrMoreSpace
801 // %5 DbcIdentifier
802 const QString regExStr =
803 "%1%2(?<messageId>%3)%4(?<multiplexedSignal>%5)%4(?<multiplexorSwitch>%5)%4"
804 "(?<firstRange>%3%2-%2%3)(%2,%2%3%2-%2%3)*%2;"_L1.
805 arg(args: kExtendedMuxDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
806 args: kDbcIdentRegExp);
807 const QRegularExpression extendedMuxRegExp(regExStr);
808
809 const auto match = extendedMuxRegExp.matchView(subjectView: data);
810 if (!match.hasMatch()) {
811 m_lineOffset = data.size();
812 addWarning(warning: QObject::tr(s: "Failed to find extended multiplexing description in string %1").
813 arg(a: data));
814 return;
815 }
816
817 m_lineOffset = match.capturedEnd(nth: 0);
818
819 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
820 if (!uidOptional) {
821 addWarning(warning: QObject::tr(s: "Failed to parse frame id from string %1").arg(a: data));
822 return;
823 }
824
825 const QtCanBus::UniqueId uid = uidOptional.value();
826 auto messageDesc = m_messageDescriptions.value(key: uid);
827 if (!messageDesc.isValid()) {
828 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
829 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
830 return;
831 }
832
833 const QString multiplexedSignalName = match.captured(name: u"multiplexedSignal"_s);
834 const QString multiplexorSwitchName = match.captured(name: u"multiplexorSwitch"_s);
835
836 auto multiplexedSignal = messageDesc.signalDescriptionForName(name: multiplexedSignalName);
837 auto multiplexorSwitch = messageDesc.signalDescriptionForName(name: multiplexorSwitchName);
838
839 if (!multiplexedSignal.isValid() || !multiplexorSwitch.isValid()) {
840 const QString invalidName = multiplexedSignal.isValid() ? multiplexorSwitchName
841 : multiplexedSignalName;
842 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
843 "Skipping string %2").arg(args: invalidName, args: data));
844 return;
845 }
846
847 auto signalRanges = multiplexedSignal.multiplexSignals();
848 signalRanges.remove(key: kQtDummySignal); // dummy signal not needed anymore
849
850 QCanSignalDescription::MultiplexValues rangeValues;
851 auto rangeView = match.capturedView(name: u"firstRange"_s);
852 const auto sepIdx = rangeView.indexOf(c: u'-');
853 if (sepIdx != -1) {
854 const auto min = rangeView.first(n: sepIdx).trimmed().toUInt();
855 const auto max = rangeView.sliced(pos: sepIdx + 1).trimmed().toUInt();
856 rangeValues.push_back(t: {.minimum: min, .maximum: max});
857 }
858
859 // We can have an arbitrary amount of ranges, so we can't use capture groups
860 // to capture them. But we know that they follow a specific pattern (because
861 // the full string matched the regexp). So we need to parse the rest of the
862 // matched string manually
863 const auto totalEnd = match.capturedEnd(nth: 0); // including the ';'
864 const auto firstRangeEnd = match.capturedEnd(name: u"firstRange"_s);
865 const auto len = totalEnd - firstRangeEnd - 1;
866 if (len > 0) {
867 const auto otherRangesView = data.sliced(pos: firstRangeEnd, n: len).trimmed();
868 const QStringTokenizer parts = otherRangesView.tokenize(needle: u',', flags: Qt::SkipEmptyParts);
869 for (const QStringView range : parts) {
870 const auto sepIdx = range.indexOf(c: u'-');
871 if (sepIdx != -1) {
872 const auto min = range.first(n: sepIdx).trimmed().toUInt();
873 const auto max = range.sliced(pos: sepIdx + 1).trimmed().toUInt();
874 rangeValues.push_back(t: {.minimum: min, .maximum: max});
875 }
876 }
877 }
878
879 if (!rangeValues.isEmpty())
880 signalRanges.insert(key: multiplexorSwitchName, value: rangeValues);
881 else
882 signalRanges.remove(key: multiplexorSwitchName);
883
884 // update the value
885 multiplexedSignal.setMultiplexSignals(signalRanges);
886 messageDesc.addSignalDescription(description: multiplexedSignal);
887 m_messageDescriptions.insert(key: uid, value: messageDesc);
888}
889
890void QCanDbcFileParserPrivate::parseValueDescriptions(const QStringView data)
891{
892 // The regexp should match the following pattern:
893 // VAL_ message_id signal_name { value_description };
894 // Here the value_description is defined as follows
895 // value_description = unsigned_int char_string
896
897 // %1 valDef
898 // %2 maybeSpace
899 // %3 unsignedInt
900 // %4 oneOrMoreSpace
901 // %5 DbcIdentifier
902 // %6 charStr
903 const QString regExStr =
904 "%1%2(?<messageId>%3)%4(?<signalName>%5)(%4%3%4\"(%6)\")+%2;"_L1.
905 arg(args: kValDef, args: kMaybeSpaceRegExp, args: kUnsignedIntRegExp, args: kOneOrMoreSpaceRegExp,
906 args: kDbcIdentRegExp, args: kCharStrRegExp);
907
908 const QRegularExpression valueDescRegExp(regExStr);
909
910 const auto match = valueDescRegExp.matchView(subjectView: data);
911 if (!match.hasMatch()) {
912 m_lineOffset = data.size();
913 addWarning(warning: QObject::tr(s: "Failed to parse value description from string %1").arg(a: data));
914 return;
915 }
916
917 m_lineOffset = match.capturedEnd(nth: 0);
918
919 const auto uidOptional = extractUniqueId(view: match.capturedView(name: u"messageId"_s));
920 if (!uidOptional) {
921 addWarning(warning: QObject::tr(s: "Failed to parse value description from string %1").arg(a: data));
922 return;
923 }
924
925 const QtCanBus::UniqueId uid = uidOptional.value();
926 // Check if the message exists
927 const auto messageDesc = m_messageDescriptions.value(key: uid);
928 if (!messageDesc.isValid()) {
929 addWarning(warning: QObject::tr(s: "Failed to find message description for unique id %1. "
930 "Skipping string %2").arg(a: qToUnderlying(e: uid)).arg(a: data));
931 return;
932 }
933
934 // Check if the signal exists within the message
935 const QString signalName = match.captured(name: u"signalName"_s);
936 if (!messageDesc.signalDescriptionForName(name: signalName).isValid()) {
937 addWarning(warning: QObject::tr(s: "Failed to find signal description for signal %1. "
938 "Skipping string %2").arg(args: signalName, args: data));
939 return;
940 }
941
942 // We can have an arbitrary amount of value descriptions, so we can't use
943 // capture groups to capture them. But we know that they follow a specific
944 // pattern (because the full string matched the regexp). So we need to parse
945 // the rest of the matched string manually
946 const auto totalEnd = match.capturedEnd(nth: 0); // including the ';'
947 const auto signalNameEnd = match.capturedEnd(name: u"signalName"_s);
948 const auto len = totalEnd - signalNameEnd - 1;
949 if (len > 0) {
950 auto signalDescriptionsView = data.sliced(pos: signalNameEnd, n: len).trimmed();
951 while (signalDescriptionsView.size()) {
952 const auto spacePos = signalDescriptionsView.indexOf(c: u' ');
953 if (spacePos == -1)
954 break;
955 bool ok = false;
956 const auto value = signalDescriptionsView.sliced(pos: 0, n: spacePos).toUInt(ok: &ok);
957 if (!ok)
958 break;
959 const auto firstQuotePos = signalDescriptionsView.indexOf(c: u'"', from: spacePos + 1);
960 if (firstQuotePos == -1)
961 break;
962 const auto nextQuotePos = signalDescriptionsView.indexOf(c: u'"', from: firstQuotePos + 1);
963 if (nextQuotePos == -1)
964 break;
965 const auto description = signalDescriptionsView.sliced(
966 pos: firstQuotePos + 1, n: nextQuotePos - firstQuotePos - 1);
967
968 m_valueDescriptions[uid][signalName].insert(key: value, value: description.toString());
969 signalDescriptionsView = signalDescriptionsView.sliced(pos: nextQuotePos + 1).trimmed();
970 }
971 }
972}
973
974void QCanDbcFileParserPrivate::postProcessSignalMultiplexing()
975{
976 // For the case of simple multiplexing we need to do the following for
977 // every message description:
978 // 1. Find the multiplexor signal
979 // 2. Replace all kQtDummySignal entries with the name of the multiplexor
980 // 3. While doing so, check if we have any signals with type
981 // SwitchAndSignal. This will mean that extended multiplexing is used
982 // 4. Also detect conditions when we have more than one multiplexor signal.
983 // This is an error as well, and message description should be discarded.
984
985 QList<QtCanBus::UniqueId> uidsToRemove;
986
987 for (auto &messageDesc : m_messageDescriptions) {
988 bool useExtendedMux = false;
989 QString multiplexorSignal;
990 auto &signalDescriptions = QCanMessageDescriptionPrivate::get(desc: messageDesc)->messageSignals;
991 for (const auto &signalDesc : std::as_const(t&: signalDescriptions)) {
992 if (signalDesc.multiplexState() == QtCanBus::MultiplexState::MultiplexorSwitch) {
993 if (multiplexorSignal.isEmpty()) {
994 multiplexorSignal = signalDesc.name();
995 } else {
996 // invalid config
997 multiplexorSignal.clear();
998 uidsToRemove.push_back(t: messageDesc.uniqueId());
999 break;
1000 }
1001 } else if (signalDesc.multiplexState() == QtCanBus::MultiplexState::SwitchAndSignal) {
1002 // extended multiplexing
1003 useExtendedMux = true;
1004 }
1005 }
1006 if (!useExtendedMux && !multiplexorSignal.isEmpty()) {
1007 // iterate through all signal descriptions and update kQtDummySignal
1008 for (auto &signalDesc : signalDescriptions) {
1009 if (signalDesc.multiplexState() == QtCanBus::MultiplexState::MultiplexedSignal) {
1010 auto &muxValues = QCanSignalDescriptionPrivate::get(desc: signalDesc)->muxSignals;
1011 auto val = muxValues.value(key: kQtDummySignal);
1012 muxValues.remove(key: kQtDummySignal);
1013 muxValues.insert(key: multiplexorSignal, value: val);
1014 }
1015 }
1016 } else if (useExtendedMux) {
1017 // Iterate through all signal descriptions and check that we do
1018 // not have any kQtDummySignal entries. It such entry exists, this
1019 // means that there were errors while parsing extended multiplexing
1020 // table, and this message description is invalid
1021 for (const auto &signalDesc : std::as_const(t&: signalDescriptions)) {
1022 const auto muxSignals = signalDesc.multiplexSignals();
1023 if (muxSignals.contains(key: kQtDummySignal)) {
1024 uidsToRemove.push_back(t: messageDesc.uniqueId());
1025 break;
1026 }
1027 }
1028 }
1029 }
1030
1031 for (const auto &uid : std::as_const(t&: uidsToRemove)) {
1032 m_messageDescriptions.remove(key: uid);
1033 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped because "
1034 "it has invalid multiplexing description.").
1035 arg(a: qToUnderlying(e: uid)));
1036 }
1037}
1038
1039void QCanDbcFileParserPrivate::addWarning(QString &&warning)
1040{
1041 m_warnings.emplace_back(args&: warning);
1042}
1043
1044void QCanDbcFileParserPrivate::addCurrentMessage()
1045{
1046 if (m_isProcessingMessage) {
1047 auto uid = m_currentMessage.uniqueId();
1048 if (!m_currentMessage.isValid()) {
1049 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped "
1050 "because it's not valid.").arg(a: qToUnderlying(e: uid)));
1051 } else if (m_messageDescriptions.contains(key: uid)) {
1052 addWarning(warning: QObject::tr(s: "Message description with unique id %1 is skipped "
1053 "because such unique id is already used.").
1054 arg(a: qToUnderlying(e: uid)));
1055 } else {
1056 m_messageDescriptions.insert(key: uid, value: m_currentMessage);
1057 }
1058 m_currentMessage = {};
1059 m_isProcessingMessage = false;
1060 }
1061}
1062
1063QList<QCanMessageDescription> QCanDbcFileParserPrivate::getMessages() const
1064{
1065 return QList<QCanMessageDescription>(m_messageDescriptions.cbegin(),
1066 m_messageDescriptions.cend());
1067}
1068
1069QT_END_NAMESPACE
1070

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