From 9a2ae3664d39cabf991a30324a110d1e3db837c2 Mon Sep 17 00:00:00 2001 From: dontgetfoundout Date: Wed, 6 Dec 2023 20:04:07 -0800 Subject: [PATCH 1/4] Highlight changed bytes in Frames view when Overwrite mode is enabled --- SavvyCAN.pro | 6 +- can_structs.h | 8 ++- candataitemdelegate.cpp | 146 ++++++++++++++++++++++++++++++++++++++++ candataitemdelegate.h | 23 +++++++ canframemodel.cpp | 95 +++++++++----------------- canframemodel.h | 3 + mainwindow.cpp | 4 ++ utility.h | 1 + 8 files changed, 218 insertions(+), 68 deletions(-) create mode 100644 candataitemdelegate.cpp create mode 100644 candataitemdelegate.h diff --git a/SavvyCAN.pro b/SavvyCAN.pro index 3b782a9d..293dc465 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -100,7 +100,8 @@ SOURCES += main.cpp\ connections/newconnectiondialog.cpp \ re/temporalgraphwindow.cpp \ filterutility.cpp \ - pcaplite.cpp + pcaplite.cpp \ + candataitemdelegate.cpp HEADERS += mainwindow.h \ can_structs.h \ @@ -194,7 +195,8 @@ HEADERS += mainwindow.h \ connections/newconnectiondialog.h \ re/temporalgraphwindow.h \ filterutility.h \ - pcaplite.h + pcaplite.h \ + candataitemdelegate.h FORMS += ui/candatagrid.ui \ triggerdialog.ui \ diff --git a/can_structs.h b/can_structs.h index 6b8cdee9..a613e612 100644 --- a/can_structs.h +++ b/can_structs.h @@ -5,6 +5,7 @@ #include #include #include +#include //Now inherits from the built-in CAN frame class from Qt. This should be more future proof and easier to integrate with other code @@ -14,7 +15,10 @@ struct CANFrame : public QCanBusFrame int bus; bool isReceived; //did we receive this or send it? uint64_t timedelta; - uint32_t frameCount; //used in overwrite mode + + //used in overwrite mode + uint32_t frameCount; + uint8_t changedPayloadBytes; friend bool operator<(const CANFrame& l, const CANFrame& r) { @@ -35,6 +39,8 @@ struct CANFrame : public QCanBusFrame } }; +Q_DECLARE_METATYPE(CANFrame); + class CANFltObserver { public: diff --git a/candataitemdelegate.cpp b/candataitemdelegate.cpp new file mode 100644 index 00000000..a371aaf3 --- /dev/null +++ b/candataitemdelegate.cpp @@ -0,0 +1,146 @@ + + +#include "candataitemdelegate.h" +#include +#include +#include "can_structs.h" + +#define DATA_ITEM_LEFT_PADDING 5 + +CanDataItemDelegate::CanDataItemDelegate(CANFrameModel *_model) : QItemDelegate() { + + dbcHandler = DBCHandler::getReference(); + model = _model; +} + +void CanDataItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const +{ + const auto frame = index.data().value(); + const unsigned char *data = reinterpret_cast(frame.payload().constData()); + + bool overwriteDups = model->getOverwriteMode(); + int bytesPerLine = model->getBytesPerLine(); + bool useHexMode = model->getHexMode(); + + // cache the painter state so it can be restored after this item is done painting + painter->save(); + QStyleOptionViewItem opt = setOptions(index, option); + drawBackground(painter, opt, index); + + auto pen = painter->pen(); + const auto defaultColor = pen.color(); + + // FontMetrics are used to measure the bounding rects for drawn text, allowing + // us to properly position subsequent draw calls + const auto fm = painter->fontMetrics(); + + int dataLen = frame.payload().count(); + if (dataLen < 0) dataLen = 0; + if (frame.frameType() != QCanBusFrame::RemoteRequestFrame) { + QRect boundingRect; + QPoint startPoint = option.rect.topLeft(); + const auto charBounds = fm.boundingRect("0"); + int x = startPoint.x() + DATA_ITEM_LEFT_PADDING; + int y = startPoint.y() + charBounds.height(); + + // Draw each data byte manually, advancing the start point as we go + // This allows us to change the styling of individual draw calls. + // We use this to highlight data bytes that have changed since the last + // frame when overwriteDups mode is on. + for (int i = 0; i < dataLen; i++) + { + if((frame.changedPayloadBytes & (1 << i)) && overwriteDups) { + painter->setPen(Qt::red); + } + else { + painter->setPen(defaultColor); + } + + QString dataStr = + useHexMode ? + QString::number(data[i], 16).toUpper().rightJustified(2, '0') : + QString::number(data[i], 10); + const auto bounds = fm.boundingRect(dataStr); + + painter->drawText(x, y, dataStr); + + x += bounds.width(); + + if (!((i+1) % bytesPerLine) && (i != (dataLen - 1))) { + // Wrap to next line + y += bounds.height(); + x = startPoint.x() + DATA_ITEM_LEFT_PADDING; + } + else { + // Advance a character width for a space + x += charBounds.width(); + } + } + + painter->setPen(defaultColor); + + QString tempString; + buildStringFromFrame(frame, tempString); + + if (tempString.length()) + painter->drawText(x, y, tempString); + } + + drawFocus(painter, opt, option.rect); + painter->restore(); +} + +void CanDataItemDelegate::buildStringFromFrame(CANFrame frame, QString &tempString) const { + bool interpretFrames = model->getInterpretMode(); + bool overwriteDups = model->getOverwriteMode(); + tempString.append(""); + if (frame.frameType() == frame.ErrorFrame) + { + if (frame.error() & frame.TransmissionTimeoutError) tempString.append("\nTX Timeout"); + if (frame.error() & frame.LostArbitrationError) tempString.append("\nLost Arbitration"); + if (frame.error() & frame.ControllerError) tempString.append("\nController Error"); + if (frame.error() & frame.ProtocolViolationError) tempString.append("\nProtocol Violation"); + if (frame.error() & frame.TransceiverError) tempString.append("\nTransceiver Error"); + if (frame.error() & frame.MissingAcknowledgmentError) tempString.append("\nMissing ACK"); + if (frame.error() & frame.BusOffError) tempString.append("\nBus OFF"); + if (frame.error() & frame.BusError) tempString.append("\nBus ERR"); + if (frame.error() & frame.ControllerRestartError) tempString.append("\nController restart err"); + if (frame.error() & frame.UnknownError) tempString.append("\nUnknown error type"); + } + //TODO: technically the actual returned bytes for an error frame encode some more info. Not interpreting it yet. + + //now, if we're supposed to interpret the data and the DBC handler is loaded then use it + if ( (dbcHandler != nullptr) && interpretFrames && (frame.frameType() == frame.DataFrame) ) + { + DBC_MESSAGE *msg = dbcHandler->findMessage(frame); + if (msg != nullptr) + { + tempString.append(" <" + msg->name + ">\n"); + if (msg->comment.length() > 1) tempString.append(msg->comment + "\n"); + for (int j = 0; j < msg->sigHandler->getCount(); j++) + { + QString sigString; + DBC_SIGNAL* sig = msg->sigHandler->findSignalByIdx(j); + + if ( (sig->multiplexParent == nullptr) && sig->processAsText(frame, sigString)) + { + tempString.append(sigString); + tempString.append("\n"); + if (sig->isMultiplexor) + { + qDebug() << "Multiplexor. Diving into the tree"; + tempString.append(sig->processSignalTree(frame)); + } + } + else if (sig->isMultiplexed && overwriteDups) //wasn't in this exact frame but is in the message. Use cached value + { + bool isInteger = false; + if (sig->valType == UNSIGNED_INT || sig->valType == SIGNED_INT) isInteger = true; + tempString.append(sig->makePrettyOutput(sig->cachedValue.toDouble(), sig->cachedValue.toLongLong(), true, isInteger)); + tempString.append("\n"); + } + } + } + } +} \ No newline at end of file diff --git a/candataitemdelegate.h b/candataitemdelegate.h new file mode 100644 index 00000000..d60cb6ab --- /dev/null +++ b/candataitemdelegate.h @@ -0,0 +1,23 @@ +#ifndef CANDATAITEMDELEGATE_H +#define CANDATAITEMDELEGATE_H + +#include +#include "dbc/dbchandler.h" +#include "canframemodel.h" + +class CanDataItemDelegate : public QItemDelegate +{ +public: + using QItemDelegate::QItemDelegate; + CanDataItemDelegate(CANFrameModel *model); + + void paint(QPainter *painter, const QStyleOptionViewItem &option, + const QModelIndex &index) const override; + +private: + void buildStringFromFrame(CANFrame frame, QString &tempString) const; + DBCHandler *dbcHandler; + CANFrameModel *model; +}; + +#endif \ No newline at end of file diff --git a/canframemodel.cpp b/canframemodel.cpp index ef2ccf87..f16066e9 100644 --- a/canframemodel.cpp +++ b/canframemodel.cpp @@ -90,6 +90,11 @@ void CANFrameModel::setBytesPerLine(int bpl) bytesPerLine = bpl; } +int CANFrameModel::getBytesPerLine() +{ + return bytesPerLine; +} + void CANFrameModel::setHexMode(bool mode) { if (useHexMode != mode) @@ -101,6 +106,11 @@ void CANFrameModel::setHexMode(bool mode) } } +bool CANFrameModel::getHexMode() +{ + return useHexMode; +} + void CANFrameModel::setTimeStyle(TimeStyle newStyle) { if (timeStyle != newStyle) @@ -195,6 +205,11 @@ void CANFrameModel::setOverwriteMode(bool mode) endResetModel(); } +bool CANFrameModel::getOverwriteMode() +{ + return overwriteDups; +} + void CANFrameModel::setClearMode(bool mode) { filtersPersistDuringClear = mode; @@ -342,6 +357,7 @@ void CANFrameModel::sortByColumn(int column) //End of custom sorting code +// TODO - update this as well? void CANFrameModel::recalcOverwrite() { if (!overwriteDups) return; //no need to do a thing if mode is disabled @@ -412,7 +428,6 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const thisFrame = filteredFrames.at(index.row()); - const unsigned char *data = reinterpret_cast(thisFrame.payload().constData()); int dataLen = thisFrame.payload().count(); if (role == Qt::BackgroundRole) @@ -516,67 +531,7 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const } return tempString; case Column::Data: - if (dataLen < 0) dataLen = 0; - //if (useHexMode) tempString.append("0x "); - if (thisFrame.frameType() == QCanBusFrame::RemoteRequestFrame) { - return tempString; - } - for (int i = 0; i < dataLen; i++) - { - if (useHexMode) tempString.append( QString::number(data[i], 16).toUpper().rightJustified(2, '0')); - else tempString.append(QString::number(data[i], 10)); - if (!((i+1) % bytesPerLine) && (i != (dataLen - 1))) tempString.append("\n"); - else tempString.append(" "); - } - if (thisFrame.frameType() == thisFrame.ErrorFrame) - { - if (thisFrame.error() & thisFrame.TransmissionTimeoutError) tempString.append("\nTX Timeout"); - if (thisFrame.error() & thisFrame.LostArbitrationError) tempString.append("\nLost Arbitration"); - if (thisFrame.error() & thisFrame.ControllerError) tempString.append("\nController Error"); - if (thisFrame.error() & thisFrame.ProtocolViolationError) tempString.append("\nProtocol Violation"); - if (thisFrame.error() & thisFrame.TransceiverError) tempString.append("\nTransceiver Error"); - if (thisFrame.error() & thisFrame.MissingAcknowledgmentError) tempString.append("\nMissing ACK"); - if (thisFrame.error() & thisFrame.BusOffError) tempString.append("\nBus OFF"); - if (thisFrame.error() & thisFrame.BusError) tempString.append("\nBus ERR"); - if (thisFrame.error() & thisFrame.ControllerRestartError) tempString.append("\nController restart err"); - if (thisFrame.error() & thisFrame.UnknownError) tempString.append("\nUnknown error type"); - } - //TODO: technically the actual returned bytes for an error frame encode some more info. Not interpreting it yet. - - //now, if we're supposed to interpret the data and the DBC handler is loaded then use it - if ( (dbcHandler != nullptr) && interpretFrames && (thisFrame.frameType() == thisFrame.DataFrame) ) - { - DBC_MESSAGE *msg = dbcHandler->findMessage(thisFrame); - if (msg != nullptr) - { - tempString.append(" <" + msg->name + ">\n"); - if (msg->comment.length() > 1) tempString.append(msg->comment + "\n"); - for (int j = 0; j < msg->sigHandler->getCount(); j++) - { - QString sigString; - DBC_SIGNAL* sig = msg->sigHandler->findSignalByIdx(j); - - if ( (sig->multiplexParent == nullptr) && sig->processAsText(thisFrame, sigString)) - { - tempString.append(sigString); - tempString.append("\n"); - if (sig->isMultiplexor) - { - qDebug() << "Multiplexor. Diving into the tree"; - tempString.append(sig->processSignalTree(thisFrame)); - } - } - else if (sig->isMultiplexed && overwriteDups) //wasn't in this exact frame but is in the message. Use cached value - { - bool isInteger = false; - if (sig->valType == UNSIGNED_INT || sig->valType == SIGNED_INT) isInteger = true; - tempString.append(sig->makePrettyOutput(sig->cachedValue.toDouble(), sig->cachedValue.toLongLong(), true, isInteger)); - tempString.append("\n"); - } - } - } - } - return tempString; + return QVariant::fromValue(thisFrame); default: return tempString; } @@ -719,10 +674,20 @@ void CANFrameModel::addFrame(const CANFrame& frame, bool autoRefresh = false) // } for (int i = 0; i < filteredFrames.count(); i++) { - if ( (filteredFrames[i].frameId() == tempFrame.frameId()) && (filteredFrames[i].bus == tempFrame.bus) ) + CANFrame currentFrame = filteredFrames[i]; + if ( (currentFrame.frameId() == tempFrame.frameId()) && (currentFrame.bus == tempFrame.bus) ) { - tempFrame.frameCount = filteredFrames[i].frameCount + 1; - tempFrame.timedelta = tempFrame.timeStamp().microSeconds() - filteredFrames[i].timeStamp().microSeconds(); + tempFrame.frameCount = currentFrame.frameCount + 1; + tempFrame.timedelta = tempFrame.timeStamp().microSeconds() - currentFrame.timeStamp().microSeconds(); + + // Keep track of changed bytes so we can draw them in a different color in CanDataItemDelegate + QByteArray curPayload = currentFrame.payload(); + QByteArray tempPayload = tempFrame.payload(); + + for (int i = 0; i < curPayload.size() && i < tempPayload.size(); i++) { + tempFrame.changedPayloadBytes |= (curPayload[i] != tempPayload[i]) << i; + } + filteredFrames.replace(i, tempFrame); found = true; break; diff --git a/canframemodel.h b/canframemodel.h index 6a6bc54f..bec04ea5 100644 --- a/canframemodel.h +++ b/canframemodel.h @@ -46,7 +46,9 @@ class CANFrameModel: public QAbstractTableModel void setInterpretMode(bool); bool getInterpretMode(); void setOverwriteMode(bool); + bool getOverwriteMode(); void setHexMode(bool); + bool getHexMode(); void setClearMode(bool mode); void setTimeStyle(TimeStyle newStyle); void setIgnoreDBCColors(bool mode); @@ -55,6 +57,7 @@ class CANFrameModel: public QAbstractTableModel void setAllFilters(bool state); void setTimeFormat(QString); void setBytesPerLine(int bpl); + int getBytesPerLine(); void loadFilterFile(QString filename); void saveFilterFile(QString filename); void normalizeTiming(); diff --git a/mainwindow.cpp b/mainwindow.cpp index b2718bd6..7ac17ea2 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -9,6 +9,7 @@ #include "helpwindow.h" #include "utility.h" #include "filterutility.h" +#include "candataitemdelegate.h" /* Some notes on things I'd like to put into the program but haven't put on github (yet) @@ -48,6 +49,9 @@ MainWindow::MainWindow(QWidget *parent) : ui->canFramesView->setModel(proxyModel); + CanDataItemDelegate* dataColumnFormatter = new CanDataItemDelegate(model); + ui->canFramesView->setItemDelegateForColumn(8, dataColumnFormatter); + settingsDialog = new MainSettingsDialog(); //instantiate the settings dialog so it can initialize settings if this is the first run or the config file was deleted. settingsDialog->updateSettings(); //write out all the settings. If this is the first run it'll write defaults out. diff --git a/utility.h b/utility.h index 01abc3da..60bc4f49 100644 --- a/utility.h +++ b/utility.h @@ -184,6 +184,7 @@ class Utility return (double)timestamp / 1000.0; break; case TS_MICROS: + default: return (unsigned long long)(timestamp); break; case TS_SECONDS: From 8b9b4dd3dfefbbae1e1574d55055c541bd94e3fc Mon Sep 17 00:00:00 2001 From: dontgetfoundout Date: Wed, 6 Dec 2023 20:14:32 -0800 Subject: [PATCH 2/4] Remove TODO --- canframemodel.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/canframemodel.cpp b/canframemodel.cpp index f16066e9..ee588176 100644 --- a/canframemodel.cpp +++ b/canframemodel.cpp @@ -357,7 +357,6 @@ void CANFrameModel::sortByColumn(int column) //End of custom sorting code -// TODO - update this as well? void CANFrameModel::recalcOverwrite() { if (!overwriteDups) return; //no need to do a thing if mode is disabled From eebb69f57726c2fc0f04046a32c3e43ea581f3fc Mon Sep 17 00:00:00 2001 From: dontgetfoundout Date: Wed, 6 Dec 2023 22:08:13 -0800 Subject: [PATCH 3/4] Add a setting to control overwrite mode highlighting behavior --- candataitemdelegate.cpp | 5 +++-- canframemodel.cpp | 12 ++++++++++++ canframemodel.h | 3 +++ mainsettingsdialog.cpp | 3 +++ mainwindow.cpp | 2 ++ ui/mainsettingsdialog.ui | 8 ++++++++ 6 files changed, 31 insertions(+), 2 deletions(-) diff --git a/candataitemdelegate.cpp b/candataitemdelegate.cpp index a371aaf3..86c0d6d3 100644 --- a/candataitemdelegate.cpp +++ b/candataitemdelegate.cpp @@ -20,6 +20,7 @@ void CanDataItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o const unsigned char *data = reinterpret_cast(frame.payload().constData()); bool overwriteDups = model->getOverwriteMode(); + bool markChangedBytes = model->getMarkChangedBytes(); int bytesPerLine = model->getBytesPerLine(); bool useHexMode = model->getHexMode(); @@ -50,7 +51,7 @@ void CanDataItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o // frame when overwriteDups mode is on. for (int i = 0; i < dataLen; i++) { - if((frame.changedPayloadBytes & (1 << i)) && overwriteDups) { + if((frame.changedPayloadBytes & (1 << i)) && overwriteDups && markChangedBytes) { painter->setPen(Qt::red); } else { @@ -77,7 +78,7 @@ void CanDataItemDelegate::paint(QPainter *painter, const QStyleOptionViewItem &o x += charBounds.width(); } } - + painter->setPen(defaultColor); QString tempString; diff --git a/canframemodel.cpp b/canframemodel.cpp index ee588176..f70345c5 100644 --- a/canframemodel.cpp +++ b/canframemodel.cpp @@ -210,6 +210,18 @@ bool CANFrameModel::getOverwriteMode() return overwriteDups; } +void CANFrameModel::setMarkChangedBytes(bool mcf) +{ + beginResetModel(); + markChangedBytes = mcf; + endResetModel(); +} + +bool CANFrameModel::getMarkChangedBytes() +{ + return markChangedBytes; +} + void CANFrameModel::setClearMode(bool mode) { filtersPersistDuringClear = mode; diff --git a/canframemodel.h b/canframemodel.h index bec04ea5..8e9f2b0b 100644 --- a/canframemodel.h +++ b/canframemodel.h @@ -47,6 +47,8 @@ class CANFrameModel: public QAbstractTableModel bool getInterpretMode(); void setOverwriteMode(bool); bool getOverwriteMode(); + void setMarkChangedBytes(bool); + bool getMarkChangedBytes(); void setHexMode(bool); bool getHexMode(); void setClearMode(bool mode); @@ -93,6 +95,7 @@ public slots: QMutex mutex; bool interpretFrames; //should we use the dbcHandler? bool overwriteDups; //should we display all frames or only the newest for each ID? + bool markChangedBytes; //should we highlight bytes that have been changed from the baseline? bool filtersPersistDuringClear; QString timeFormat; TimeStyle timeStyle; diff --git a/mainsettingsdialog.cpp b/mainsettingsdialog.cpp index eae6272c..1e8394cf 100644 --- a/mainsettingsdialog.cpp +++ b/mainsettingsdialog.cpp @@ -48,6 +48,7 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) : ui->cbLoadConnections->setChecked(settings.value("Main/SaveRestoreConnections", false).toBool()); ui->spinFontSize->setValue(settings.value("Main/FontSize", ui->cbDisplayHex->font().pointSize()).toUInt()); + ui->cbMarkChangedBytes->setChecked(settings.value("Main/MarkChangedBytes", true).toBool()); ui->cbFontFixedWidth->setChecked(settings.value("Main/FontFixedWidth", false).toBool()); bool secondsMode = settings.value("Main/TimeSeconds", false).toBool(); @@ -138,6 +139,7 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) : connect(ui->spinMaximumFrames, SIGNAL(valueChanged(int)), this, SLOT(updateSettings())); connect(ui->cbFontFixedWidth, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); connect(ui->spinBytesPerLine, SIGNAL(valueChanged(int)), this, SLOT(updateSettings())); + connect(ui->cbMarkChangedBytes, SIGNAL(toggled(bool)), this, SLOT(updateSettings())); installEventFilter(this); } @@ -207,6 +209,7 @@ void MainSettingsDialog::updateSettings() settings.setValue("Main/IgnoreDBCColors", ui->cbIgnoreDBCColors->isChecked()); settings.setValue("Main/MaximumFrames", ui->spinMaximumFrames->value()); settings.setValue("Main/BytesPerLine", ui->spinBytesPerLine->value()); + settings.setValue("Main/MarkChangedBytes", ui->cbMarkChangedBytes->isChecked()); settings.setValue("Main/FontFixedWidth", ui->cbFontFixedWidth->isChecked()); settings.sync(); diff --git a/mainwindow.cpp b/mainwindow.cpp index 7ac17ea2..a74b4d8e 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -408,6 +408,8 @@ void MainWindow::readUpdateableSettings() model->setIgnoreDBCColors(ignoreDBCColors); int bpl = settings.value("Main/BytesPerLine", 8).toInt(); model->setBytesPerLine(bpl); + int mcb = settings.value("Main/MarkChangedBytes", true).toBool(); + model->setMarkChangedBytes(mcb); CSVAbsTime = settings.value("Main/CSVAbsTime", false).toBool(); diff --git a/ui/mainsettingsdialog.ui b/ui/mainsettingsdialog.ui index e65e91cd..3c6a6960 100644 --- a/ui/mainsettingsdialog.ui +++ b/ui/mainsettingsdialog.ui @@ -53,6 +53,13 @@ + + + + Highlight changed bytes in Overwrite mode + + + @@ -484,6 +491,7 @@ cbMainAutoScroll + cbMarkChangedBytes cbRestorePositions cbLoadConnections cbDisplayHex From af579b38d5789fc33ca8cba8df8a1f89fc1779f1 Mon Sep 17 00:00:00 2001 From: dontgetfoundout Date: Wed, 6 Dec 2023 22:46:43 -0800 Subject: [PATCH 4/4] Increase size of changed bytes bit field --- can_structs.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/can_structs.h b/can_structs.h index a613e612..5b50a24d 100644 --- a/can_structs.h +++ b/can_structs.h @@ -18,7 +18,7 @@ struct CANFrame : public QCanBusFrame //used in overwrite mode uint32_t frameCount; - uint8_t changedPayloadBytes; + uint64_t changedPayloadBytes; friend bool operator<(const CANFrame& l, const CANFrame& r) {