Source code
Revision control
Copy as Markdown
// 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
// Qt-Security score:significant reason:default
#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.
\section1 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.
\section1 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.
\section2 Little Endian
For the little endian case the data layout can be represented by the
following image:
\image canbus_signals_le.png
{Little endian CAN signal bit layout: blue and orange blocks
showing two values across bytes 0-2 with LSB and MSB positions}
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
\section2 Big Endian
The following image represents the value layout for the big endian case:
\image canbus_signals_be.png
{Big endian CAN signal bit layout: blue and orange blocks
showing two values across bytes 0-2 with MSB and LSB positions}
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.
\section1 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.
\section1 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 <typename T>
static bool checkValue(const QVariant &valueVar,
const QCanSignalDescription::MultiplexValues &ranges)
{
const T val = valueVar.value<T>();
for (const auto &pair : ranges) {
T min = pair.minimum.value<T>();
T max = pair.maximum.value<T>();
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<qint64>(value, ranges);
case QtCanBus::DataFormat::UnsignedInteger:
return checkValue<quint64>(value, ranges);
case QtCanBus::DataFormat::Float:
return checkValue<float>(value, ranges);
case QtCanBus::DataFormat::Double:
return checkValue<double>(value, ranges);
case QtCanBus::DataFormat::AsciiString:
return checkValue<QByteArray>(value, ranges);
}
Q_UNREACHABLE_RETURN(false);
}
QCanSignalDescriptionPrivate *QCanSignalDescriptionPrivate::get(const QCanSignalDescription &desc)
{
return desc.d.data();
}
QT_END_NAMESPACE