// 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 "qcansignaldescription.h" #include "qcansignaldescription_p.h" QT_BEGIN_NAMESPACE /*! \class QCanSignalDescription \inmodule QtSerialBus \since 6.5 \preliminary \brief The QCanSignalDescription class describes the rules to extract one value out of the CAN frame and represent it in an application-defined format. The QCanSignalDescription class can be used to provide a signal description and later use it to decode a received \l QCanBusFrame or encode the input data into a \l QCanBusFrame that can be sent to the receiver. \section2 General Description Each CAN frame can contain multiple values. The rules to extract the values from a CAN frame include the following: \list \li Data source (frame ID or payload). \li Data endianness. See \l {Data Endianness Processing} section for more details. \li Data format. \li Start bit position. \li Data length in bits. \li Multiplexing options. \endlist Start bit position is specified relative to the selected data source. The bits are counted starting from the LSB. Once the data is extracted, it might require conversion to an application-defined format. The following parameters can be used for that: \list \li Various parameters for converting the extracted value to a physical value (factor, offset, scale). \li Expected data range. \li Data units. \endlist The QCanSignalDescription class provides methods to control all those parameters. \section2 Data Endianness Processing Little endian and big endian data is encoded differently. For big endian values, start bit positions are given for the most significant bit. For little endian values, the start position is that of the least significant bit. Let's consider two examples. In both examples we will encode two 12-bit values in the 3-byte payload. \section3 Little Endian For the little endian case the data layout can be represented by the following image: \image canbus_signals_le.png Here the columns represent bit numbers, and the rows represent byte numbers. \c {LSB} marks the first (least significant) bit of the value, and \c {MSB} marks the last (most significant) bit of the value. The blue color marks the first value, and the orange color marks the second value. The information about these values will be encoded in QCanSignalDescription in the following way: \code QCanSignalDescription signal1; signal1.setDataEndian(QSysInfo::Endian::LittleEndian); signal1.setStartBit(0); signal1.setBitLength(12); // other parameters for signal1 QCanSignalDescription signal2; signal2.setDataEndian(QSysInfo::Endian::LittleEndian); signal2.setStartBit(12); signal2.setBitLength(12); // other parameters for signal2 \endcode \section3 Big Endian The following image represents the value layout for the big endian case: \image canbus_signals_be.png The values can be represented in QCanSignalDescription in the following way: \code QCanSignalDescription signal1; signal1.setDataEndian(QSysInfo::Endian::BigEndian); signal1.setStartBit(7); signal1.setBitLength(12); // other parameters for signal1 QCanSignalDescription signal2; signal2.setDataEndian(QSysInfo::Endian::BigEndian); signal2.setStartBit(11); signal2.setBitLength(12); // other parameters for signal2 \endcode Note how the start bits are different from the little endian case. Also the values are aligned differently. \section2 Multiplexed Signals Explained There are two common ways to encode the data in the CAN payload: \list \li Each range of bits always represents the same signal. For example, \c {Bytes 0-1} in a payload can represent an engine speed (in rpm), and \c {Bytes 2-3} can represent the vehicle speed (in km/h). \li The same range of bits can represent different data, depending on the values of some other bits in the payload. For example, if \c {Byte 0} has the value \c {0}, the \c {Bytes 1-2} represent an engine speed (in rpm), and if \c {Byte 0} has the value \c {1}, the same \c {Bytes 1-2} represent a vehicle speed (in km/h). \endlist The second case uses signal multiplexing. In the provided example we will have three signals. The first signal represents the value of \c {Byte 0} and acts like a multiplexor signal. The other two signals represent an engine speed and a vehicle speed respectively, but only one of them can be extracted from the CAN payload at a time. Which signal should be extracted is defined by the value of the multiplexor signal. In more complicated cases the payload can have multiple multiplexor signals. In such cases the signal can be extracted from the payload only when all multiplexors contain the expected values. \section2 Value Conversions In many cases the signals transferred over CAN bus cannot hold the full range of the physical values that they represent. To overcome these limitations, the physical values are converted to a smaller range before transmission, and can be restored on the receiving end. The following formulas are used to convert between the physical value and the signal's value: \badcode physicalValue = scaling * (signalValue * factor + offset); signalValue = (physicalValue / scaling - offset) / factor; \endcode The factor and scaling parameters cannot be equal to \c {0}. If any of the parameters equals to \l qQNaN(), it is not used during the conversion. If all of the parameters are equal to \l qQNaN() (which is the default), the conversion is not performed. */ /*! \struct QCanSignalDescription::MultiplexValueRange \inmodule QtSerialBus \since 6.5 \brief Defines a range of values for a multiplexor signal. Each multiplexor signal can have several ranges of values assigned to it. This type represents one range. Both minimum and maximum values are included in the range. Minimum and maximum values can be equal, so a MultiplexValueRange is never empty. If maximum is less than minimum, the values will be swapped while doing the range check. \sa {Multiplexed Signals Explained} */ /*! \variable QCanSignalDescription::MultiplexValueRange::minimum \brief the minimum value of the range. */ /*! \variable QCanSignalDescription::MultiplexValueRange::maximum \brief the maximum value of the range. */ /*! \typealias QCanSignalDescription::MultiplexValues */ /*! \typealias QCanSignalDescription::MultiplexSignalValues */ /*! Creates an empty signal description. */ QCanSignalDescription::QCanSignalDescription() : d(new QCanSignalDescriptionPrivate) { } /*! Creates a signal description with the values copied from \a other. */ QCanSignalDescription::QCanSignalDescription(const QCanSignalDescription &other) : d(other.d) { } /*! \fn QCanSignalDescription::QCanSignalDescription(QCanSignalDescription &&other) noexcept Creates a signal description by moving from \a other. \note The moved-from QCanSignalDescription object can only be destroyed or assigned to. The effect of calling other functions than the destructor or one of the assignment operators is undefined. */ /*! \fn QCanSignalDescription::~QCanSignalDescription() Destroys this signal description. */ QT_DEFINE_QESDP_SPECIALIZATION_DTOR(QCanSignalDescriptionPrivate) /*! Assigns the values from \a other to this signal description. */ QCanSignalDescription &QCanSignalDescription::operator=(const QCanSignalDescription &other) { d = other.d; return *this; } /*! \fn QCanSignalDescription &QCanSignalDescription::operator=(QCanSignalDescription &&other) noexcept Move-assigns the values from \a other to this signal description. \note The moved-from QCanSignalDescription object can only be destroyed or assigned to. The effect of calling other functions than the destructor or one of the assignment operators is undefined. */ /*! Returns \c true when the signal description is valid and \c false otherwise. A valid signal description \e must fulfill the following conditions: \list \li have a non-empty \l name() \li have \l bitLength() \c {== 32} if the \l dataFormat() is \l {QtCanBus::DataFormat::}{Float} \li have \l bitLength() \c {== 64} if the \l dataFormat() is \l {QtCanBus::DataFormat::}{Double} \li the \l bitLength() \e must be a multiple of \c 8 if the \l dataFormat() is \l {QtCanBus::DataFormat::}{AsciiString} \li the \l bitLength() \e must be greater than \c 0 and less than or equal to \c {64}. \endlist \sa bitLength(), dataFormat(), name() */ bool QCanSignalDescription::isValid() const { const bool formatMatch = [this]() { if (d->format == QtCanBus::DataFormat::Float) return d->dataLength == 32; if (d->format == QtCanBus::DataFormat::Double) return d->dataLength == 64; if (d->format == QtCanBus::DataFormat::AsciiString) return d->dataLength % 8 == 0; return d->dataLength > 0 && d->dataLength <= 64; }(); return !d->name.isEmpty() && formatMatch; } /*! Returns the name of the signal. \sa setName(), isValid() */ QString QCanSignalDescription::name() const { return d->name; } /*! Sets the name of the signal to \a name. The signal's name must be unique within a CAN message. \sa name() */ void QCanSignalDescription::setName(const QString &name) { d.detach(); d->name = name; } /*! Returns the physical unit (e.g. km/h) of the signal's value or an empty string if the unit is not set. //! [qcansignaldesc-aux-parameter] This parameter is introduced only for extra description. It's not used during signal processing. //! [qcansignaldesc-aux-parameter] \sa setPhysicalUnit() */ QString QCanSignalDescription::physicalUnit() const { return d->unit; } /*! Sets the physical \a unit (e.g. km/h) of the signal's value. \include qcansignaldescription.cpp qcansignaldesc-aux-parameter \sa physicalUnit() */ void QCanSignalDescription::setPhysicalUnit(const QString &unit) { d.detach(); d->unit = unit; } /*! Returns the receiver node for this signal. \include qcansignaldescription.cpp qcansignaldesc-aux-parameter \sa setReceiver() */ QString QCanSignalDescription::receiver() const { return d->receiver; } /*! Sets the \a receiver node for this signal. \include qcansignaldescription.cpp qcansignaldesc-aux-parameter \sa receiver() */ void QCanSignalDescription::setReceiver(const QString &receiver) { d.detach(); d->receiver = receiver; } /*! Returns the comment for the signal. \include qcansignaldescription.cpp qcansignaldesc-aux-parameter \sa setComment() */ QString QCanSignalDescription::comment() const { return d->comment; } /*! Sets the comment for the signal to \a text. \include qcansignaldescription.cpp qcansignaldesc-aux-parameter \sa comment() */ void QCanSignalDescription::setComment(const QString &text) { d.detach(); d->comment = text; } /*! Returns the data source of the signal's value. By default, \l {QtCanBus::DataSource::}{Payload} is used. \sa setDataSource(), QtCanBus::DataSource */ QtCanBus::DataSource QCanSignalDescription::dataSource() const { return d->source; } /*! Sets the data source of the signal's value to \a source. \sa dataSource(), QtCanBus::DataSource */ void QCanSignalDescription::setDataSource(QtCanBus::DataSource source) { d.detach(); d->source = source; } /*! Returns the data endian of the signal's value. By default, \l {QSysInfo::}{BigEndian} is used. \note The data endian is ignored if the \l dataFormat() is set to \l {QtCanBus::DataFormat::}{AsciiString}. \sa setDataEndian(), QSysInfo::Endian */ QSysInfo::Endian QCanSignalDescription::dataEndian() const { return d->endian; } /*! Sets the data endian of the signal's value to \a endian. \sa dataEndian(), QSysInfo::Endian */ void QCanSignalDescription::setDataEndian(QSysInfo::Endian endian) { d.detach(); d->endian = endian; } /*! Returns the data format of the signal's value. By default, \l {QtCanBus::DataFormat::}{SignedInteger} is used. \sa setDataFormat(), QtCanBus::DataFormat */ QtCanBus::DataFormat QCanSignalDescription::dataFormat() const { return d->format; } /*! Sets the data format of the signal's value to \a format. \sa dataFormat(), QtCanBus::DataFormat */ void QCanSignalDescription::setDataFormat(QtCanBus::DataFormat format) { d.detach(); d->format = format; } /*! Returns the start bit of the signal's value in the \l dataSource(). \sa setStartBit(), bitLength(), setBitLength() */ quint16 QCanSignalDescription::startBit() const { return d->startBit; } /*! Sets the start bit of the signal's value in the \l dataSource() to \a bit. \sa startBit(), bitLength(), setBitLength() */ void QCanSignalDescription::setStartBit(quint16 bit) { d.detach(); d->startBit = bit; } /*! Returns the bit length of the signal's value. \sa setBitLength(), startBit(), setStartBit() */ quint16 QCanSignalDescription::bitLength() const { return d->dataLength; } /*! Sets the bit length of the signal's value to \a length. \sa bitLength(), startBit(), setStartBit() */ void QCanSignalDescription::setBitLength(quint16 length) { d.detach(); d->dataLength = length; } /*! Returns the factor that is used to convert the signal's value to a physical value and back. By default the function returns \l qQNaN(), which means that a factor is not used. The \l {Value Conversions} section explains how this parameter is used. \sa setFactor(), offset(), scaling() */ double QCanSignalDescription::factor() const { return d->factor; } /*! Sets the factor that is used to convert the signal's value to a physical value and back to \a factor. Pass \l qQNaN() to this method to skip this parameter during the conversion. The factor cannot be 0. An attempt to set a zero factor is equivalent to setting it to \l qQNaN(). The \l {Value Conversions} section explains how this parameter is used. \sa factor(), setOffset(), setScaling() */ void QCanSignalDescription::setFactor(double factor) { d.detach(); if (qFuzzyIsNull(factor)) d->factor = qQNaN(); else d->factor = factor; } /*! Returns the offset that is used to convert the signal's value to a physical value and back. By default the function returns \l qQNaN(), which means that an offset is not used. The \l {Value Conversions} section explains how this parameter is used. \sa setOffset(), factor(), scaling() */ double QCanSignalDescription::offset() const { return d->offset; } /*! Sets the offset that is used to convert the signal's value to a physical value and back to \a offset. Pass \l qQNaN() to this method to skip this parameter during the conversion. The \l {Value Conversions} section explains how this parameter is used. \sa offset(), setFactor(), setScaling() */ void QCanSignalDescription::setOffset(double offset) { d.detach(); d->offset = offset; } /*! Returns the scaling that is used to convert the signal's value to a physical value and back. By default the function returns \l qQNaN(), which means that scaling is not used. The \l {Value Conversions} section explains how this parameter is used. \sa setScaling(), offset(), factor() */ double QCanSignalDescription::scaling() const { return d->scaling; } /*! Sets the scaling that is used to convert the signal's value to a physical value and back to \a scaling. Pass \l qQNaN() to this method to skip this parameter during the conversion. The scaling cannot be 0. An attempt to set zero scaling is equivalent to setting it to \l qQNaN(). The \l {Value Conversions} section explains how this parameter is used. \sa scaling(), setOffset(), setFactor() */ void QCanSignalDescription::setScaling(double scaling) { d.detach(); if (qFuzzyIsNull(scaling)) d->scaling = qQNaN(); else d->scaling = scaling; } /*! Returns the minimum supported value for the signal. By default the function returns \l qQNaN(), which means that there is no minimum value. \sa setRange(), maximum() */ double QCanSignalDescription::minimum() const { return d->minimum; } /*! Returns the maximum supported value for the signal. By default the function returns \l qQNaN(), which means that there is no maximum value. \sa setRange(), minimum() */ double QCanSignalDescription::maximum() const { return d->maximum; } /*! Sets the \a minimum and \a maximum for the signal's value. Setting one or both of the parameters to \l qQNaN() means that the corresponding limit will not be used. \sa minimum(), maximum() */ void QCanSignalDescription::setRange(double minimum, double maximum) { d.detach(); if (qIsNaN(minimum) || qIsNaN(maximum) || minimum <= maximum) { d->minimum = minimum; d->maximum = maximum; } else { qWarning("Minimum value is greater than maximum. The values will be swapped."); d->minimum = maximum; d->maximum = minimum; } } /*! Returns the multiplex state of the signal. See the \l {Multiplexed Signals Explained} section for more details on multiplexed signals. By default this method returns \l {QtCanBus::MultiplexState::}{None}. \sa setMultiplexState(), QtCanBus::MultiplexState */ QtCanBus::MultiplexState QCanSignalDescription::multiplexState() const { return d->muxState; } /*! Sets the multiplex state of the signal to \a state. See the \l {Multiplexed Signals Explained} section for more details on multiplexed signals. \sa multiplexState(), QtCanBus::MultiplexState */ void QCanSignalDescription::setMultiplexState(QtCanBus::MultiplexState state) { d.detach(); d->muxState = state; } /*! Returns the \l {Multiplexed Signals Explained}{multiplexor signals} and their desired values that are used to properly identify this signal. The returned hash contains signal names as keys and respective desired ranges of values as values. This signal's value can be extracted from the payload only when all the signals from the hash have the expected values. \sa multiplexState(), clearMultiplexSignals(), setMultiplexSignals(), addMultiplexSignal() */ QCanSignalDescription::MultiplexSignalValues QCanSignalDescription::multiplexSignals() const { return d->muxSignals; } /*! Removes all \l {Multiplexed Signals Explained}{multiplexor signals} for this signal. \sa multiplexSignals(), setMultiplexSignals(), addMultiplexSignal() */ void QCanSignalDescription::clearMultiplexSignals() { d.detach(); d->muxSignals.clear(); } /*! Sets the \l {Multiplexed Signals Explained}{multiplexor signals} for this signal to \a multiplexorSignals. The \a multiplexorSignals hash \e must contain signal names as keys and respective desired value ranges as values. \sa multiplexState(), multiplexSignals(), clearMultiplexSignals(), addMultiplexSignal() */ void QCanSignalDescription::setMultiplexSignals(const MultiplexSignalValues &multiplexorSignals) { d.detach(); d->muxSignals = multiplexorSignals; } /*! Adds a new \l {Multiplexed Signals Explained}{multiplexor signal} for this signal. The \a name parameter contains the name of the multiplexor signal, and the \a ranges parameter contains the desired value ranges. If this signal already has desired value ranges for the multiplexor signal \a name, the ranges are overwritten. \sa multiplexState(), multiplexSignals(), clearMultiplexSignals(), setMultiplexSignals() */ void QCanSignalDescription::addMultiplexSignal(const QString &name, const MultiplexValues &ranges) { d.detach(); d->muxSignals.insert(name, ranges); } /*! \overload This is a convenience overload for the case when the multiplexor signal is expected to have only one specific value, not a range of values. The \a name parameter contains the name of the multiplexor signal, and the \a value parameter contains the desired value. If this signal already has desired value ranges for the multiplexor signal \a name, the ranges are overwritten. \sa multiplexState(), multiplexSignals(), clearMultiplexSignals(), setMultiplexSignals() */ void QCanSignalDescription::addMultiplexSignal(const QString &name, const QVariant &value) { d.detach(); d->muxSignals.insert(name, { {value, value} }); } #ifndef QT_NO_DEBUG_STREAM QDebug QCanSignalDescription::debugStreaming(QDebug dbg, const QCanSignalDescription &sig) { QDebugStateSaver saver(dbg); dbg.nospace() << "QCanSignalDescription(" << sig.name() << ", Source = " << sig.dataSource() << ", Format = " << sig.dataFormat() << ", Endian = " << sig.dataEndian() << ", StartBit = " << sig.startBit() << ", BitLength = " << sig.bitLength(); if (!sig.physicalUnit().isEmpty()) dbg << ", Units = " << sig.physicalUnit(); if (!sig.receiver().isEmpty()) dbg << ", Receiver = " << sig.receiver(); if (!sig.comment().isEmpty()) dbg << ", Comment = " << sig.comment(); dbg << ", Factor = " << sig.factor() << ", Offset = " << sig.offset() << ", Scaling = " << sig.scaling(); dbg << ", Minimum = " << sig.minimum() << ", Maximum = " << sig.maximum(); dbg << ", Multiplex State = " << sig.multiplexState(); const auto muxSignals = sig.multiplexSignals(); if (!muxSignals.isEmpty()) { dbg << ", Multiplexor Signals: {"; for (auto it = muxSignals.cbegin(); it != muxSignals.cend(); ++it) { if (it != muxSignals.cbegin()) dbg << ", "; dbg << "(" << it.key() << ", " << it.value() << ")"; } dbg << "}"; } dbg << ")"; return dbg; } QDebug QCanSignalDescription::MultiplexValueRange::debugStreaming(QDebug dbg, const MultiplexValueRange &range) { QDebugStateSaver saver(dbg); dbg.nospace() << "MultiplexValueRange(" << range.minimum << ", " << range.maximum << ")"; return dbg; } #endif // QT_NO_DEBUG_STREAM template static bool checkValue(const QVariant &valueVar, const QCanSignalDescription::MultiplexValues &ranges) { const T val = valueVar.value(); for (const auto &pair : ranges) { T min = pair.minimum.value(); T max = pair.maximum.value(); if (min > max) max = std::exchange(min, max); if (val >= min && val <= max) return true; } return false; } bool QCanSignalDescriptionPrivate::muxValueInRange( const QVariant &value, const QCanSignalDescription::MultiplexValues &ranges) const { // Use the current data format to convert QVariant values. // Do we really need it for Float, Double and Ascii? switch (format) { case QtCanBus::DataFormat::SignedInteger: return checkValue(value, ranges); case QtCanBus::DataFormat::UnsignedInteger: return checkValue(value, ranges); case QtCanBus::DataFormat::Float: return checkValue(value, ranges); case QtCanBus::DataFormat::Double: return checkValue(value, ranges); case QtCanBus::DataFormat::AsciiString: return checkValue(value, ranges); } Q_UNREACHABLE_RETURN(false); } QCanSignalDescriptionPrivate *QCanSignalDescriptionPrivate::get(const QCanSignalDescription &desc) { return desc.d.data(); } QT_END_NAMESPACE