From 23cd6d9dfee90261161181c2f87d0a511d791391 Mon Sep 17 00:00:00 2001 From: Charles Galpin Date: Sun, 24 Dec 2023 10:45:56 -0500 Subject: [PATCH 01/57] add save as, reload, and save log buttons to scriptingwindow --- .vscode/settings.json | 8 ++++ scriptingwindow.cpp | 89 +++++++++++++++++++++++++++++++++++++++++++ scriptingwindow.h | 3 ++ ui/scriptingwindow.ui | 42 +++++++++++++++++--- 4 files changed, 137 insertions(+), 5 deletions(-) create mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..936eb86 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,8 @@ +{ + "files.associations": { + "thread": "cpp", + "*.tcc": "cpp", + "chrono": "cpp", + "algorithm": "cpp" + } +} \ No newline at end of file diff --git a/scriptingwindow.cpp b/scriptingwindow.cpp index 1f24dee..ea254c3 100644 --- a/scriptingwindow.cpp +++ b/scriptingwindow.cpp @@ -41,8 +41,11 @@ ScriptingWindow::ScriptingWindow(const QVector *frames, QWidget *paren connect(ui->btnRecompile, &QAbstractButton::pressed, this, &ScriptingWindow::recompileScript); connect(ui->btnRemoveScript, &QAbstractButton::pressed, this, &ScriptingWindow::deleteCurrentScript); connect(ui->btnRevertScript, &QAbstractButton::pressed, this, &ScriptingWindow::revertScript); + connect(ui->btnReloadScript, &QAbstractButton::pressed, this, &ScriptingWindow::reloadScript); connect(ui->btnSaveScript, &QAbstractButton::pressed, this, &ScriptingWindow::saveScript); + connect(ui->btnSaveAsScript, &QAbstractButton::pressed, this, &ScriptingWindow::saveAsScript); connect(ui->btnClearLog, &QAbstractButton::pressed, this, &ScriptingWindow::clickedLogClear); + connect(ui->btnSaveLog, &QAbstractButton::pressed, this, &ScriptingWindow::saveLog); connect(ui->listLoadedScripts, &QListWidget::currentRowChanged, this, &ScriptingWindow::changeCurrentScript); connect(ui->tableVariables, SIGNAL(cellChanged(int,int)), this, SLOT(updatedValue(int, int))); @@ -301,6 +304,21 @@ void ScriptingWindow::refreshSourceWindow() } void ScriptingWindow::saveScript() +{ + QFile *outFile = new QFile(currentScript->filePath); + + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) + { + delete outFile; + return; + } + outFile->write(editor->toPlainText().toUtf8()); + currentScript->scriptText = editor->toPlainText(); + outFile->close(); + delete outFile; +} + +void ScriptingWindow::saveAsScript() { QString filename; QFileDialog dialog(this); @@ -358,6 +376,38 @@ void ScriptingWindow::revertScript() } } +void ScriptingWindow::reloadScript() +{ + QMessageBox msgBox; + msgBox.setText("Are you sure you'd like to reload from disk?"); + msgBox.setInformativeText("Really do it?"); + msgBox.setStandardButtons(QMessageBox::Yes | QMessageBox::No); + msgBox.setDefaultButton(QMessageBox::Cancel); + int ret = msgBox.exec(); + switch (ret) + { + case QMessageBox::Yes: + { + // get the latest version from disk and set in the editor/state + QFile scriptFile(currentScript->filePath); + if (scriptFile.open(QIODevice::ReadOnly | QIODevice::Text)) + { + QString contents = scriptFile.readAll(); + scriptFile.close(); + editor->setPlainText(contents); + currentScript->scriptText = contents; + currentScript->compileScript(); + } + break; + } + case QMessageBox::No: + break; + default: + // should never be reached + break; + } +} + void ScriptingWindow::recompileScript() { if (currentScript) @@ -373,6 +423,45 @@ void ScriptingWindow::clickedLogClear() elapsedTime.start(); } +void ScriptingWindow::saveLog() +{ + QString filename; + QFileDialog dialog(this); + QSettings settings; + + QStringList filters; + filters.append(QString(tr("Log File (*.log)"))); + + dialog.setDirectory(settings.value("ScriptingWindow/LoadSaveDirectory", dialog.directory().path()).toString()); + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setNameFilters(filters); + dialog.setViewMode(QFileDialog::Detail); + dialog.setAcceptMode(QFileDialog::AcceptSave); + + if (dialog.exec() == QDialog::Accepted) + { + filename = dialog.selectedFiles()[0]; + if (!filename.contains('.')) filename += ".log"; + if (dialog.selectedNameFilter() == filters[0]) + { + QFile *outFile = new QFile(filename); + + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) + { + delete outFile; + return; + } + int c = ui->listLog ->count(); + for (int row = 0; row < c; row++) { + outFile->write(ui->listLog->item(row)->data(Qt::DisplayRole).toString().toUtf8() + "\n"); + } + outFile->close(); + delete outFile; + settings.setValue("ScriptingWindow/LoadSaveDirectory", dialog.directory().path()); + } + } +} + void ScriptingWindow::log(QString text) { ScriptContainer *cont = qobject_cast(sender()); diff --git a/scriptingwindow.h b/scriptingwindow.h index 156a72e..5577099 100644 --- a/scriptingwindow.h +++ b/scriptingwindow.h @@ -37,7 +37,9 @@ private slots: void deleteCurrentScript(); void refreshSourceWindow(); void saveScript(); + void saveAsScript(); void revertScript(); + void reloadScript(); void recompileScript(); void changeCurrentScript(); void newFrames(const CANConnection*, const QVector&); @@ -49,6 +51,7 @@ private: void closeEvent(QCloseEvent *event); void readSettings(); void writeSettings(); + void saveLog(); bool eventFilter(QObject *obj, QEvent *event); Ui::ScriptingWindow *ui; diff --git a/ui/scriptingwindow.ui b/ui/scriptingwindow.ui index 35c0b9e..5623804 100644 --- a/ui/scriptingwindow.ui +++ b/ui/scriptingwindow.ui @@ -108,6 +108,13 @@ + + + + Sa&ve As + + + @@ -115,6 +122,13 @@ + + + + R&eload + + + @@ -151,11 +165,26 @@ - - - Clear Log &Window - - + + + Qt::AlignCenter + + 10, 0, 10, 0 + + + + Clear Log &Window + + + + + + + Save L&og + + + + @@ -169,10 +198,13 @@ listLoadedScripts tableVariables btnSaveScript + btnSaveAsScript btnRevertScript + btnReloadScript btnRecompile cbAutoScroll btnClearLog + btnSaveLog From 81b1afd31e87e889453bf6be5728bcb464934b5d Mon Sep 17 00:00:00 2001 From: Charles Galpin Date: Sun, 24 Dec 2023 10:55:37 -0500 Subject: [PATCH 02/57] exclude .vscode from git --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 6cb0bf6..13068cd 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,4 @@ Makefile SavvyCAN.pro.qtds .xcode SavvyCAN.xcodeproj +.vscode From 4c8f5f979376b6b59977f5cf9b1f4830d843dc5e Mon Sep 17 00:00:00 2001 From: Charles Galpin Date: Sun, 24 Dec 2023 10:56:58 -0500 Subject: [PATCH 03/57] Delete .vscode/settings.json unintentional commit --- .vscode/settings.json | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .vscode/settings.json diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index 936eb86..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "files.associations": { - "thread": "cpp", - "*.tcc": "cpp", - "chrono": "cpp", - "algorithm": "cpp" - } -} \ No newline at end of file From 854f9c914f2322ebf4d5cdb12517610745fab537 Mon Sep 17 00:00:00 2001 From: Miklos Marton Date: Thu, 18 Jan 2024 16:53:13 +0100 Subject: [PATCH 04/57] Do not add extra newline on the DBC decoded message texts --- dbc/dbc_classes.cpp | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/dbc/dbc_classes.cpp b/dbc/dbc_classes.cpp index 09b7cb1..da44f80 100644 --- a/dbc/dbc_classes.cpp +++ b/dbc/dbc_classes.cpp @@ -76,12 +76,16 @@ QString DBC_SIGNAL::processSignalTree(const CANFrame &frame) if (sig->processAsText(frame, sigString)) { qDebug() << "Returned value: " << sigString; + if (!build.isEmpty() && !sigString.isEmpty()) + build.append("\n"); build.append(sigString); - build.append("\n"); if (sig->isMultiplexor) { qDebug() << "Spelunkin!"; - build.append(sig->processSignalTree(frame)); + auto subTreeString = sig->processSignalTree(frame); + if (!build.isEmpty() && !subTreeString.isEmpty()) + build.append("\n"); + build.append(subTreeString); } } } From a6cd2f1baa7f61f0cd80c07353e0c4bf83569a8f Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 12:42:12 +0400 Subject: [PATCH 05/57] Update required C++ standard to C++17 (needed by [[fallthrough]] statements) --- SavvyCAN.pro | 2 +- connections/lawicel_serial.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/SavvyCAN.pro b/SavvyCAN.pro index 3b782a9..def354a 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -8,7 +8,7 @@ QT = core gui printsupport qml serialbus serialport widgets help network opengl CONFIG(release, debug|release):DEFINES += QT_NO_DEBUG_OUTPUT -CONFIG += c++11 +CONFIG += c++17 CONFIG += NO_UNIT_TESTS DEFINES += QCUSTOMPLOT_USE_OPENGL diff --git a/connections/lawicel_serial.cpp b/connections/lawicel_serial.cpp index 0b502f8..34b4fd4 100644 --- a/connections/lawicel_serial.cpp +++ b/connections/lawicel_serial.cpp @@ -551,6 +551,7 @@ void LAWICELSerial::readSerialData() break; case 'b': buildFrame.setBitrateSwitch(true); //BRS enabled + [[fallthrough]]; case 'd': //standard fd frame, BRS disabled //tIIILDD buildFrame.setFlexibleDataRateFormat(true); @@ -581,6 +582,7 @@ void LAWICELSerial::readSerialData() break; case 'B': buildFrame.setBitrateSwitch(true); //BRS enabled + [[fallthrough]]; case 'D': //extended fd frame //TIIIIIIIILDD. buildFrame.setFlexibleDataRateFormat(true); From 54151327eb9cdf3dca510ebf284860c342738090 Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 10:57:27 +0400 Subject: [PATCH 06/57] FirmwareUploader: fix memory leaks --- firmwareuploaderwindow.cpp | 73 ++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 38 deletions(-) diff --git a/firmwareuploaderwindow.cpp b/firmwareuploaderwindow.cpp index 05028be..0a6be75 100644 --- a/firmwareuploaderwindow.cpp +++ b/firmwareuploaderwindow.cpp @@ -133,38 +133,38 @@ void FirmwareUploaderWindow::timerElapsed() void FirmwareUploaderWindow::sendFirmwareChunk() { - CANFrame *output = new CANFrame; + CANFrame output; int firmwareLocation = currentSendingPosition * 4; int xorByte = 0; - output->setExtendedFrameFormat(false); + output.setExtendedFrameFormat(false); QByteArray bytes(7,0); - output->bus = bus; - output->setFrameId(baseAddress + 0x16); - output->payload()[0] = currentSendingPosition & 0xFF; - output->payload()[1] = (currentSendingPosition >> 8) & 0xFF; - output->payload()[2] = firmwareData[firmwareLocation++]; - output->payload()[3] = firmwareData[firmwareLocation++]; - output->payload()[4] = firmwareData[firmwareLocation++]; - output->payload()[5] = firmwareData[firmwareLocation++]; - for (int i = 0; i < 6; i++) xorByte = xorByte ^ static_cast(output->payload()[i]); - output->payload()[6] = xorByte; - output->setPayload(bytes); - CANConManager::getInstance()->sendFrame(*output); + output.bus = bus; + output.setFrameId(baseAddress + 0x16); + output.payload()[0] = currentSendingPosition & 0xFF; + output.payload()[1] = (currentSendingPosition >> 8) & 0xFF; + output.payload()[2] = firmwareData[firmwareLocation++]; + output.payload()[3] = firmwareData[firmwareLocation++]; + output.payload()[4] = firmwareData[firmwareLocation++]; + output.payload()[5] = firmwareData[firmwareLocation++]; + for (int i = 0; i < 6; i++) xorByte ^= static_cast(output.payload()[i]); + output.payload()[6] = xorByte; + output.setPayload(bytes); + CANConManager::getInstance()->sendFrame(output); timer->start(); } void FirmwareUploaderWindow::sendFirmwareEnding() { - CANFrame *output = new CANFrame; - output->setExtendedFrameFormat(false); - output->bus = bus; + CANFrame output; + output.setExtendedFrameFormat(false); + output.bus = bus; QByteArray bytes(4,0); - output->setFrameId(baseAddress + 0x30); - output->payload()[3] = 0xC0; - output->payload()[2] = 0xDE; - output->payload()[1] = 0xFA; - output->payload()[0] = 0xDE; - output->setPayload(bytes); + output.setFrameId(baseAddress + 0x30); + output.payload()[3] = 0xC0; + output.payload()[2] = 0xDE; + output.payload()[1] = 0xFA; + output.payload()[0] = 0xDE; + output.setPayload(bytes); //sendCANFrame(output, bus); } @@ -182,12 +182,12 @@ void FirmwareUploaderWindow::handleStartStopTransfer() qDebug() << "Base address: " + QString::number(baseAddress); CANConManager::getInstance()->addTargettedFrame(bus, baseAddress + 0x10, 0x7FF, this); CANConManager::getInstance()->addTargettedFrame(bus, baseAddress + 0x20, 0x7FF, this); - CANFrame *output = new CANFrame; - output->setExtendedFrameFormat(false); + CANFrame output; + output.setExtendedFrameFormat(false); QByteArray bytes(8,0); - output->bus = bus; - output->setFrameId(baseAddress); - output->setFrameType(QCanBusFrame::DataFrame); + output.bus = bus; + output.setFrameId(baseAddress); + output.setFrameType(QCanBusFrame::DataFrame); bytes[0] = 0xEF; bytes[1] = 0xBE; @@ -197,8 +197,8 @@ void FirmwareUploaderWindow::handleStartStopTransfer() bytes[5] = (token >> 8) & 0xFF; bytes[6] = (token >> 16) & 0xFF; bytes[7] = (token >> 24) & 0xFF; - output->setPayload(bytes); - CANConManager::getInstance()->sendFrame(*output); + output.setPayload(bytes); + CANConManager::getInstance()->sendFrame(output); } else //stop anything in process { @@ -209,7 +209,6 @@ void FirmwareUploaderWindow::handleStartStopTransfer() void FirmwareUploaderWindow::handleLoadFile() { - QString filename; QFileDialog dialog; QStringList filters; @@ -221,7 +220,7 @@ void FirmwareUploaderWindow::handleLoadFile() if (dialog.exec() == QDialog::Accepted) { - filename = dialog.selectedFiles()[0]; + QString filename = dialog.selectedFiles().constFirst(); loadBinaryFile(filename); } @@ -232,22 +231,20 @@ void FirmwareUploaderWindow::loadBinaryFile(QString filename) if (transferInProgress) handleStartStopTransfer(); - QFile *inFile = new QFile(filename); + QFile inFile(filename); - if (!inFile->open(QIODevice::ReadOnly)) + if (!inFile.open(QIODevice::ReadOnly)) { - delete inFile; return; } - firmwareData = inFile->readAll(); + firmwareData = inFile.readAll(); currentSendingPosition = 0; firmwareSize = firmwareData.length(); updateProgress(); - inFile->close(); - delete inFile; + inFile.close(); } From 6d814b4fb9c62e37ea6941733fe1ddaa33783aa2 Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 16:10:37 +0400 Subject: [PATCH 07/57] FirmwareUpdater: fix populating CAN frames frame.payload() returns a copy (!) of data, so assignig to it has no effect --- firmwareuploaderwindow.cpp | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/firmwareuploaderwindow.cpp b/firmwareuploaderwindow.cpp index 0a6be75..bc234e2 100644 --- a/firmwareuploaderwindow.cpp +++ b/firmwareuploaderwindow.cpp @@ -140,14 +140,14 @@ void FirmwareUploaderWindow::sendFirmwareChunk() QByteArray bytes(7,0); output.bus = bus; output.setFrameId(baseAddress + 0x16); - output.payload()[0] = currentSendingPosition & 0xFF; - output.payload()[1] = (currentSendingPosition >> 8) & 0xFF; - output.payload()[2] = firmwareData[firmwareLocation++]; - output.payload()[3] = firmwareData[firmwareLocation++]; - output.payload()[4] = firmwareData[firmwareLocation++]; - output.payload()[5] = firmwareData[firmwareLocation++]; - for (int i = 0; i < 6; i++) xorByte ^= static_cast(output.payload()[i]); - output.payload()[6] = xorByte; + bytes[0] = currentSendingPosition & 0xFF; + bytes[1] = (currentSendingPosition >> 8) & 0xFF; + bytes[2] = firmwareData[firmwareLocation++]; + bytes[3] = firmwareData[firmwareLocation++]; + bytes[4] = firmwareData[firmwareLocation++]; + bytes[5] = firmwareData[firmwareLocation++]; + for (int i = 0; i < 6; i++) xorByte ^= static_cast(bytes[i]); + bytes[6] = xorByte; output.setPayload(bytes); CANConManager::getInstance()->sendFrame(output); timer->start(); @@ -160,10 +160,10 @@ void FirmwareUploaderWindow::sendFirmwareEnding() output.bus = bus; QByteArray bytes(4,0); output.setFrameId(baseAddress + 0x30); - output.payload()[3] = 0xC0; - output.payload()[2] = 0xDE; - output.payload()[1] = 0xFA; - output.payload()[0] = 0xDE; + bytes[3] = (char)0xC0; + bytes[2] = (char)0xDE; + bytes[1] = (char)0xFA; + bytes[0] = (char)0xDE; output.setPayload(bytes); //sendCANFrame(output, bus); } @@ -189,10 +189,10 @@ void FirmwareUploaderWindow::handleStartStopTransfer() output.setFrameId(baseAddress); output.setFrameType(QCanBusFrame::DataFrame); - bytes[0] = 0xEF; - bytes[1] = 0xBE; - bytes[2] = 0xAD; - bytes[3] = 0xDE; + bytes[0] = (char)0xEF; + bytes[1] = (char)0xBE; + bytes[2] = (char)0xAD; + bytes[3] = (char)0xDE; bytes[4] = token & 0xFF; bytes[5] = (token >> 8) & 0xFF; bytes[6] = (token >> 16) & 0xFF; From 43ea48575fc6d2d0ba08c48fadfc365d9455cbbe Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 11:00:15 +0400 Subject: [PATCH 08/57] Minor: fix Frame Info window layout (same Widget names produced warning during compilation) --- ui/frameinfowindow.ui | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/frameinfowindow.ui b/ui/frameinfowindow.ui index 0b61394..375c63e 100644 --- a/ui/frameinfowindow.ui +++ b/ui/frameinfowindow.ui @@ -120,7 +120,7 @@ - + From 3dc426a5b812151da9f34681e3e49984ca639222 Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 11:29:47 +0400 Subject: [PATCH 09/57] Access to the CANBus parameters only through setters/getters Also removed explicit copy constructor, in such trivial cases compiler can create all needed copy/move stubs automatically --- connections/canbus.cpp | 27 +++++++++-------------- connections/canbus.h | 33 +++++++++++++++-------------- connections/serialbusconnection.cpp | 15 ++++++------- 3 files changed, 34 insertions(+), 41 deletions(-) diff --git a/connections/canbus.cpp b/connections/canbus.cpp index 3779e86..0623baa 100644 --- a/connections/canbus.cpp +++ b/connections/canbus.cpp @@ -13,21 +13,13 @@ CANBus::CANBus() } -CANBus::CANBus(const CANBus& pBus) : - speed(pBus.speed), - listenOnly(pBus.listenOnly), - singleWire(pBus.singleWire), - active(pBus.active), - canFD(pBus.canFD), - dataRate(pBus.dataRate){} - - bool CANBus::operator==(const CANBus& bus) const{ return speed == bus.speed && listenOnly == bus.listenOnly && singleWire == bus.singleWire && active == bus.active && - canFD == bus.canFD; + canFD == bus.canFD && + dataRate == bus.dataRate; } void CANBus::setSpeed(int newSpeed){ @@ -55,11 +47,11 @@ void CANBus::setCanFD(bool mode){ canFD = mode; } -int CANBus::getSpeed(){ +int CANBus::getSpeed() const { return speed; } -int CANBus::getDataRate(){ +int CANBus::getDataRate() const { return dataRate; } @@ -68,29 +60,30 @@ void CANBus::setDataRate(int newSpeed){ dataRate = newSpeed; } -bool CANBus::isListenOnly(){ +bool CANBus::isListenOnly() const { return listenOnly; } -bool CANBus::isSingleWire(){ +bool CANBus::isSingleWire() const { return singleWire; } -bool CANBus::isActive(){ +bool CANBus::isActive() const { return active; } -bool CANBus::isCanFD(){ +bool CANBus::isCanFD() const { return canFD; } -QDataStream& operator<<( QDataStream & pStream, const CANBus& pCanBus ) +QDataStream& operator<<(QDataStream & pStream, const CANBus& pCanBus) { pStream << pCanBus.speed; pStream << pCanBus.listenOnly; pStream << pCanBus.singleWire; pStream << pCanBus.active; + // FIXME CANFD settings missing return pStream; } diff --git a/connections/canbus.h b/connections/canbus.h index ff3d887..61fc122 100644 --- a/connections/canbus.h +++ b/connections/canbus.h @@ -5,13 +5,6 @@ class CANBus { -public: - CANBus(); - CANBus(const CANBus&); - bool operator==(const CANBus&) const; - CANBus& operator=(const CANBus& other) = default; - //virtual ~CANBus(){} - int speed; bool listenOnly; bool singleWire; @@ -19,28 +12,36 @@ public: bool canFD; int dataRate; + friend QDataStream& operator<<(QDataStream & pStream, const CANBus& pCanBus); + friend QDataStream& operator>>(QDataStream & pStream, CANBus& pCanBus); +public: + CANBus(); + + bool operator==(const CANBus&) const; + void setSpeed(int); // new speed void setListenOnly(bool); //bool for whether to only listen void setSingleWire(bool); //bool for whether to use single wire mode void setActive(bool); //whether this bus should be enabled or not. void setCanFD(bool); // enable or disable CANFD support - int getSpeed(); - int getDataRate(); void setDataRate(int newSpeed); - bool isListenOnly(); - bool isSingleWire(); - bool isActive(); - bool isCanFD(); + + int getSpeed() const; + int getDataRate() const; + bool isListenOnly() const; + bool isSingleWire() const; + bool isActive() const; + bool isCanFD() const; }; -QDataStream& operator<<( QDataStream & pStream, const CANBus& pCanBus ); -QDataStream & operator>>(QDataStream & pStream, CANBus& pCanBus); +QDataStream& operator<<(QDataStream & pStream, const CANBus& pCanBus); +QDataStream& operator>>(QDataStream & pStream, CANBus& pCanBus); Q_DECLARE_METATYPE(CANBus); struct BusData { CANBus mBus; - bool mConfigured; + bool mConfigured = {}; QVector mTargettedFrames; }; diff --git a/connections/serialbusconnection.cpp b/connections/serialbusconnection.cpp index a5ec860..8ebebbf 100644 --- a/connections/serialbusconnection.cpp +++ b/connections/serialbusconnection.cpp @@ -94,7 +94,7 @@ void SerialBusConnection::piSetBusSettings(int pBusIdx, CANBus bus) setBusConfig(0, bus); /* if bus is not active we are done */ - if(!bus.active) + if(!bus.isActive()) return; /* set configuration */ @@ -105,10 +105,10 @@ void SerialBusConnection::piSetBusSettings(int pBusIdx, CANBus bus) //You cannot set the speed of a socketcan interface, it has to be set with console commands. //But, you can probabaly set the speed of many of the other serialbus devices so go ahead and try - mDev_p->setConfigurationParameter(QCanBusDevice::BitRateKey, bus.speed); - mDev_p->setConfigurationParameter(QCanBusDevice::CanFdKey, bus.canFD); + mDev_p->setConfigurationParameter(QCanBusDevice::BitRateKey, bus.getSpeed()); + mDev_p->setConfigurationParameter(QCanBusDevice::CanFdKey, bus.isCanFD()); - if(bus.listenOnly) + if(bus.isListenOnly()) sbusconfig |= EN_SILENT_MODE; mDev_p->setConfigurationParameter(QCanBusDevice::UserKey, sbusconfig); @@ -188,6 +188,7 @@ void SerialBusConnection::framesReceived() /* check frame */ //if (recFrame.payload().length() <= 8) { + if (true) { CANFrame* frame_p = getQueue().get(); if(frame_p) { frame_p->setPayload(recFrame.payload()); @@ -206,7 +207,7 @@ void SerialBusConnection::framesReceived() frame_p->setTimeStamp(recFrame.timeStamp()); frame_p->setFrameType(recFrame.frameType()); frame_p->setError(recFrame.error()); - /* If recorded frame has a local echo, it is a Tx message, and thus should not be marked as Rx */ + /* If recorded frame has a local echo, it is a Tx message, and thus should not be marked as Rx */ frame_p->isReceived = !recFrame.hasLocalEcho(); if (useSystemTime) { @@ -218,11 +219,9 @@ void SerialBusConnection::framesReceived() /* enqueue frame */ getQueue().queue(); - //} -#if 0 + } else qDebug() << "can't get a frame, ERROR"; -#endif } } } From 5e027e5cef8a032b31e2150c60ff34f30eb4fd0a Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 11:52:22 +0400 Subject: [PATCH 10/57] GraphingWindow: don't use size_t as indexes for Qt containers --- re/graphingwindow.cpp | 46 ++++++++++++++++++++----------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/re/graphingwindow.cpp b/re/graphingwindow.cpp index f2a41e7..860aa7f 100644 --- a/re/graphingwindow.cpp +++ b/re/graphingwindow.cpp @@ -779,12 +779,12 @@ void GraphingWindow::saveSpreadsheet() QList::iterator iter; double xMin = std::numeric_limits::max(), xMax = std::numeric_limits::min(); - size_t maxCount = 0; - size_t numGraphs = graphParams.length(); + int maxCount = 0; + int numGraphs = graphParams.length(); for (auto && graph : graphParams) { xMin = std::min(xMin, graph.x[0]); xMax = std::max(xMax, graph.x[graph.x.count() - 1]); - maxCount = std::max(maxCount, static_cast(graph.x.count())); + maxCount = std::max(maxCount, graph.x.count()); } qDebug() << "xMin: " << xMin; qDebug() << "xMax: " << xMax; @@ -795,8 +795,6 @@ void GraphingWindow::saveSpreadsheet() double xSize = xMax - xMin; double sliceSize = xSize / ((double)maxCount); double equivValue = sliceSize / 100.0; - double currentX; - double value; QList indices; indices.reserve(numGraphs); @@ -808,14 +806,14 @@ void GraphingWindow::saveSpreadsheet() } outFile->write("\n"); - for (size_t j = 1; j < (maxCount - 1); j++) + for (int j = 1; j < (maxCount - 1); j++) { - currentX = xMin + (j * sliceSize); + double currentX = xMin + (j * sliceSize); qDebug() << "X: " << currentX; outFile->write(QString::number(currentX, 'f').toUtf8()); - for (size_t k = 0; k < numGraphs; k++) + for (int k = 0; k < numGraphs; k++) { - value = 0.0; + double value = 0.0; // move cursor to last sample before currentX while (graphParams[k].x[indices[k]+1] < currentX) @@ -849,7 +847,7 @@ void GraphingWindow::saveSpreadsheet() else { // find index, where x >= currentX - size_t cursor = indices[k]; + int cursor = indices[k]; double span = graphParams[k].x[cursor+1] - graphParams[k].x[cursor]; double progress = (currentX - graphParams[k].x[cursor]) / span; Q_ASSERT(progress >= 0.0 && progress <= 1.0); @@ -1325,10 +1323,8 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) double yminval=10000000.0, ymaxval = -1000000.0; double xminval=10000000000.0, xmaxval = -10000000000.0; GraphParams *refParam = ¶ms; - int sBit, bits; - bool intelFormat, isSigned; QString tempStr; - double x,y; + double x{}, y{}; qDebug() << "New Graph ID: " << params.ID; qDebug() << "Start bit: " << params.startBit; @@ -1365,10 +1361,10 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) //params.x.fill(0, numEntries); //params.y.fill(0, numEntries); - sBit = params.startBit; - bits = params.numBits; - intelFormat = params.intelFormat; - isSigned = params.isSigned; + int sBit = params.startBit; + int bits = params.numBits; + bool intelFormat = params.intelFormat; + bool isSigned = params.isSigned; for (int j = 0; j < numEntries; j++) { @@ -1558,16 +1554,16 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) void GraphingWindow::moveLegend() { qDebug() << "moveLegend"; - if (QAction* contextAction = qobject_cast(sender())) // make sure this slot is really called by a context menu action, so it carries the data we need - { - bool ok; - int dataInt = contextAction->data().toInt(&ok); - if (ok) + if (QAction* contextAction = qobject_cast(sender())) // make sure this slot is really called by a context menu action, so it carries the data we need { - ui->graphingView->axisRect()->insetLayout()->setInsetAlignment(0, (Qt::Alignment)dataInt); - ui->graphingView->replot(); + bool ok; + int dataInt = contextAction->data().toInt(&ok); + if (ok) + { + ui->graphingView->axisRect()->insetLayout()->setInsetAlignment(0, (Qt::Alignment)dataInt); + ui->graphingView->replot(); + } } - } } GraphParams::GraphParams() From 48f62ac41cb5bc354cb9326c2cf5399ad40b5455 Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 12:44:16 +0400 Subject: [PATCH 11/57] CANConnection constructor: use correct order of members in the initialization list --- connections/canconnection.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connections/canconnection.cpp b/connections/canconnection.cpp index 74e4fb8..9eac817 100644 --- a/connections/canconnection.cpp +++ b/connections/canconnection.cpp @@ -13,11 +13,11 @@ CANConnection::CANConnection(QString pPort, int pQueueLen, bool pUseThread) : mNumBuses(pNumBuses), + mSerialSpeed(pSerialSpeed), mQueue(), mPort(pPort), mDriver(pDriver), mType(pType), - mSerialSpeed(pSerialSpeed), mIsCapSuspended(false), mStatus(CANCon::NOT_CONNECTED), mStarted(false), From c3683b0f40d2675bf5c0f6c0dd6cf6c1acb5e85d Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 12:52:25 +0400 Subject: [PATCH 12/57] Minor: Utility::formatTimestamp() suppress compiler warning --- utility.h | 1 + 1 file changed, 1 insertion(+) diff --git a/utility.h b/utility.h index 01abc3d..88bbb31 100644 --- a/utility.h +++ b/utility.h @@ -190,6 +190,7 @@ public: return (double)timestamp / 1000000.0; break; } + return QVariant(); // should never happen } //parses the input string to grab as much of it as possible while staying alpha numeric From fa707ab2da11e84c5ce49e297f977db7ebb04d53 Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 14:35:02 +0400 Subject: [PATCH 13/57] Minor: fix memory leak on GraphingWindow file operations --- re/graphingwindow.cpp | 154 ++++++++++++++++++++---------------------- re/newgraphdialog.cpp | 1 - 2 files changed, 75 insertions(+), 80 deletions(-) diff --git a/re/graphingwindow.cpp b/re/graphingwindow.cpp index 860aa7f..d2f2581 100644 --- a/re/graphingwindow.cpp +++ b/re/graphingwindow.cpp @@ -39,7 +39,7 @@ GraphingWindow::GraphingWindow(const QVector *frames, QWidget *parent) if (Utility::timeStyle == TS_CLOCK) { - QSharedPointer timeTicker(new QCPAxisTickerTime); + QSharedPointer timeTicker = QSharedPointer::create(); timeTicker->setTimeFormat("%h:%m:%s.%z"); ui->graphingView->xAxis->setTicker(timeTicker); } @@ -571,7 +571,7 @@ void GraphingWindow::removeSelectedGraph() int idx = -1; for (int i = 0; i < graphParams.count(); i++) { - if (graphParams[i].ref == ui->graphingView->selectedGraphs().first()) + if (graphParams[i].ref == ui->graphingView->selectedGraphs().constFirst()) { idx = i; break; @@ -590,7 +590,7 @@ void GraphingWindow::removeSelectedGraph() graphParams.removeAt(idx); - ui->graphingView->removeGraph(ui->graphingView->selectedGraphs().first()); + ui->graphingView->removeGraph(ui->graphingView->selectedGraphs().constFirst()); if (graphParams.count() == 0) needScaleSetup = true; @@ -605,7 +605,7 @@ void GraphingWindow::editSelectedGraph() int idx = -1; for (int i = 0; i < graphParams.count(); i++) { - if (graphParams[i].ref == ui->graphingView->selectedGraphs().first()) + if (graphParams[i].ref == ui->graphingView->selectedGraphs().constFirst()) { idx = i; break; @@ -701,7 +701,6 @@ void GraphingWindow::contextMenuRequest(QPoint pos) void GraphingWindow::saveGraphs() { - QString filename; QFileDialog dialog(this); QSettings settings; @@ -718,7 +717,7 @@ void GraphingWindow::saveGraphs() if (dialog.exec() == QDialog::Accepted) { - filename = dialog.selectedFiles()[0]; + QString filename = dialog.selectedFiles().constFirst(); settings.setValue("Graphing/LoadSaveDirectory", dialog.directory().path()); if (dialog.selectedNameFilter() == filters[0]) @@ -741,7 +740,6 @@ void GraphingWindow::saveGraphs() void GraphingWindow::saveSpreadsheet() { - QString filename; QFileDialog dialog(this); QSettings settings; @@ -756,14 +754,14 @@ void GraphingWindow::saveSpreadsheet() if (dialog.exec() == QDialog::Accepted) { - filename = dialog.selectedFiles()[0]; + QString filename = dialog.selectedFiles().constFirst(); settings.setValue("Graphing/LoadSaveDirectory", dialog.directory().path()); if (!filename.contains('.')) filename += ".csv"; - QFile *outFile = new QFile(filename); + QFile outFile(filename); - if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) return; /* @@ -798,19 +796,19 @@ void GraphingWindow::saveSpreadsheet() QList indices; indices.reserve(numGraphs); - outFile->write("TimeStamp"); + outFile.write("TimeStamp"); for (auto && graph : graphParams) { indices.append(0); - outFile->putChar(','); - outFile->write(graph.graphName.toUtf8()); + outFile.putChar(','); + outFile.write(graph.graphName.toUtf8()); } - outFile->write("\n"); + outFile.write("\n"); for (int j = 1; j < (maxCount - 1); j++) { double currentX = xMin + (j * sliceSize); qDebug() << "X: " << currentX; - outFile->write(QString::number(currentX, 'f').toUtf8()); + outFile.write(QString::number(currentX, 'f').toUtf8()); for (int k = 0; k < numGraphs; k++) { double value = 0.0; @@ -854,20 +852,19 @@ void GraphingWindow::saveSpreadsheet() value = Utility::Lerp(graphParams[k].y[cursor], graphParams[k].y[cursor+1], progress); qDebug() << "Span: " << span << " Prog: " << progress << " Value: " << value; } - outFile->putChar(','); - outFile->write(QString::number(value).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(value).toUtf8()); } - outFile->write("\n"); + outFile.write("\n"); } - outFile->close(); + outFile.close(); } } void GraphingWindow::saveDefinitions() { - QString filename; QFileDialog dialog(this); QSettings settings; @@ -882,79 +879,78 @@ void GraphingWindow::saveDefinitions() if (dialog.exec() == QDialog::Accepted) { - filename = dialog.selectedFiles()[0]; + QString filename = dialog.selectedFiles().constFirst(); settings.setValue("Graphing/LoadSaveDirectory", dialog.directory().path()); if (!filename.contains('.')) filename += ".gdf"; - QFile *outFile = new QFile(filename); + QFile outFile(filename); - if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) + if (!outFile.open(QIODevice::WriteOnly | QIODevice::Text)) return; QList::iterator iter; for (iter = graphParams.begin(); iter != graphParams.end(); ++iter) { - outFile->write("Z,"); - outFile->write(QString::number(iter->ID, 16).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->mask, 16).toUtf8()); - outFile->putChar(','); - if (iter->intelFormat) outFile->write(QString::number(iter->startBit).toUtf8()); - else outFile->write(QString::number(iter->startBit * -1).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->numBits).toUtf8()); - outFile->putChar(','); - if (iter->isSigned) outFile->putChar('Y'); - else outFile->putChar('N'); - outFile->putChar(','); - outFile->write(QString::number(iter->bias).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->scale).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->stride).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->bus).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->lineColor.red()).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->lineColor.green()).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->lineColor.blue()).toUtf8()); - outFile->putChar(','); - outFile->write(iter->graphName.toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->fillColor.red()).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->fillColor.green()).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->fillColor.blue()).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->fillColor.alpha()).toUtf8()); - outFile->putChar(','); - if (iter->drawOnlyPoints) outFile->putChar('Y'); - else outFile->putChar('N'); - outFile->putChar(','); - outFile->write(QString::number(iter->pointType).toUtf8()); - outFile->putChar(','); - outFile->write(QString::number(iter->lineWidth).toUtf8()); + outFile.write("Z,"); + outFile.write(QString::number(iter->ID, 16).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->mask, 16).toUtf8()); + outFile.putChar(','); + if (iter->intelFormat) outFile.write(QString::number(iter->startBit).toUtf8()); + else outFile.write(QString::number(iter->startBit * -1).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->numBits).toUtf8()); + outFile.putChar(','); + if (iter->isSigned) outFile.putChar('Y'); + else outFile.putChar('N'); + outFile.putChar(','); + outFile.write(QString::number(iter->bias).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->scale).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->stride).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->bus).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->lineColor.red()).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->lineColor.green()).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->lineColor.blue()).toUtf8()); + outFile.putChar(','); + outFile.write(iter->graphName.toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->fillColor.red()).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->fillColor.green()).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->fillColor.blue()).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->fillColor.alpha()).toUtf8()); + outFile.putChar(','); + if (iter->drawOnlyPoints) outFile.putChar('Y'); + else outFile.putChar('N'); + outFile.putChar(','); + outFile.write(QString::number(iter->pointType).toUtf8()); + outFile.putChar(','); + outFile.write(QString::number(iter->lineWidth).toUtf8()); if (iter->associatedSignal) { - outFile->putChar(','); - outFile->write(iter->associatedSignal->parentMessage->name.toUtf8()); - outFile->putChar(','); - outFile->write(iter->associatedSignal->name.toUtf8()); + outFile.putChar(','); + outFile.write(iter->associatedSignal->parentMessage->name.toUtf8()); + outFile.putChar(','); + outFile.write(iter->associatedSignal->name.toUtf8()); } - outFile->write("\n"); + outFile.write("\n"); } - outFile->close(); + outFile.close(); } } void GraphingWindow::loadDefinitions() { - QString filename; QFileDialog dialog; QSettings settings; @@ -971,17 +967,17 @@ void GraphingWindow::loadDefinitions() if (dialog.exec() == QDialog::Accepted) { - filename = dialog.selectedFiles()[0]; + QString filename = dialog.selectedFiles().constFirst(); settings.setValue("Graphing/LoadSaveDirectory", dialog.directory().path()); - QFile *inFile = new QFile(filename); + QFile inFile(filename); QByteArray line; - if (!inFile->open(QIODevice::ReadOnly | QIODevice::Text)) + if (!inFile.open(QIODevice::ReadOnly | QIODevice::Text)) return; - while (!inFile->atEnd()) { - line = inFile->readLine().simplified(); + while (!inFile.atEnd()) { + line = inFile.readLine().simplified(); if (line.length() > 2) { GraphParams gp; @@ -1203,7 +1199,7 @@ void GraphingWindow::loadDefinitions() } } } - inFile->close(); + inFile.close(); } } diff --git a/re/newgraphdialog.cpp b/re/newgraphdialog.cpp index 6b759f4..62f73c6 100644 --- a/re/newgraphdialog.cpp +++ b/re/newgraphdialog.cpp @@ -361,7 +361,6 @@ void NewGraphDialog::loadNodes() void NewGraphDialog::loadMessages(int idx) { int numFiles = 0; - DBC_MESSAGE *msg; ui->cbMessages->clear(); if (dbcHandler == nullptr) return; if ((numFiles = dbcHandler->getFileCount()) == 0) return; From 0885149492a88677a2ccb3cae177d126bdbd1585 Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 15:02:44 +0400 Subject: [PATCH 14/57] FrameSender: fix memory corruption while writing to the QCanBusFrame with empty payload --- framesenderwindow.cpp | 2 -- re/flowviewwindow.cpp | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/framesenderwindow.cpp b/framesenderwindow.cpp index 73162e5..a6e867c 100644 --- a/framesenderwindow.cpp +++ b/framesenderwindow.cpp @@ -1054,8 +1054,6 @@ void FrameSenderWindow::processCellChange(int line, int col) } break; case ST_COLS::SENDTAB_COL_DATA: //Data bytes - for (int i = 0; i < 8; i++) sendingData[line].payload().data()[i] = 0; - #if QT_VERSION >= QT_VERSION_CHECK( 5, 14, 0 ) tokens = ui->tableSender->item(line, ST_COLS::SENDTAB_COL_DATA)->text().split(" ", Qt::SkipEmptyParts); #else diff --git a/re/flowviewwindow.cpp b/re/flowviewwindow.cpp index c51960b..77c088f 100644 --- a/re/flowviewwindow.cpp +++ b/re/flowviewwindow.cpp @@ -851,7 +851,7 @@ void FlowViewWindow::updatePosition(bool forward) int maxVal = qMin(chunk * 8 + 8, frameCache.at(currentPosition).payload().length()); for (int i = chunk * 8; i < maxVal; i++) { - unsigned char thisByte = static_cast(frameCache.at(currentPosition).payload().data()[i]); + unsigned char thisByte = static_cast(frameCache.at(currentPosition).payload()[i]); cngByte = currBytes[i] ^ thisByte; changedBits |= (uint64_t)cngByte << (8ull * (i & 7)); } From 2082845d0310672e2977e8c92d629ed1d781e1d4 Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 15:13:48 +0400 Subject: [PATCH 15/57] SocketCANd: fix memory leak: string can be accessed after object destruction --- connections/socketcand.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/connections/socketcand.cpp b/connections/socketcand.cpp index 83fb144..6ef8975 100644 --- a/connections/socketcand.cpp +++ b/connections/socketcand.cpp @@ -219,8 +219,8 @@ void SocketCANd::connectDevice() void SocketCANd::deviceConnected(int busNum) { sendDebug("Opening CAN on Kayak Device!"); - const char* openCanCmd = ("< open " + hostCanIDs[busNum] + " >").toStdString().c_str(); - sendStringToTCP(openCanCmd, busNum); + QString openCanCmd("< open " % hostCanIDs[busNum] % " >"); + sendStringToTCP(openCanCmd.toUtf8().data(), busNum); QCoreApplication::processEvents(); } @@ -383,8 +383,6 @@ void SocketCANd::procRXData(QString data, int busNum) mTimer.stop(); mTimer.start(); } - QByteArray output; - switch (rx_state.at(busNum)) { case IDLE: From a86a78e19afbbb82b65be7576c154e827f92b9ff Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 15:25:42 +0400 Subject: [PATCH 16/57] Minor: DBCComparatorWindow: sanity checks to fix gcc warnings --- re/dbccomparatorwindow.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/re/dbccomparatorwindow.cpp b/re/dbccomparatorwindow.cpp index e9b87b2..f8eb0bd 100644 --- a/re/dbccomparatorwindow.cpp +++ b/re/dbccomparatorwindow.cpp @@ -222,8 +222,8 @@ void DBCComparatorWindow::calculateDetails() ui->treeDetails->addTopLevelItem(msgDiffRoot); - QTreeWidgetItem *msgItem; - QTreeWidgetItem *sigTemp; + QTreeWidgetItem *msgItem {}; + QTreeWidgetItem *sigTemp {}; for (int i = 0; i < firstDBC->messageHandler->getCount(); i++) { @@ -257,7 +257,8 @@ void DBCComparatorWindow::calculateDetails() msgItem->setText(0, msgName + " (" + Utility::formatCANID(thisMsg->ID) + ")"); sigDiffTwo->addChild(msgItem); } - msgItem->addChild(missingSigItem); + if (msgItem) + msgItem->addChild(missingSigItem); } else //signal exists on both sides. See if it as changed position or length { @@ -301,7 +302,8 @@ void DBCComparatorWindow::calculateDetails() sigTemp->setText(0, msgName + " (" + Utility::formatCANID(thisMsg->ID) + ")"); sigModifiedRoot->addChild(sigTemp); } - sigTemp->addChild(sigItem); + if (sigTemp) + sigTemp->addChild(sigItem); } } } From f2a692c30947718357bbb608e62ef5f1f921d2be Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 18:42:06 +0400 Subject: [PATCH 17/57] Minor: SnifferWindow: fix deprecated constant --- re/sniffer/snifferwindow.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/re/sniffer/snifferwindow.cpp b/re/sniffer/snifferwindow.cpp index e0ecfb8..f8145b1 100644 --- a/re/sniffer/snifferwindow.cpp +++ b/re/sniffer/snifferwindow.cpp @@ -181,7 +181,7 @@ void SnifferWindow::notchTick() } else { - ui->lblNotch->setBackgroundRole(QPalette::Background); + ui->lblNotch->setBackgroundRole(QPalette::Window); ui->lblNotch->repaint(); //qDebug() << "Tock"; } From 8aa3659999c20bd84ab49617b89ce4b2a1960b8b Mon Sep 17 00:00:00 2001 From: Alexey Esaulenko Date: Thu, 18 Jan 2024 18:06:45 +0400 Subject: [PATCH 18/57] ConnectionWindow: declare debug output connection as unique to prevent duplicates --- connections/connectionwindow.cpp | 27 +++++++++++++-------------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/connections/connectionwindow.cpp b/connections/connectionwindow.cpp index 639d531..5e4df7a 100644 --- a/connections/connectionwindow.cpp +++ b/connections/connectionwindow.cpp @@ -79,12 +79,12 @@ ConnectionWindow::ConnectionWindow(QWidget *parent) : //Need to make sure it tries to share the address in case there are //multiple instances of SavvyCAN running. rxBroadcastGVRET->bind(QHostAddress::AnyIPv4, 17222, QAbstractSocket::ShareAddress); - connect(rxBroadcastGVRET, SIGNAL(readyRead()), this, SLOT(readPendingDatagrams())); + connect(rxBroadcastGVRET, &QUdpSocket::readyRead, this, &ConnectionWindow::readPendingDatagrams); //Doing the same for socketcand/kayak hosts: rxBroadcastKayak = new QUdpSocket(this); rxBroadcastKayak->bind(QHostAddress::AnyIPv4, 42000, QAbstractSocket::ShareAddress); - connect(rxBroadcastKayak, SIGNAL(readyRead()), this, SLOT(readPendingDatagrams())); + connect(rxBroadcastKayak, &QUdpSocket::readyRead, this, &ConnectionWindow::readPendingDatagrams); } @@ -236,12 +236,12 @@ void ConnectionWindow::consoleEnableChanged(bool checked) { CANConnection* conn_p = connModel->getAtIdx(selIdx); if (checked) { //enable console - connect(conn_p, SIGNAL(debugOutput(QString)), this, SLOT(getDebugText(QString))); - connect(this, SIGNAL(sendDebugData(QByteArray)), conn_p, SLOT(debugInput(QByteArray))); + connect(conn_p, &CANConnection::debugOutput, this, &ConnectionWindow::getDebugText, Qt::UniqueConnection); + connect(this, &ConnectionWindow::sendDebugData, conn_p, &CANConnection::debugInput, Qt::UniqueConnection); } else { //turn it off - disconnect(conn_p, SIGNAL(debugOutput(QString)), nullptr, nullptr); - disconnect(this, SIGNAL(sendDebugData(QByteArray)), conn_p, SLOT(debugInput(QByteArray))); + disconnect(conn_p, &CANConnection::debugOutput, nullptr, nullptr); + disconnect(this, &ConnectionWindow::sendDebugData, conn_p, &CANConnection::debugInput); } } @@ -454,8 +454,8 @@ void ConnectionWindow::currentRowChanged(const QModelIndex ¤t, const QMode int selIdx = current.row(); CANConnection* prevConn = connModel->getAtIdx(previous.row()); if(prevConn != nullptr) - disconnect(prevConn, SIGNAL(debugOutput(QString)), nullptr, nullptr); - disconnect(this, SIGNAL(sendDebugData(QByteArray)), nullptr, nullptr); + disconnect(prevConn, &CANConnection::debugOutput, nullptr, nullptr); + disconnect(this, &ConnectionWindow::sendDebugData, nullptr, nullptr); /* set parameters */ if (selIdx == -1) { @@ -472,7 +472,7 @@ void ConnectionWindow::currentRowChanged(const QModelIndex ¤t, const QMode if(!conn_p) return; //because this might have already been setup during the initial setup so tear that one down and then create the normal one. - //disconnect(conn_p, SIGNAL(debugOutput(QString)), 0, 0); + //disconnect(conn_p, &CANConnection::debugOutput, 0, 0); numBuses = conn_p->getNumBuses(); int numB = ui->tabBuses->count(); @@ -485,8 +485,8 @@ void ConnectionWindow::currentRowChanged(const QModelIndex ¤t, const QMode populateBusDetails(0); if (ui->ckEnableConsole->isChecked()) { - connect(conn_p, SIGNAL(debugOutput(QString)), this, SLOT(getDebugText(QString))); - connect(this, SIGNAL(sendDebugData(QByteArray)), conn_p, SLOT(debugInput(QByteArray))); + connect(conn_p, &CANConnection::debugOutput, this, &ConnectionWindow::getDebugText, Qt::UniqueConnection); + connect(this, &ConnectionWindow::sendDebugData, conn_p, &CANConnection::debugInput, Qt::UniqueConnection); } } } @@ -524,12 +524,11 @@ CANConnection* ConnectionWindow::create(CANCon::type pTye, QString pPortName, QS if(conn_p) { /* connect signal */ - connect(conn_p, SIGNAL(status(CANConStatus)), - this, SLOT(connectionStatus(CANConStatus))); + connect(conn_p, &CANConnection::status, this, &ConnectionWindow::connectionStatus); if (ui->ckEnableConsole->isChecked()) { //set up the debug console to operate if we've selected it. Doing so here allows debugging right away during set up - connect(conn_p, SIGNAL(debugOutput(QString)), this, SLOT(getDebugText(QString))); + connect(conn_p, &CANConnection::debugOutput, this, &ConnectionWindow::getDebugText, Qt::UniqueConnection); } /*TODO add return value and checks */ conn_p->start(); From b5619e0de841be762685079e2e97286fb0a7a593 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 6 Feb 2024 20:22:42 -0500 Subject: [PATCH 19/57] Update CANalyzer ASC loader to accept CANFD messages as well --- framefileio.cpp | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/framefileio.cpp b/framefileio.cpp index 5ea46ca..f7e8658 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -1680,12 +1680,16 @@ bool FrameFileIO::isCanalyzerASC(QString filename) //0.001371 CANFD 1 Rx 171 A0066_Unknown 0 0 8 8 00 00 07 76 23 00 00 00 230199 119 220000 62e7 46500250 460a0250 20011736 20010205 //Version 16.0.0 //0.008600 1 358 Rx d 8 04 03 50 01 F0 40 54 2C Length = 233910 BitCount = 121 ID = 856 -//Time bus id dir ? len databytes (Ver 8.0) -//Time type bus dir ID ? ? length length bytes then many values of unknown type (ver 8.1) -//Time type bus dir ID SignalName ? ? length length bytes then many values of unknown type (ver 9.0) -//Time bus id dir ? len databytes additional crap (ver 16.0) -//So it seems the file format is not entirely different based on version but rather on some other -//settings... Fun! +//Version 17.3.0 +//0.001684 CANFD 1 Rx 65b 1 0 d 32 f1 01 00 00 50 00 00 00 00 00 00 00 00 00 00 40 00 88 f5 77 17 78 00 00 c0 03 00 00 00 00 00 00 213797 356 303000 11729 46500250 4b140250 20011736 2001040d + +//Time bus id dir ? len databytes (Ver 8.0) +//Time bus id dir ? len databytes (addl info) (ver 16.0) +//Time type bus dir ID ? ? length length (bytes) (many values of unknown type) (ver 8.1) +//Time type bus dir ID SignalName ? ? length length bytes (then many values of unknown type)(ver 9.0) +//Time Type Bus Dir ID ? ? (length) (Real Length) (bytes) (many values of unknown type) (Ver 17.3) +//0 1 2 3 4 5 6 7 8 9 10 +//This seems like a rather eclectic mix. It's almost arbitrary! bool FrameFileIO::loadCanalyzerASC(QString filename, QVector* frames) { QFile *inFile = new QFile(filename); @@ -1736,7 +1740,7 @@ bool FrameFileIO::loadCanalyzerASC(QString filename, QVector* frames) } } if (inHeader) continue; - if (line.length() > 2) + if (line.length() > 2 && !line.startsWith("//")) { tokens = line.simplified().split(' '); @@ -1766,7 +1770,7 @@ bool FrameFileIO::loadCanalyzerASC(QString filename, QVector* frames) int payloadLen = tokens[8].toInt(); qDebug() << "PayloadLen: " << payloadLen << " Tokens: " << tokens; QByteArray bytes(payloadLen, 0); - if (payloadLen > 8) + if (payloadLen > 64) { qDebug() << "Payload length too long. Original line: " << line; return false; From 0c84b6998f79f3e27b7ce5eed1951f3e795dbe0b Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 12 Feb 2024 21:20:54 -0500 Subject: [PATCH 20/57] Partial implementation of a more generic and multi-threaded version of the frame sender. Now a simplified interface is attached to the main window as well. Doesn't necessarily work just yet... --- SavvyCAN.pro | 2 + can_trigger_structs.h | 6 +- framesenderobject.cpp | 453 +++++++++++++++++++++++++++++ framesenderobject.h | 81 ++++++ framesenderwindow.cpp | 41 ++- mainwindow.cpp | 198 +++++++++++++ mainwindow.h | 20 +- ui/framesenderwindow.ui | 2 +- ui/mainwindow.ui | 625 ++++++++++++++++++++-------------------- 9 files changed, 1104 insertions(+), 324 deletions(-) create mode 100644 framesenderobject.cpp create mode 100644 framesenderobject.h diff --git a/SavvyCAN.pro b/SavvyCAN.pro index def354a..5512e42 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -26,6 +26,7 @@ SOURCES += main.cpp\ connections/lawicel_serial.cpp \ connections/mqtt_bus.cpp \ dbc/dbcnodeduplicateeditor.cpp \ + framesenderobject.cpp \ mqtt/qmqtt_client.cpp \ mqtt/qmqtt_client_p.cpp \ mqtt/qmqtt_frame.cpp \ @@ -113,6 +114,7 @@ HEADERS += mainwindow.h \ connections/mqtt_bus.h \ dbc/dbcnodeduplicateeditor.h \ dbc/dbcnoderebaseeditor.h \ + framesenderobject.h \ mqtt/qmqtt.h \ mqtt/qmqtt_client.h \ mqtt/qmqtt_client_p.h \ diff --git a/can_trigger_structs.h b/can_trigger_structs.h index 32ef0ef..177df4f 100644 --- a/can_trigger_structs.h +++ b/can_trigger_structs.h @@ -4,6 +4,7 @@ #include "can_structs.h" #include +#include enum TriggerMask { @@ -38,6 +39,7 @@ public: //If ID is -1 then this is the temporary storage register. This is a shadow //register used to accumulate the results of a multi operation modifier. //if ID is -2 then this is a look up of our own data bytes stored in the class data. +//Of course, if the ID is positive then we grab bytes or signals from newest message with that ID class ModifierOperand { public: @@ -45,6 +47,7 @@ public: int bus; int databyte; bool notOper; //should a bitwise NOT be applied to this prior to doing the actual calculation? + QString signalName; //if ID is positive and there is text in here then we'll look up the signal and use its value }; //list of operations that can be done between the two operands @@ -73,7 +76,8 @@ public: class Modifier { public: - int destByte; + int destByte; //if -1 then target one of this ID's signals instead + QString signalName; QList operations; }; diff --git a/framesenderobject.cpp b/framesenderobject.cpp new file mode 100644 index 0000000..9212747 --- /dev/null +++ b/framesenderobject.cpp @@ -0,0 +1,453 @@ +#include "framesenderobject.h" +#include "mainwindow.h" + +FrameSenderObject::FrameSenderObject(const QVector *frames) +{ + mThread_p = new QThread(); + + statusCounter = 0; + modelFrames = frames; + dbcHandler = DBCHandler::getReference(); +} + +FrameSenderObject::~FrameSenderObject() +{ + mThread_p->quit(); + mThread_p->wait(); + delete mThread_p; +} + +void FrameSenderObject::piStart() +{ + sendingTimer = new QTimer(); + sendingTimer->setTimerType(Qt::PreciseTimer); + sendingTimer->setInterval(1); + + connect(sendingTimer, &QTimer::timeout, this, &FrameSenderObject::timerTriggered); +} + +void FrameSenderObject::piStop() +{ + sendingTimer->stop(); + delete sendingTimer; +} + +void FrameSenderObject::initialize() +{ + if( mThread_p && (mThread_p != QThread::currentThread()) ) + { + /* move ourself to the thread */ + moveToThread(mThread_p); /*TODO handle errors */ + /* connect started() */ + connect(mThread_p, SIGNAL(started()), this, SLOT(initialize())); + /* start the thread */ + mThread_p->start(QThread::HighPriority); + return; + } + + /* set started flag */ + //mStarted = true; + + connect(MainWindow::getReference(), SIGNAL(framesUpdated(int)), this, SLOT(updatedFrames(int))); + + /* in multithread case, this will be called before entering thread event loop */ + return piStart(); + +} + +void FrameSenderObject::finalize() +{ + /* 1) execute in mThread_p context */ + if( mThread_p && (mThread_p != QThread::currentThread()) ) + { + /* if thread is finished, it means we call this function for the second time so we can leave */ + if( !mThread_p->isFinished() ) + { + /* we need to call piStop() */ + QMetaObject::invokeMethod(this, "finalize", + Qt::BlockingQueuedConnection); + /* 3) stop thread */ + mThread_p->quit(); + if(!mThread_p->wait()) { + qDebug() << "can't stop thread"; + } + } + return; + } + + /* 2) call piStop in mThread context */ + return piStop(); +} + +void FrameSenderObject::startSending() +{ + /* make sure we execute in mThread context */ + if( mThread_p && (mThread_p != QThread::currentThread()) ) { + QMetaObject::invokeMethod(this, "startSending", + Qt::BlockingQueuedConnection); + return; + } + + sendingElapsed.start(); + sendingTimer->start(); +} + +void FrameSenderObject::stopSending() +{ + /* make sure we execute in mThread context */ + if( mThread_p && (mThread_p != QThread::currentThread()) ) { + QMetaObject::invokeMethod(this, "stopPlayback", + Qt::BlockingQueuedConnection); + return; + } + + sendingTimer->stop(); //pushing this button halts automatic playback + //emit statusUpdate(currentPosition); +} + +void FrameSenderObject::addSendRecord(FrameSendData record) +{ + /* make sure we execute in mThread context */ + if( mThread_p && (mThread_p != QThread::currentThread()) ) { + QMetaObject::invokeMethod(this, "addSendRecord", + Qt::BlockingQueuedConnection, + Q_ARG(FrameSendData, record)); + return; + } + sendingData.append(record); +} + +void FrameSenderObject::removeSendRecord(int idx) +{ + /* make sure we execute in mThread context */ + if( mThread_p && (mThread_p != QThread::currentThread()) ) { + QMetaObject::invokeMethod(this, "removeSendRecord", + Qt::BlockingQueuedConnection, + Q_ARG(int, idx)); + return; + } + sendingData.removeAt(idx); +} + +/* + * Note: Getting a reference to an item in a QList is a dangerous thing. If anyone + * or anything happens to reallocate the list you're screwed. So, call this, do the + * work, and drop the reference as soon as possible. +*/ +FrameSendData *FrameSenderObject::getSendRecordRef(int idx) +{ + if (idx < 0 || idx >= sendingData.count()) return nullptr; + return &sendingData[idx]; +} + +/// +/// Called every millisecond to set the system update figures and send frames if necessary. +/// +/// +/// +/// +void FrameSenderObject::timerTriggered() +{ + FrameSendData *sendData; + Trigger *trigger; + + sendingList.clear(); + if(mutex.tryLock()) + { + //get elapsed microseconds since last tick (in case timer skips or is otherwise inaccurate, though there are no guarantees about elapsed timer either) + quint64 elapsed = sendingElapsed.nsecsElapsed() / 1000; + if (elapsed == 0) elapsed = 1; + sendingElapsed.start(); + sendingLastTimeStamp += elapsed; + //qDebug() << playbackLastTimeStamp; + //qDebug() << "El: " << elapsed; + statusCounter++; + for (int i = 0; i < sendingData.count(); i++) + { + sendData = &sendingData[i]; + if (!sendData->enabled) + { + if (sendData->triggers.count() > 0) + { + for (int j = 0; j < sendData->triggers.count(); j++) //resetting currCount when line is disabled + { + sendData->triggers[j].currCount = 0; + } + } + continue; //abort any processing on this if it is not enabled. + } + if (sendData->triggers.count() == 0) + { + qDebug() << "No triggers to process"; + break; + } + for (int j = 0; j < sendData->triggers.count(); j++) + { + trigger = &sendData->triggers[j]; + if (trigger->currCount >= trigger->maxCount) continue; //don't process if we've sent max frames we were supposed to + if (!trigger->readyCount) continue; //don't tick if not ready to tick + //is it time to fire? + trigger->msCounter += elapsed; //gives proper tracking even if timer doesn't fire as fast as it should + if (trigger->msCounter >= trigger->milliseconds) + { + trigger->msCounter -= trigger->milliseconds; + sendData->count++; + trigger->currCount++; + doModifiers(i); + //updateGridRow(i); + //qDebug() << "About to try to send a frame"; + //CANConManager::getInstance()->sendFrame(sendingData[i]); + sendingList.append(sendingData[i]); //queue it instead of immediate sending + if (trigger->ID > 0) trigger->readyCount = false; //reset flag if this is a timed ID trigger + } + } + } + + //if we have any frames to send after the above then send as a batch + if (sendingList.count() > 0) CANConManager::getInstance()->sendFrames(sendingList); + mutex.unlock(); + } + else + { + qDebug() << "framesenderobject::handleTick() couldn't get mutex, elapsed is: " << sendingElapsed.elapsed(); + } +} + +void FrameSenderObject::buildFrameCache() +{ + CANFrame thisFrame; + frameCache.clear(); + for (int i = 0; i < modelFrames->count(); i++) + { + thisFrame = modelFrames->at(i); + if (!frameCache.contains(thisFrame.frameId())) + { + frameCache.insert(thisFrame.frameId(), thisFrame); + } + else + { + frameCache[thisFrame.frameId()] = thisFrame; + } + } +} + +//remember, negative numbers are special -1 = all frames deleted, -2 = totally new set of frames. +void FrameSenderObject::updatedFrames(int numFrames) +{ + CANFrame thisFrame; + if (numFrames == -1) //all frames deleted. + { + } + else if (numFrames == -2) //all new set of frames. + { + buildFrameCache(); + } + else //just got some new frames. See if they are relevant. + { + if (numFrames > modelFrames->count()) return; + qDebug() << "New frames in sender window"; + //run through the supposedly new frames in order + for (int i = modelFrames->count() - numFrames; i < modelFrames->count(); i++) + { + thisFrame = modelFrames->at(i); + if (!frameCache.contains(thisFrame.frameId())) + { + frameCache.insert(thisFrame.frameId(), thisFrame); + } + else + { + frameCache[thisFrame.frameId()] = thisFrame; + } + processIncomingFrame(&thisFrame); + } + } +} + +void FrameSenderObject::processIncomingFrame(CANFrame *frame) +{ + for (int sd = 0; sd < sendingData.count(); sd++) + { + if (sendingData[sd].triggers.count() == 0) continue; + bool passedChecks = true; + for (int trig = 0; trig < sendingData[sd].triggers.count(); trig++) + { + Trigger *thisTrigger = &sendingData[sd].triggers[trig]; + //need to ensure that this trigger is actually related to frames incoming. + //Otherwise ignore it here. Only frames that have BUS and/or ID set as trigger will work here. + if (!(thisTrigger->triggerMask & (TriggerMask::TRG_BUS | TriggerMask::TRG_ID) ) ) continue; + + passedChecks = true; + //qDebug() << "Trigger ID: " << thisTrigger->ID; + //qDebug() << "Frame ID: " << frame->frameId(); + + //Check to see if we have a bus trigger condition and if so does it match + if (thisTrigger->bus != frame->bus && (thisTrigger->triggerMask & TriggerMask::TRG_BUS) ) + passedChecks = false; + + //check to see if we have an ID trigger condition and if so does it match + if ((thisTrigger->triggerMask & TriggerMask::TRG_ID) && (uint32_t)thisTrigger->ID != frame->frameId() ) + passedChecks = false; + + //check to see if we're limiting the trigger by max count and have we reached that count? + if ( (thisTrigger->triggerMask & TriggerMask::TRG_COUNT) && (thisTrigger->currCount >= thisTrigger->maxCount) ) + passedChecks = false; + + //if the above passed then are we triggering not only on ID but also signal? + if (passedChecks && (thisTrigger->triggerMask & (TriggerMask::TRG_SIGNAL | TriggerMask::TRG_ID) ) ) + { + bool sigCheckPassed = false; + DBC_MESSAGE *msg = dbcHandler->findMessage(thisTrigger->ID); + if (msg) + { + DBC_SIGNAL *sig = msg->sigHandler->findSignalByName(thisTrigger->sigName); + if (sig) + { + //first of all, is this signal really in this message we got? + if (sig->isSignalInMessage(*frame)) sigCheckPassed = true; + //if it was and we're also filtering on value then try that next + if (sigCheckPassed && (thisTrigger->triggerMask & TriggerMask::TRG_SIGVAL)) + { + double sigval = 0.0; + if (sig->processAsDouble(*frame, sigval)) + { + if (abs(sigval - thisTrigger->sigValueDbl) > 0.001) + { + sigCheckPassed = false; + } + } + else sigCheckPassed = false; + } + } + else sigCheckPassed = false; + } + else sigCheckPassed = false; + passedChecks &= sigCheckPassed; //passedChecks can only be true if both are + } + + //if all the above says it's OK then we'll go ahead and send that message. + //If a message that comes through here has a MS value then we use it as a delay + //after the check passes. This allows for delaying the sending of the frame if that + //is required. Otherwise, just send it immediately. + if (passedChecks) + { + if (thisTrigger->milliseconds == 0) //immediate reply + { + thisTrigger->currCount++; + sendingData[sd].count++; + doModifiers(sd); + //updateGridRow(sd); + CANConManager::getInstance()->sendFrame(sendingData[sd]); + } + else //delayed sending frame + { + thisTrigger->readyCount = true; + } + } + } + } +} + + +/// +/// given an index into the sendingData list we run the modifiers that it has set up +/// +/// The index into the sendingData list +void FrameSenderObject::doModifiers(int idx) +{ + int shadowReg = 0; //shadow register we use to accumulate results + int first=0, second=0; + + FrameSendData *sendData = &sendingData[idx]; + Modifier *mod; + ModifierOp op; + + if (sendData->modifiers.count() == 0) return; //if no modifiers just leave right now + + //qDebug() << "Executing mods"; + + for (int i = 0; i < sendData->modifiers.count(); i++) + { + mod = &sendData->modifiers[i]; + for (int j = 0; j < mod->operations.count(); j++) + { + op = mod->operations.at(j); + if (op.first.ID == -1) + { + first = shadowReg; + } + else first = fetchOperand(idx, op.first); + second = fetchOperand(idx, op.second); + switch (op.operation) + { + case ADDITION: + shadowReg = first + second; + break; + case AND: + shadowReg = first & second; + break; + case DIVISION: + shadowReg = first / second; + break; + case MULTIPLICATION: + shadowReg = first * second; + break; + case OR: + shadowReg = first | second; + break; + case SUBTRACTION: + shadowReg = first - second; + break; + case XOR: + shadowReg = first ^ second; + break; + case MOD: + shadowReg = first % second; + } + } + //Finally, drop the result into the proper data byte + QByteArray newArr(sendData->payload()); + newArr[mod->destByte] = (char) shadowReg; + sendData->setPayload(newArr); + } +} + +int FrameSenderObject::fetchOperand(int idx, ModifierOperand op) +{ + CANFrame *tempFrame = nullptr; + if (op.ID == 0) //numeric constant + { + if (op.notOper) return ~op.databyte; + else return op.databyte; + } + else if (op.ID == -2) //fetch data from a data byte within the output frame + { + if (op.notOper) return ~((unsigned char)sendingData.at(idx).payload()[op.databyte]); + else return (unsigned char)sendingData.at(idx).payload()[op.databyte]; + } + else //look up external data byte + { + tempFrame = lookupFrame(op.ID, op.bus); + if (tempFrame != nullptr) + { + if (op.notOper) return ~((unsigned char)tempFrame->payload()[op.databyte]); + else return (unsigned char)tempFrame->payload()[op.databyte]; + } + else return 0; + } +} + +/// +/// Try to find the most recent frame given the input criteria +/// +/// The ID to find +/// Which bus to look on (-1 if you don't care) +/// +CANFrame* FrameSenderObject::lookupFrame(int ID, int bus) +{ + if (!frameCache.contains(ID)) return nullptr; + + if (bus == -1 || frameCache[ID].bus == bus) return &frameCache[ID]; + + return nullptr; +} + diff --git a/framesenderobject.h b/framesenderobject.h new file mode 100644 index 0000000..47a5800 --- /dev/null +++ b/framesenderobject.h @@ -0,0 +1,81 @@ +#ifndef FRAMESENDEROBJECT_H +#define FRAMESENDEROBJECT_H + +#include +#include +#include +#include +#include +#include +#include "can_structs.h" +#include "connections/canconmanager.h" +#include "can_trigger_structs.h" +#include "dbc/dbchandler.h" + +class FrameSenderObject : public QObject +{ + Q_OBJECT + +public: + FrameSenderObject(const QVector *frames); + ~FrameSenderObject(); + +public slots: + /** + * @brief start the device. This calls piStarted + * @note starts the working thread if required (piStart is called in the working thread context) + */ + void initialize(); + + /** + * @brief stop the device. This calls piStop + * @note if a working thread is used, piStop is called before exiting the working thread + */ + void finalize(); + + void startSending(); + void stopSending(); + + void addSendRecord(FrameSendData record); + void removeSendRecord(int idx); + FrameSendData *getSendRecordRef(int idx); + +signals: + +private slots: + void timerTriggered(); + void updatedFrames(int); + +private: + QList sendingList; + int currentPosition; + QTimer *sendingTimer; + QElapsedTimer sendingElapsed; + quint64 sendingLastTimeStamp; + int statusCounter; + QList sendingData; + QThread* mThread_p; + QHash frameCache; //hash with frame ID as the key and the most recent frame as the value + const QVector *modelFrames; + bool inhibitChanged = false; + QMutex mutex; + DBCHandler *dbcHandler; + + void doModifiers(int); + int fetchOperand(int, ModifierOperand); + CANFrame* lookupFrame(int, int); + void buildFrameCache(); + void processIncomingFrame(CANFrame *frame); + + /** + * @brief starts the device + */ + void piStart(); + + /** + * @brief stops the device + */ + void piStop(); +}; + +#endif // FRAMESENDEROBJECT_H diff --git a/framesenderwindow.cpp b/framesenderwindow.cpp index a6e867c..cd4b32f 100644 --- a/framesenderwindow.cpp +++ b/framesenderwindow.cpp @@ -63,8 +63,8 @@ void FrameSenderWindow::setupGrid() ui->tableSender->setColumnWidth(ST_COLS::SENDTAB_COL_EXT, 50); ui->tableSender->setColumnWidth(ST_COLS::SENDTAB_COL_REM, 50); ui->tableSender->setColumnWidth(ST_COLS::SENDTAB_COL_DATA, 220); - ui->tableSender->setColumnWidth(ST_COLS::SENDTAB_COL_TRIGGER, 220); - ui->tableSender->setColumnWidth(ST_COLS::SENDTAB_COL_MODS, 220); + ui->tableSender->setColumnWidth(ST_COLS::SENDTAB_COL_TRIGGER, 270); + ui->tableSender->setColumnWidth(ST_COLS::SENDTAB_COL_MODS, 270); ui->tableSender->setColumnWidth(ST_COLS::SENDTAB_COL_COUNT, 80); ui->tableSender->setHorizontalHeaderLabels(headers); } @@ -693,6 +693,12 @@ void FrameSenderWindow::processModifierText(int line) //Example line: //d0 = D0 + 1,d1 = id:0x200:d3 + id:0x200:d4 AND 0xF0 - Original version //D0=D0+1,D1=ID:0x200:D3+ID:0x200:D4&0xF0 + + //[BMS_TargetVoltage]=45 would set the value of signal BMS_TargetVoltage to 45 + + //[BMS_TargetVoltage]=[0x234:BMS_CurrentVoltage] + 4 would instead grab the value + //of BMS_CurrentVoltage from ID 0x234, add 4 to it, and set BMS_TargetVoltage to that value. + //This is certainly much harder to parse than the trigger definitions. //the left side of the = has to be D0 to D7. After that there is a string of //data. Spaces used to be required but no longer are. This makes parsing harder but data entry easier @@ -710,22 +716,33 @@ void FrameSenderWindow::processModifierText(int line) Modifier thisMod; thisMod.destByte = 0; - QString leftSide = Utility::grabAlphaNumeric(mods[i]); - if (leftSide.startsWith("D") && leftSide.length() == 2) + QRegularExpression regex; + QRegularExpressionMatch match; + + regex.setPattern("^\\[(\\w+)]=(\\w+)"); + match = regex.match(mods[i]); + if (match.hasMatch()) { - thisMod.destByte = leftSide.right(1).toInt(); + thisMod.destByte = -1; + thisMod.signalName = match.captured(1); + mods[i] = match.captured(2); thisMod.operations.clear(); } else { - qDebug() << "Something wrong with lefthand val"; - continue; - } - if (!(Utility::grabOperation(mods[i]) == "=")) - { - qDebug() << "Err: No = after lefthand val"; - continue; + QString leftSide = Utility::grabAlphaNumeric(mods[i]); + if (leftSide.startsWith("D") && leftSide.length() == 2) + { + thisMod.destByte = leftSide.right(1).toInt(); + thisMod.operations.clear(); + if (!(Utility::grabOperation(mods[i]) == "=")) + { + qDebug() << "Err: No = after lefthand val"; + continue; + } + } } + abort = false; token = Utility::grabAlphaNumeric(mods[i]); diff --git a/mainwindow.cpp b/mainwindow.cpp index 8b14149..bcc65cf 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -164,6 +164,8 @@ MainWindow::MainWindow(QWidget *parent) : connect(ui->btnExpandAll, &QAbstractButton::clicked, this, &MainWindow::expandAllRows); connect(ui->btnCollapseAll, &QAbstractButton::clicked, this, &MainWindow::collapseAllRows); + connect(ui->tableSimpleSender, SIGNAL(cellChanged(int,int)), this, SLOT(onSenderCellChanged(int,int))); + lbStatusConnected.setText(tr("Connected to 0 buses")); lbHelp.setText(tr("Press F1 on any screen for help")); lbHelp.setAlignment(Qt::AlignCenter); @@ -227,12 +229,35 @@ MainWindow::MainWindow(QWidget *parent) : ui->actionMotorControlConfig->setVisible(false); ui->actionSingle_Multi_State_2->setVisible(false); + QStringList headers; + headers << "En" << "Bus" << "ID" << "Len" << "Ext" << "Rem" << "Data" + << "Interval" << "Count"; + ui->tableSimpleSender->setColumnCount(9); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_EN, 70); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_BUS, 70); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_ID, 70); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_LEN, 70); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_EXT, 70); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_REM, 70); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_DATA, 300); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_INTERVAL, 100); + ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_COUNT, 100); + ui->tableSimpleSender->setHorizontalHeaderLabels(headers); + + createSenderRow(); + + frameSender = new FrameSenderObject(model->getListReference()); + + frameSender->initialize(); //creates the thread and sets things up + frameSender->startSending(); //start the timer in the object so enabled things can send + installEventFilter(this); } MainWindow::~MainWindow() { updateTimer.stop(); + frameSender->stopSending(); killEmAll(); //Ride the lightning delete ui; delete model; @@ -438,6 +463,174 @@ void MainWindow::writeSettings() } } +void MainWindow::onSenderCellChanged(int row, int col) +{ + if (inhibitSenderChanged) return; + qDebug() << "onCellChanged"; + if (row == ui->tableSimpleSender->rowCount() - 1) + { + createSenderRow(); + } + + processSenderCellChange(row, col); +} + +void MainWindow::processSenderCellChange(int line, int col) +{ + qDebug() << "processSenderCellChange"; + FrameSendData *tempData; + QStringList tokens; + int tempVal; + + int numBuses = CANConManager::getInstance()->getNumBuses(); + QByteArray arr; + + tempData = frameSender->getSendRecordRef(line); + + if (!tempData) + { + FrameSendData dat; + frameSender->addSendRecord(dat); + tempData = frameSender->getSendRecordRef(line); + } + + if (!tempData) + { + qDebug() << "No data to modify in processSenderCellChange. This is a bug!"; + return; + } + + switch (col) + { + case SIMP_COL::SC_COL_EN: //Enable check box + if (ui->tableSimpleSender->item(line, 0)->checkState() == Qt::Checked) + { + tempData->enabled = true; + } + else tempData->enabled = false; + qDebug() << "Setting enabled to " << tempData->enabled; + break; + case SIMP_COL::SC_COL_BUS: //Bus designation + tempVal = Utility::ParseStringToNum(ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_BUS)->text()); + if (tempVal < -1) tempVal = -1; + if (tempVal >= numBuses) tempVal = numBuses - 1; + tempData->bus = tempVal; + qDebug() << "Setting bus to " << tempVal; + break; + case SIMP_COL::SC_COL_ID: //ID field + tempVal = Utility::ParseStringToNum(ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_ID)->text()); + if (tempVal < 0) tempVal = 0; + if (tempVal > 0x7FFFFFFF) tempVal = 0x7FFFFFFF; + tempData->setFrameId(tempVal); + if (tempData->frameId() > 0x7FF) { + tempData->setExtendedFrameFormat(true); + ui->tableSimpleSender->blockSignals(true); + ui->tableSimpleSender->item(line, ST_COLS::SENDTAB_COL_EXT)->setCheckState(Qt::Checked); + ui->tableSimpleSender->blockSignals(false); + } + qDebug() << "setting ID to " << tempVal; + break; + case SIMP_COL::SC_COL_LEN: + tempVal = Utility::ParseStringToNum(ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_LEN)->text()); + if (tempVal < 0) tempVal = 0; + if (tempVal > 8) tempVal = 8; + arr.resize(tempVal); + tempData->setPayload(arr); + break; + case SIMP_COL::SC_COL_EXT: + if (ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_EXT)->checkState() == Qt::Checked) { + tempData->setExtendedFrameFormat(true); + } else { + tempData->setExtendedFrameFormat(false); + } + break; + case SIMP_COL::SC_COL_REM: + if (ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_REM)->checkState() == Qt::Checked) { + tempData->setFrameType(QCanBusFrame::RemoteRequestFrame); + } else { + tempData->setFrameType(QCanBusFrame::DataFrame); + } + break; + case SIMP_COL::SC_COL_DATA: //Data bytes + for (int i = 0; i < 8; i++) tempData->payload().data()[i] = 0; + +#if QT_VERSION >= QT_VERSION_CHECK( 5, 14, 0 ) + tokens = ui->tableSimpleSender->item(line, ST_COLS::SENDTAB_COL_DATA)->text().split(" ", Qt::SkipEmptyParts); +#else + tokens = ui->tableSender->item(line, ST_COLS::SENDTAB_COL_DATA)->text().split(" ", QString::SkipEmptyParts); +#endif + arr.clear(); + arr.reserve(tokens.count()); + for (int j = 0; j < tokens.count(); j++) + { + arr.append((uint8_t)Utility::ParseStringToNum(tokens[j])); + } + tempData->setPayload(arr); + break; + case SIMP_COL::SC_COL_INTERVAL: //interval in ms + + QString trigger = ui->tableSimpleSender->item(line, ST_COLS::SENDTAB_COL_TRIGGER)->text().toUpper(); + + Trigger thisTrigger; + thisTrigger.bus = -1; //-1 means we don't care which + thisTrigger.ID = -1; //the rest of these being -1 means nothing has changed it + thisTrigger.maxCount = -1; + thisTrigger.milliseconds = -1; + thisTrigger.currCount = 0; + thisTrigger.msCounter = 0; + thisTrigger.triggerMask = 0; + thisTrigger.readyCount = true; + + tempData->triggers.clear(); + tempData->triggers.reserve(1); + + if (trigger != "") + { + thisTrigger.milliseconds = Utility::ParseStringToNum(trigger); + thisTrigger.triggerMask |= TriggerMask::TRG_MS; + } + + if (thisTrigger.milliseconds < 1) thisTrigger.milliseconds = 1; + + tempData->triggers.append(thisTrigger); + + break; + } +} + + +void MainWindow::createSenderRow() +{ + int row = ui->tableSimpleSender->rowCount(); + ui->tableSimpleSender->insertRow(row); + + QTableWidgetItem *item = new QTableWidgetItem(); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + inhibitSenderChanged = true; + ui->tableSimpleSender->setItem(row, SIMP_COL::SC_COL_EN, item); + + item = new QTableWidgetItem(); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->tableSimpleSender->setItem(row, SIMP_COL::SC_COL_EXT, item); + + item = new QTableWidgetItem(); + item->setFlags(item->flags() | Qt::ItemIsUserCheckable); + item->setCheckState(Qt::Unchecked); + ui->tableSimpleSender->setItem(row, SIMP_COL::SC_COL_REM, item); + + for (int i = 1; i <= SIMP_COL::SC_COL_COUNT; i++) + { + if (i != SIMP_COL::SC_COL_EXT && i != SIMP_COL::SC_COL_REM) { + item = new QTableWidgetItem(""); + ui->tableSimpleSender->setItem(row, i, item); + } + } + + inhibitSenderChanged = false; +} + void MainWindow::updateConnectionSettings(QString connectionType, QString port, int speed0, int speed1) { Q_UNUSED(connectionType); @@ -1371,6 +1564,11 @@ CANFrameModel* MainWindow::getCANFrameModel() return model; } + +/* + * All functions past this point set up the various other windows that can be opened +*/ + void MainWindow::showSettingsDialog() { if (!settingsDialog) diff --git a/mainwindow.h b/mainwindow.h index dbc7cdf..29daf73 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -10,7 +10,7 @@ #include "framefileio.h" #include "dbc/dbchandler.h" #include "bus_protocols/isotp_handler.h" - +#include "framesenderobject.h" #include "re/graphingwindow.h" #include "re/frameinfowindow.h" #include "frameplaybackwindow.h" @@ -40,6 +40,19 @@ class ConnectionWindow; class ISOTP_InterpreterWindow; class ScriptingWindow; +enum SIMP_COL +{ + SC_COL_EN = 0, + SC_COL_BUS = 1, + SC_COL_ID = 2, + SC_COL_LEN = 3, + SC_COL_EXT = 4, + SC_COL_REM = 5, + SC_COL_DATA = 6, + SC_COL_INTERVAL = 7, + SC_COL_COUNT = 8, +}; + namespace Ui { class MainWindow; } @@ -114,6 +127,7 @@ private slots: void filterClearAll(); void headerClicked (int logicalIndex); void DBCSettingsUpdated(); + void onSenderCellChanged(int, int); public slots: void gotFrames(int); @@ -142,6 +156,7 @@ private: QByteArray inputBuffer; QTimer updateTimer; QElapsedTimer *elapsedTime; + FrameSenderObject *frameSender; int framesPerSec; int rxFrames; bool inhibitFilterUpdate; @@ -151,6 +166,7 @@ private: bool CSVAbsTime; bool bDirty; //have frames been added or subtracted since the last save/load? bool useFiltered; //should sub-windows use the unfiltered or filtered frames list? + bool inhibitSenderChanged; bool continuousLogging; int continuousLogFlushCounter; @@ -211,6 +227,8 @@ private: bool eventFilter(QObject *obj, QEvent *event); void manageRowExpansion(); void disableAutoRowExpansion(); + void createSenderRow(); + void processSenderCellChange(int line, int col); }; #endif // MAINWINDOW_H diff --git a/ui/framesenderwindow.ui b/ui/framesenderwindow.ui index 4e027bd..0f0bebf 100644 --- a/ui/framesenderwindow.ui +++ b/ui/framesenderwindow.ui @@ -6,7 +6,7 @@ 0 0 - 1008 + 1350 611 diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index 3b71991..80af4a8 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -6,8 +6,8 @@ 0 0 - 800 - 682 + 1177 + 717 @@ -26,340 +26,347 @@ SavvyCAN - + - - - - 11 - false - - - - true - - - QAbstractItemView::ScrollPerPixel - - - false - - - - - + - - - true - - - Remote Connection Active: - - - Qt::AlignCenter - - - - - - - true - - - - 1000 - 50 - - - - - 150 - 50 - - - - - false - - - - 8 - - - Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter - - - true - - - - - - - - - - - - 255 - 0 - 0 - - - - - - - - - 255 - 0 - 0 - - - - - - - - - 120 - 120 - 120 - - - - - - + 11 - true - true + false - - - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - Total Frames Captured: - - - - - - - - 0 - 0 - - - - - - - - - - - - 0 - 0 - - - - Frames Per Second: - - - - - - - - 0 - 0 - - - - - - - - - - - Suspend Capturing - - - false - - - - - - - Normalize Frame Timing - - - - - - - Clear Frames - - - - - - - Keep Filters When Clearing - - - - - - - Auto Scroll Window - - - - - - + true - - Overwrite Mode + + QAbstractItemView::ScrollPerPixel - - - - - - Qt::Horizontal - - - - - - - Interpret Frames - - - - - - - Expand All Rows - - - - - - - Collapse All Rows - - - - - - - Bus Filtering: - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - - 175 - 40 - - - - QListView::LeftToRight - - - - - - - Frame Filtering: - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - - 0 - 160 - - - - - 175 - 16777215 - - - + false - + - + + + true + - All + Remote Connection Active: + + + Qt::AlignCenter - - - None + + + true + + + + 1000 + 50 + + + + + 150 + 50 + + + + + false + + + + 8 + + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter + + + true + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 255 + 0 + 0 + + + + + + + + + 120 + 120 + 120 + + + + + + + + + 11 + true + true + + + + + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + Total Frames Captured: + + + + + + + + 0 + 0 + + + + + + + + + + + + 0 + 0 + + + + Frames Per Second: + + + + + + + + 0 + 0 + + + + + + + + + + + Suspend Capturing + + + false + + + + + + + Normalize Frame Timing + + + + + + + Clear Frames + + + + + + + Keep Filters When Clearing + + + + + + + Auto Scroll Window + + + + + + + true + + + Overwrite Mode + + + + + + + Qt::Horizontal + + + + + + + Interpret Frames + + + + + + + Expand All Rows + + + + + + + Collapse All Rows + + + + + + + Bus Filtering: + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 175 + 40 + + + + QListView::LeftToRight + + + + + + + Frame Filtering: + + + Qt::AlignCenter + + + + + + + + 0 + 0 + + + + + 0 + 160 + + + + + 175 + 16777215 + + + + false + + + + + + + + + All + + + + + + + None + + + + + + + + @@ -367,7 +374,7 @@ 0 0 - 800 + 1177 26 From 47a5aa50858fde9a4315a3b1828b0bb7ac9cbcd7 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 13 Feb 2024 21:52:52 -0500 Subject: [PATCH 21/57] Sender at bottom of main window is now operational. --- framesenderobject.cpp | 18 +++++++++------- mainwindow.cpp | 36 +++++++++++++++++++------------- mainwindow.h | 11 +++++----- ui/mainwindow.ui | 48 +++++++++++++++++++++++-------------------- 4 files changed, 64 insertions(+), 49 deletions(-) diff --git a/framesenderobject.cpp b/framesenderobject.cpp index 9212747..f9eb468 100644 --- a/framesenderobject.cpp +++ b/framesenderobject.cpp @@ -154,12 +154,15 @@ void FrameSenderObject::timerTriggered() sendingList.clear(); if(mutex.tryLock()) { - //get elapsed microseconds since last tick (in case timer skips or is otherwise inaccurate, though there are no guarantees about elapsed timer either) - quint64 elapsed = sendingElapsed.nsecsElapsed() / 1000; + /* + * Requested tick interval was 1ms but the actual interval could be wildly different. So, we track + * by counting microseconds and accumulating. This creates decent stability even for long intervals + */ + quint64 elapsed = sendingElapsed.nsecsElapsed() / 1000ul; if (elapsed == 0) elapsed = 1; sendingElapsed.start(); sendingLastTimeStamp += elapsed; - //qDebug() << playbackLastTimeStamp; + //qDebug() << sendingLastTimeStamp; //qDebug() << "El: " << elapsed; statusCounter++; for (int i = 0; i < sendingData.count(); i++) @@ -178,19 +181,20 @@ void FrameSenderObject::timerTriggered() } if (sendData->triggers.count() == 0) { - qDebug() << "No triggers to process"; + //qDebug() << "No triggers to process"; break; } for (int j = 0; j < sendData->triggers.count(); j++) { trigger = &sendData->triggers[j]; - if (trigger->currCount >= trigger->maxCount) continue; //don't process if we've sent max frames we were supposed to + //if ( (trigger->currCount >= trigger->maxCount) || (trigger->maxCount == -1) ) continue; //don't process if we've sent max frames we were supposed to if (!trigger->readyCount) continue; //don't tick if not ready to tick //is it time to fire? trigger->msCounter += elapsed; //gives proper tracking even if timer doesn't fire as fast as it should - if (trigger->msCounter >= trigger->milliseconds) + //qDebug() << trigger->msCounter; + if (trigger->msCounter >= (trigger->milliseconds * 1000)) { - trigger->msCounter -= trigger->milliseconds; + trigger->msCounter -= (trigger->milliseconds * 1000); sendData->count++; trigger->currCount++; doModifiers(i); diff --git a/mainwindow.cpp b/mainwindow.cpp index bcc65cf..9e6fea3 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -230,13 +230,12 @@ MainWindow::MainWindow(QWidget *parent) : ui->actionSingle_Multi_State_2->setVisible(false); QStringList headers; - headers << "En" << "Bus" << "ID" << "Len" << "Ext" << "Rem" << "Data" + headers << "En" << "Bus" << "ID" << "Ext" << "Rem" << "Data" << "Interval" << "Count"; - ui->tableSimpleSender->setColumnCount(9); + ui->tableSimpleSender->setColumnCount(8); ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_EN, 70); ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_BUS, 70); ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_ID, 70); - ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_LEN, 70); ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_EXT, 70); ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_REM, 70); ui->tableSimpleSender->setColumnWidth(SIMP_COL::SC_COL_DATA, 300); @@ -489,7 +488,12 @@ void MainWindow::processSenderCellChange(int line, int col) if (!tempData) { + qDebug() << "Need to set up a new entry in senders"; FrameSendData dat; + dat.enabled = false; + dat.count = 0; + dat.frameCount = 0; + dat.bus = 0; frameSender->addSendRecord(dat); tempData = frameSender->getSendRecordRef(line); } @@ -530,13 +534,6 @@ void MainWindow::processSenderCellChange(int line, int col) } qDebug() << "setting ID to " << tempVal; break; - case SIMP_COL::SC_COL_LEN: - tempVal = Utility::ParseStringToNum(ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_LEN)->text()); - if (tempVal < 0) tempVal = 0; - if (tempVal > 8) tempVal = 8; - arr.resize(tempVal); - tempData->setPayload(arr); - break; case SIMP_COL::SC_COL_EXT: if (ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_EXT)->checkState() == Qt::Checked) { tempData->setExtendedFrameFormat(true); @@ -555,9 +552,9 @@ void MainWindow::processSenderCellChange(int line, int col) for (int i = 0; i < 8; i++) tempData->payload().data()[i] = 0; #if QT_VERSION >= QT_VERSION_CHECK( 5, 14, 0 ) - tokens = ui->tableSimpleSender->item(line, ST_COLS::SENDTAB_COL_DATA)->text().split(" ", Qt::SkipEmptyParts); + tokens = ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_DATA)->text().split(" ", Qt::SkipEmptyParts); #else - tokens = ui->tableSender->item(line, ST_COLS::SENDTAB_COL_DATA)->text().split(" ", QString::SkipEmptyParts); + tokens = ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_DATA)->text().split(" ", QString::SkipEmptyParts); #endif arr.clear(); arr.reserve(tokens.count()); @@ -569,7 +566,7 @@ void MainWindow::processSenderCellChange(int line, int col) break; case SIMP_COL::SC_COL_INTERVAL: //interval in ms - QString trigger = ui->tableSimpleSender->item(line, ST_COLS::SENDTAB_COL_TRIGGER)->text().toUpper(); + QString trigger = ui->tableSimpleSender->item(line, SIMP_COL::SC_COL_INTERVAL)->text().toUpper(); Trigger thisTrigger; thisTrigger.bus = -1; //-1 means we don't care which @@ -598,7 +595,6 @@ void MainWindow::processSenderCellChange(int line, int col) } } - void MainWindow::createSenderRow() { int row = ui->tableSimpleSender->rowCount(); @@ -1018,6 +1014,18 @@ void MainWindow::tickGUIUpdate() } } + //refresh the count for all the frame senders + FrameSendData *tempData; + int numRows = ui->tableSimpleSender->rowCount(); + for (int i = 0; i < numRows; i++) + { + tempData = frameSender->getSendRecordRef(i); + if (tempData) + { + ui->tableSimpleSender->item(i, SIMP_COL::SC_COL_COUNT)->setText(QString::number( tempData->count )); + } + } + rxFrames = 0; //} } diff --git a/mainwindow.h b/mainwindow.h index 29daf73..31df02c 100644 --- a/mainwindow.h +++ b/mainwindow.h @@ -45,12 +45,11 @@ enum SIMP_COL SC_COL_EN = 0, SC_COL_BUS = 1, SC_COL_ID = 2, - SC_COL_LEN = 3, - SC_COL_EXT = 4, - SC_COL_REM = 5, - SC_COL_DATA = 6, - SC_COL_INTERVAL = 7, - SC_COL_COUNT = 8, + SC_COL_EXT = 3, + SC_COL_REM = 4, + SC_COL_DATA = 5, + SC_COL_INTERVAL = 6, + SC_COL_COUNT = 7, }; namespace Ui { diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index 80af4a8..8828f21 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -26,27 +26,34 @@ SavvyCAN - + - + - - - - 11 - false - - - - true - - - QAbstractItemView::ScrollPerPixel - - - false - - + + + + + + 11 + false + + + + true + + + QAbstractItemView::ScrollPerPixel + + + false + + + + + + + @@ -364,9 +371,6 @@ - - - From 319127c3305a73f855c1b6257efe1fad40b5f11c Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 18 Mar 2024 21:13:02 -0400 Subject: [PATCH 22/57] Fix issue where graph windows would be corrupted if a new file was loaded while graphs were visible. --- re/graphingwindow.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/re/graphingwindow.cpp b/re/graphingwindow.cpp index d2f2581..80b32d5 100644 --- a/re/graphingwindow.cpp +++ b/re/graphingwindow.cpp @@ -1333,7 +1333,8 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) for (int i = 0; i < modelFrames->count(); i++) { CANFrame thisFrame = modelFrames->at(i); - if (thisFrame.frameId() == params.ID && thisFrame.frameType() == QCanBusFrame::DataFrame && ( ( params.bus == -1) || (params.bus == thisFrame.bus) ) ) frameCache.append(thisFrame); + if ( (thisFrame.frameId() == params.ID) && (thisFrame.frameType() == QCanBusFrame::DataFrame) + && ( ( params.bus == -1) || (params.bus == thisFrame.bus) ) ) frameCache.append(thisFrame); } //to fix weirdness where a graph that has no data won't be able to be edited, selected, or deleted properly @@ -1352,6 +1353,8 @@ void GraphingWindow::createGraph(GraphParams ¶ms, bool createGraphParam) int numEntries = frameCache.count() / params.stride; if (numEntries < 1) numEntries = 1; //could happen if stride is larger than frame count + params.x.clear(); + params.y.clear(); params.x.reserve(numEntries); params.y.reserve(numEntries); //params.x.fill(0, numEntries); From 636e849607a6c22ad80faf8456fa502ed9d0f6d7 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 18 Mar 2024 21:48:27 -0400 Subject: [PATCH 23/57] Some tweaking to try to make the filters area of the main screen a little wider --- ui/mainwindow.ui | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ui/mainwindow.ui b/ui/mainwindow.ui index 8828f21..72d9cec 100644 --- a/ui/mainwindow.ui +++ b/ui/mainwindow.ui @@ -48,6 +48,9 @@ false + + false + @@ -327,20 +330,20 @@ - + 0 0 - 0 + 300 160 - 175 + 1000 16777215 From 232af4e21480d2f28f85febc1636f31b2acc8008 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 18 Mar 2024 21:52:22 -0400 Subject: [PATCH 24/57] Fix signal viewer so that it handles multiplexed signals properly --- signalviewerwindow.cpp | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/signalviewerwindow.cpp b/signalviewerwindow.cpp index f21c7f9..c31f0fb 100644 --- a/signalviewerwindow.cpp +++ b/signalviewerwindow.cpp @@ -98,15 +98,18 @@ void SignalViewerWindow::processFrame(CANFrame &frame) if (!sig) return; if (sig->parentMessage->ID == frame.frameId()) { - if (sig->processAsText(frame, sigString, false)) //if true we could interpret the signal so update it in the list + if (sig->isSignalInMessage(frame)) //filter out multiplexed signals that aren't in this message. { - QTableWidgetItem *item = ui->tableViewer->item(i, VALUE_COL); - if (!item) + if (sig->processAsText(frame, sigString, false)) //if true we could interpret the signal so update it in the list { - item = new QTableWidgetItem(sigString); - ui->tableViewer->setItem(i, VALUE_COL, item); + QTableWidgetItem *item = ui->tableViewer->item(i, VALUE_COL); + if (!item) + { + item = new QTableWidgetItem(sigString); + ui->tableViewer->setItem(i, VALUE_COL, item); + } + else item->setText(sigString); } - else item->setText(sigString); } } } From 1e0e30f0b0cdb926ed831caa4460bb2dd82faa5d Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Thu, 28 Mar 2024 19:07:21 -0400 Subject: [PATCH 25/57] Update readme --- README.md | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index e4cc5ca..cdcdbb5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SavvyCAN Qt based cross platform canbus tool -(C) 2015-2021 EVTV and Collin Kidder +(C) 2015-2024 Collin Kidder A Qt5 based cross platform tool which can be used to load, save, and capture canbus frames. This tool is designed to help with visualization, reverse engineering, debugging, and @@ -17,9 +17,6 @@ The CANDue board must be running the GVRET firmware which can also be found within the collin80 repos. It is now possible to use any Qt SerialBus driver (socketcan, Vector, PeakCAN, TinyCAN). -There may, however, be some loss of some functionality as -some functions of SavvyCAN are designed for use directly with the -EVTVDue and CANDue 2.0 boards. It should, however, be noted that use of a capture device is not required to make use of this program. It can load and save in several formats: @@ -50,8 +47,9 @@ to download it separately. This project requires 5.14.0 or higher because of a dependency on QtSerialBus and other new additions to Qt. -NOTE: Qt6 currently lacks support for QtSerialBus and many other Qt sub-features. At this time you cannot -use Qt6 to compile SavvyCAN. Support for Qt6 should be possible around Qt6.2. +NOTE: As the code in this master branch sits, it will not properly compile with QT6. +However, there is a QT6WIP branch that should successfully compile. The QT6WIP branch may not +function entirely properly yet. ## Instructions for compiling: From 33c8b24b7c328490f1c9f52723e72e973295c2c6 Mon Sep 17 00:00:00 2001 From: Guinea Wheek Date: Thu, 2 May 2024 13:34:18 -0700 Subject: [PATCH 26/57] add support for SIG_VALTYPE_ --- dbc/dbchandler.cpp | 43 +++++++++++++++++++++++++++++++++++++++++++ dbc/dbchandler.h | 1 + 2 files changed, 44 insertions(+) diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index acf3aa1..f55118b 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -674,6 +674,44 @@ bool DBCFile::parseSignalMultiplexValueLine(QString line) return false; } +bool DBCFile::parseSignalValueTypeLine(QString line) +{ + QRegularExpression regex; + QRegularExpressionMatch match; + qDebug() << "Found a signal valtype line"; + regex.setPattern("^SIG\\_VALTYPE\\_ *(\\d+) *([-\\w]+) *: *(\\d+);"); + match = regex.match(line); + + // captured 1 is the message id + // captured 2 is the signal name + // captured 3 is the valtype + if (!match.hasMatch()) { return false; } + uint32_t id = match.captured(1).toULong() & 0x1FFFFFFFUL; + + DBC_MESSAGE *msg = messageHandler->findMsgByID(match.captured(1).toULong() & 0x1FFFFFFFUL); + if (msg == nullptr) { return false; } + + DBC_SIGNAL *thisSignal = msg->sigHandler->findSignalByName(match.captured(2)); + if (thisSignal == nullptr) { return false; } + int valType = match.captured(3).toInt(); + + switch (valType) { + case 1: { + thisSignal->valType = SP_FLOAT; + break; + } + case 2: { + thisSignal->valType = DP_FLOAT; + break; + } + default: { + return false; + } + } + return true; +} + + bool DBCFile::parseValueLine(QString line) { QRegularExpression regex; @@ -941,6 +979,11 @@ bool DBCFile::loadFile(QString fileName) if (!parseSignalMultiplexValueLine(line)) numSigFaults++; } + if (line.startsWith("SIG_VALTYPE_ ")) //defines a signal value type + { + if (!parseSignalValueTypeLine(line)) numSigFaults++; + } + if (line.startsWith("BU_:")) //line specifies the nodes on this canbus { qDebug() << "Found a BU line"; diff --git a/dbc/dbchandler.h b/dbc/dbchandler.h index 3327c18..14f19f8 100644 --- a/dbc/dbchandler.h +++ b/dbc/dbchandler.h @@ -106,6 +106,7 @@ private: bool parseSignalMultiplexValueLine(QString line); DBC_MESSAGE* parseMessageLine(QString line); bool parseValueLine(QString line); + bool parseSignalValueTypeLine(QString line); bool parseAttributeLine(QString line); bool parseDefaultAttrLine(QString line); }; From e732edc8e3992aef688aaaaae6abab0f6c1e39d1 Mon Sep 17 00:00:00 2001 From: Ben Evans Date: Fri, 10 May 2024 07:32:26 +1000 Subject: [PATCH 27/57] Add support for reading Wireshark SocketCAN log format --- README.md | 1 + framefileio.cpp | 131 +++++++++++++++++++++++++++++++++++++++++++----- framefileio.h | 2 + pcaplite.cpp | 21 ++++++-- pcaplite.h | 6 ++- 5 files changed, 144 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index cdcdbb5..8c7c66c 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ of this program. It can load and save in several formats: 9. Vehicle Spy log files 10. CANDump / Kayak (Read only) 11. PCAN Viewer (Read Only) +12. Wireshark socketcan PCAP file (Read only) ## Dependencies diff --git a/framefileio.cpp b/framefileio.cpp index f7e8658..4649b1a 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -191,6 +191,7 @@ bool FrameFileIO::loadFrameFile(QString &fileName, QVector* frameCache filters.append(QString(tr("CLX000 (*.txt *.TXT)"))); filters.append(QString(tr("CANServer Binary Log (*.log *.LOG)"))); filters.append(QString(tr("Wireshark (*.pcap *.PCAP *.pcapng *.PCAPNG)"))); + filters.append(QString(tr("Wireshark SocketCAN (*.pcap *.PCAP"))); dialog.setDirectory(settings.value("FileIO/LoadSaveDirectory", dialog.directory().path()).toString()); dialog.setFileMode(QFileDialog::ExistingFile); @@ -237,6 +238,7 @@ bool FrameFileIO::loadFrameFile(QString &fileName, QVector* frameCache if (selectedNameFilter == filters[22]) result = loadCLX000File(filename, frameCache); if (selectedNameFilter == filters[23]) result = loadCANServerFile(filename, frameCache); if (selectedNameFilter == filters[24]) result = loadWiresharkFile(filename, frameCache); + if (selectedNameFilter == filters[25]) result = loadWiresharkSocketCANFile(filename, frameCache); progress.cancel(); @@ -288,6 +290,28 @@ bool FrameFileIO::autoDetectLoadFile(QString filename, QVector* frames } } + // Attempt to load socket CAN first to avoid generic wireshark logic catching it + qDebug() << "Attempting Wireshark Socket CAN Log"; + if (isWiresharkSocketCANFile(filename)) + { + if (loadWiresharkSocketCANFile(filename, frames)) + { + qDebug() << "Loaded as Wireshark SocketCAN Log successfully!"; + return true; + } + } + + // This and the decoder above were both moved above TeslaAPFile as they match based on magic numbers and sometimes these files were falling into the TeslaAP decoder + qDebug() << "Attempting Wireshark Log"; + if (isWiresharkFile(filename)) + { + if (loadWiresharkFile(filename, frames)) + { + qDebug() << "Loaded as Wireshark Log successfully!"; + return true; + } + } + qDebug() << "Attempting Tesla AP Snapshot"; if (isTeslaAPFile(filename)) { @@ -308,16 +332,6 @@ bool FrameFileIO::autoDetectLoadFile(QString filename, QVector* frames } } - qDebug() << "Attempting Wireshark Log"; - if (isWiresharkFile(filename)) - { - if (loadWiresharkFile(filename, frames)) - { - qDebug() << "Loaded as Wireshark Log successfully!"; - return true; - } - } - qDebug() << "Attempting canalyzer ASC"; if (isCanalyzerASC(filename)) { @@ -5023,7 +5037,7 @@ bool FrameFileIO::loadWiresharkFile(QString filename, QVector* frames) QByteArray ba = filename.toLocal8Bit(); - pcap_data_file = pcap_open_offline(ba.data(), errbuf); + pcap_data_file = pcap_open_offline(ba.data(), errbuf, PCAP_LINKTYPE_ANY); if (!pcap_data_file) { return false; } @@ -5079,7 +5093,7 @@ bool FrameFileIO::isWiresharkFile(QString filename) char errbuf[PCAP_ERRBUF_SIZE]; QByteArray ba = filename.toLocal8Bit(); - pcap_data_file = pcap_open_offline(ba.data(), errbuf); + pcap_data_file = pcap_open_offline(ba.data(), errbuf, PCAP_LINKTYPE_ANY); if (!pcap_data_file) { return false; } @@ -5089,3 +5103,96 @@ bool FrameFileIO::isWiresharkFile(QString filename) return true; } + +bool FrameFileIO::loadWiresharkSocketCANFile(QString filename, QVector* frames) +{ + pcap_t *pcap_data_file; + CANFrame thisFrame; + long long startTimestamp = 0; + long long timeStamp; + int lineCounter = 0; + bool foundErrors = false; + pcap_pkthdr packetHeader; + const char *packetData = NULL; + char errbuf[PCAP_ERRBUF_SIZE]; + + QByteArray ba = filename.toLocal8Bit(); + + pcap_data_file = pcap_open_offline(ba.data(), errbuf, PCAP_LINKTYPE_SOCKETCAN); + if (!pcap_data_file) { + return false; + } + + packetData = (const char*)pcap_next(pcap_data_file, &packetHeader); + while (packetData) { + lineCounter++; + if (lineCounter > 100) { + qApp->processEvents(); + lineCounter = 0; + } + thisFrame.bus = 0; + + // Timestamp + timeStamp = packetHeader.ts.tv_sec * 1000000 + packetHeader.ts.tv_usec; + if (0 == startTimestamp) { + startTimestamp = timeStamp; + } + timeStamp -= startTimestamp; + thisFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); + + // ID and extended frame format + const quint32 can_id = qFromBigEndian(packetData); + if (can_id & 0x80000000) { + thisFrame.setExtendedFrameFormat(true); + thisFrame.setFrameId(0x1fffffff & can_id); + } else { + thisFrame.setExtendedFrameFormat(false); + thisFrame.setFrameId(0x7ff & can_id); + } + + // Frame type + if (can_id & 0x20000000U) { + thisFrame.setFrameType(QCanBusFrame::ErrorFrame); + } else if (can_id & 0x40000000U) { + thisFrame.setFrameType(QCanBusFrame::RemoteRequestFrame); + } else { + thisFrame.setFrameType(QCanBusFrame::DataFrame); + } + + // Direction - This isn't actually officially supported, but CAN Bus Debugger device logs set this byte to 1 to indicate a TX frame and 0 for RX + quint8 direction = (quint8) *(packetData + 6); + thisFrame.isReceived = (direction != 1); + + // Data + quint8 numBytes = (quint8) *(packetData + 4); + if (numBytes > 8) { + numBytes = 8; + } + QByteArray bytes(numBytes, 0); + for (int d = 0; d < numBytes; d++) { + bytes[d] = *(packetData + 8 + d); + } + thisFrame.setPayload(bytes); + frames->append(thisFrame); + + packetData = (const char*) pcap_next(pcap_data_file, &packetHeader); + } + pcap_close(pcap_data_file); + pcap_data_file = NULL; + return !foundErrors; +} + +bool FrameFileIO::isWiresharkSocketCANFile(QString filename) +{ + pcap_t *pcap_data_file; + char errbuf[PCAP_ERRBUF_SIZE]; + QByteArray ba = filename.toLocal8Bit(); + + pcap_data_file = pcap_open_offline(ba.data(), errbuf, PCAP_LINKTYPE_SOCKETCAN); + if (!pcap_data_file) { + return false; + } + pcap_close(pcap_data_file); + pcap_data_file = NULL; + return true; +} diff --git a/framefileio.h b/framefileio.h index 3d52517..7171591 100644 --- a/framefileio.h +++ b/framefileio.h @@ -54,6 +54,7 @@ public: static bool loadCLX000File(QString filename, QVector* frames); static bool loadCANServerFile(QString filename, QVector* frames); static bool loadWiresharkFile(QString filename, QVector* frames); + static bool loadWiresharkSocketCANFile(QString filename, QVector* frames); //functions that pre-scan a file to try to figure out if they could read it. Used to automatically determine //file type and load it. @@ -80,6 +81,7 @@ public: static bool isCLX000File(QString filename); static bool isCANServerFile(QString filename); static bool isWiresharkFile(QString filename); + static bool isWiresharkSocketCANFile(QString filename); static bool saveCRTDFile(QString, const QVector*); static bool saveNativeCSVFile(QString, const QVector*); diff --git a/pcaplite.cpp b/pcaplite.cpp index 189b2ce..a0f6c90 100644 --- a/pcaplite.cpp +++ b/pcaplite.cpp @@ -19,7 +19,7 @@ static unsigned char pcap_buffer[MAX_CAN_PACKET_SIZE]; static pcap_t p; -pcap *pcap_open_offline(const char *filename, char *error_text) { +pcap *pcap_open_offline(const char *filename, char *error_text, int expected_link_type) { FILE *file; snprintf(error_text, PCAP_ERRBUF_SIZE, "OK"); @@ -48,6 +48,23 @@ pcap *pcap_open_offline(const char *filename, char *error_text) { return (NULL); } + unsigned int link_type; + fseek(file, PCAP_FILE_HEADER_LENGTH - 4, SEEK_SET); + bytes_read = fread(&link_type, 1, sizeof(link_type), file); + if (bytes_read != sizeof(link_type)) { + snprintf(error_text, PCAP_ERRBUF_SIZE, "Cannot read linktype word"); + fclose(file); + return (NULL); + } + if (expected_link_type >= 0) { + // Check the link type + if ((int) link_type != expected_link_type) { + snprintf(error_text, PCAP_ERRBUF_SIZE, "This link type is not supported by this decoder"); + fclose(file); + return (NULL); + } + } + // set the format // and seek past file header if (MAGIC_NG == magic) { @@ -61,14 +78,12 @@ pcap *pcap_open_offline(const char *filename, char *error_text) { return (NULL); } fseek(file, section_length, SEEK_SET); - } else { p.is_ng = 0; fseek(file, PCAP_FILE_HEADER_LENGTH, SEEK_SET); } p.file = file; - return(&p); } diff --git a/pcaplite.h b/pcaplite.h index 8a48153..02fbb77 100644 --- a/pcaplite.h +++ b/pcaplite.h @@ -8,7 +8,9 @@ #include #endif -#define PCAP_ERRBUF_SIZE 256 +#define PCAP_ERRBUF_SIZE (256) +#define PCAP_LINKTYPE_SOCKETCAN (227) +#define PCAP_LINKTYPE_ANY (-1) struct pcap_pkthdr { struct timeval ts; /* time stamp */ @@ -23,7 +25,7 @@ struct pcap { typedef struct pcap pcap_t; -pcap *pcap_open_offline(const char *, char *); +pcap *pcap_open_offline(const char *, char *, int); const unsigned char *pcap_next(pcap_t *, struct pcap_pkthdr *); From 17d369491d5282f2a11526aa7494406016a8c276 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 13 May 2024 19:30:58 -0400 Subject: [PATCH 28/57] Update README.md --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index cdcdbb5..0574fef 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,9 @@ to download it separately. This project requires 5.14.0 or higher because of a dependency on QtSerialBus and other new additions to Qt. -NOTE: As the code in this master branch sits, it will not properly compile with QT6. -However, there is a QT6WIP branch that should successfully compile. The QT6WIP branch may not -function entirely properly yet. +NOTE: As the code in this master branch sits, it does compile with QT6. Support for QT6 is approximately "beta" quality. Most all functions should work, please send issues if found. + +It appears that the current binary build for MacOS requires at least MacOS 10.15 ## Instructions for compiling: From 41b5afbb0e3fecc35bbbe48fe59455b742105a54 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Thu, 23 May 2024 10:55:19 -0600 Subject: [PATCH 29/57] make qmake check Qt version This provides a more helpful compile-time error message, hopefully preventing issues like #727 (https://github.com/collin80/SavvyCAN/issues/727). --- SavvyCAN.pro | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/SavvyCAN.pro b/SavvyCAN.pro index 5512e42..ceb0aae 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -4,6 +4,10 @@ # #------------------------------------------------- +!versionAtLeast(QT_VERSION, 5.14.0) { + error("Current version of Qt ($${QT_VERSION}) is too old, this project requires Qt 5.14 or newer") +} + QT = core gui printsupport qml serialbus serialport widgets help network opengl CONFIG(release, debug|release):DEFINES += QT_NO_DEBUG_OUTPUT From ca5d231107566aabcba012362ccfcc88ba62c9c2 Mon Sep 17 00:00:00 2001 From: Ben Evans Date: Wed, 29 May 2024 19:24:58 +1000 Subject: [PATCH 30/57] Fix indentation issues. --- framefileio.cpp | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/framefileio.cpp b/framefileio.cpp index 4649b1a..eded03b 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -5113,15 +5113,15 @@ bool FrameFileIO::loadWiresharkSocketCANFile(QString filename, QVector int lineCounter = 0; bool foundErrors = false; pcap_pkthdr packetHeader; - const char *packetData = NULL; - char errbuf[PCAP_ERRBUF_SIZE]; + const char *packetData = NULL; + char errbuf[PCAP_ERRBUF_SIZE]; QByteArray ba = filename.toLocal8Bit(); pcap_data_file = pcap_open_offline(ba.data(), errbuf, PCAP_LINKTYPE_SOCKETCAN); - if (!pcap_data_file) { - return false; - } + if (!pcap_data_file) { + return false; + } packetData = (const char*)pcap_next(pcap_data_file, &packetHeader); while (packetData) { @@ -5143,8 +5143,8 @@ bool FrameFileIO::loadWiresharkSocketCANFile(QString filename, QVector // ID and extended frame format const quint32 can_id = qFromBigEndian(packetData); if (can_id & 0x80000000) { - thisFrame.setExtendedFrameFormat(true); - thisFrame.setFrameId(0x1fffffff & can_id); + thisFrame.setExtendedFrameFormat(true); + thisFrame.setFrameId(0x1fffffff & can_id); } else { thisFrame.setExtendedFrameFormat(false); thisFrame.setFrameId(0x7ff & can_id); @@ -5178,7 +5178,7 @@ bool FrameFileIO::loadWiresharkSocketCANFile(QString filename, QVector packetData = (const char*) pcap_next(pcap_data_file, &packetHeader); } pcap_close(pcap_data_file); - pcap_data_file = NULL; + pcap_data_file = NULL; return !foundErrors; } @@ -5189,10 +5189,10 @@ bool FrameFileIO::isWiresharkSocketCANFile(QString filename) QByteArray ba = filename.toLocal8Bit(); pcap_data_file = pcap_open_offline(ba.data(), errbuf, PCAP_LINKTYPE_SOCKETCAN); - if (!pcap_data_file) { - return false; - } + if (!pcap_data_file) { + return false; + } pcap_close(pcap_data_file); - pcap_data_file = NULL; + pcap_data_file = NULL; return true; } From 7b297bd8957bdc0228d7ff03e7d4739dda91c431 Mon Sep 17 00:00:00 2001 From: Sebastian Kuzminsky Date: Tue, 18 Jun 2024 13:58:45 -0600 Subject: [PATCH 31/57] signal makePrettyOutput: add a space between value and unit Instead of rendering as "45Celsius" it now renders as "45 Celsius". --- dbc/dbc_classes.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dbc/dbc_classes.cpp b/dbc/dbc_classes.cpp index da44f80..9ea594a 100644 --- a/dbc/dbc_classes.cpp +++ b/dbc/dbc_classes.cpp @@ -214,12 +214,12 @@ QString DBC_SIGNAL::makePrettyOutput(double floatVal, int64_t intVal, bool outpu } } if (!foundVal) outputString += QString::number(intVal); - if (outputUnit) outputString += unitName; + if (outputUnit) outputString += " " + unitName; } else //otherwise display the actual number and unit (if it exists) { outputString += (isInteger ? QString::number(intVal) : QString::number(floatVal)); - if (outputUnit) outputString += unitName; + if (outputUnit) outputString += " " + unitName; } return outputString; } From 6da492f0eb4c18a04746a8106302169330a3b0d0 Mon Sep 17 00:00:00 2001 From: Superwofy Date: Fri, 21 Jun 2024 12:55:26 +0100 Subject: [PATCH 32/57] Added/corrected timestamps for LAWICEL protocol. Fixed System Clock timestamps and added decoding for timestamps coming from the tool. --- connections/lawicel_serial.cpp | 21 ++++++++++++++++++++- connections/lawicel_serial.h | 1 + 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/connections/lawicel_serial.cpp b/connections/lawicel_serial.cpp index 34b4fd4..ddf9e4d 100644 --- a/connections/lawicel_serial.cpp +++ b/connections/lawicel_serial.cpp @@ -491,7 +491,26 @@ void LAWICELSerial::readSerialData() { qDebug() << "Got CR!"; - buildFrame.setTimeStamp(QDateTime::currentMSecsSinceEpoch() * 1000l); + if (useSystemTime) + { + buildTimestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; + } + else + { + //If total length is greater than command, header and data, timestamps must be enabled. + if (data.length() > (5 + mBuildLine.mid(4, 1).toInt() * 2 + 1)) + { + //Four bytes after the end of the data bytes. + buildTimestamp = mBuildLine.mid(5 + mBuildLine.mid(4, 1).toInt() * 2, 4).toInt(nullptr, 16) * 1000l; + } + else + { + //Default to system time if timestamps are disabled. + buildTimestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; + } + } + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); + switch (mBuildLine[0].toLatin1()) { case 't': //standard frame diff --git a/connections/lawicel_serial.h b/connections/lawicel_serial.h index 3d32a88..cfee96d 100644 --- a/connections/lawicel_serial.h +++ b/connections/lawicel_serial.h @@ -61,6 +61,7 @@ protected: QSerialPort *serial; int framesRapid; CANFrame buildFrame; + qint64 buildTimestamp; bool can0Enabled; bool can0ListenOnly; bool canFd; From b0c1672144037b072d46c54c13b85bbe829c9046 Mon Sep 17 00:00:00 2001 From: Chavdar Kirov Date: Thu, 4 Jul 2024 01:09:39 +0300 Subject: [PATCH 33/57] Support for ext CAN IDs in wireshark files --- framefileio.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/framefileio.cpp b/framefileio.cpp index 5dc82d4..e64a767 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -4882,9 +4882,13 @@ bool FrameFileIO::loadWiresharkFile(QString filename, QVector* frames) thisFrame.isReceived = true; // TODO: check if tx detection is possible thisFrame.setFrameType(QCanBusFrame::DataFrame); - thisFrame.setFrameId((0xff & *(packetData+17)) << 8 | (0xff & *(packetData+16))); - if (thisFrame.frameId() <= 0x7FF) thisFrame.setExtendedFrameFormat(false); - else thisFrame.setExtendedFrameFormat(true); + if ((0x80 & *(packetData+19))) { + thisFrame.setExtendedFrameFormat(true); + thisFrame.setFrameId((0x3f & *(packetData+19))<<24 | (0xff & *(packetData+18)) << 16 | (0xff & *(packetData+17)) << 8 | (0xff & *(packetData+16))); + } else { + thisFrame.setExtendedFrameFormat(false); + thisFrame.setFrameId((0xff & *(packetData+17)) << 8 | (0xff & *(packetData+16))); + } thisFrame.bus = 0; int numBytes = *(packetData+20); QByteArray bytes(numBytes, 0); From ab11691901dd1afab7f30f5c3d1b639a6e81d1a2 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 9 Jul 2024 20:18:24 -0400 Subject: [PATCH 34/57] Fix timestamp in lawicel connection code --- connections/lawicel_serial.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/connections/lawicel_serial.cpp b/connections/lawicel_serial.cpp index 34b4fd4..91e58f5 100644 --- a/connections/lawicel_serial.cpp +++ b/connections/lawicel_serial.cpp @@ -491,7 +491,7 @@ void LAWICELSerial::readSerialData() { qDebug() << "Got CR!"; - buildFrame.setTimeStamp(QDateTime::currentMSecsSinceEpoch() * 1000l); + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000l)); switch (mBuildLine[0].toLatin1()) { case 't': //standard frame From c62d88958cfcdd482a604eed654e1d99d7a4551e Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 9 Jul 2024 20:43:19 -0400 Subject: [PATCH 35/57] Rearrange new graph window so it fits on a 800 pixel tall screen --- ui/newgraphdialog.ui | 435 ++++++++++++++++++++++--------------------- 1 file changed, 223 insertions(+), 212 deletions(-) diff --git a/ui/newgraphdialog.ui b/ui/newgraphdialog.ui index 9b2f036..46d7684 100644 --- a/ui/newgraphdialog.ui +++ b/ui/newgraphdialog.ui @@ -7,7 +7,7 @@ 0 0 681 - 828 + 797 @@ -15,7 +15,7 @@ - + @@ -49,216 +49,27 @@ - 300 - 300 + 280 + 280 - 300 - 300 + 280 + 280 - - - - Data Len: - - - - - - - - - - Little Endian - - - - - - - (LSB First) - - - - - - - Signed: - - - - - - - - - - - - - - QFrame::NoFrame - - - Mask: - - - - - - - - - - Bias: - - - - - - - - - - Scale - - - - - - - - - - Stride - - - - - - - - - - Only Points - - - - - - - - - - - - - - Point Style - - - - - - - - - - Line Thickness - - - - - - - 15 - - - 1 - - - - - - - Line Color - - - - - - - true - - - CHANGE - - - true - - - - - - - Fill Color - - - - - - - true - - - CHANGE - - - true - - - - - - - Add this graph - - - - - - - Qt::Vertical - - - - 20 - 40 - - - - - - - - Associated Bus - - - - - - - -1 - - - + + + + Qt::Vertical + + + @@ -356,7 +167,7 @@ 20 - 40 + 30 @@ -365,6 +176,215 @@ + + + + Qt::Horizontal + + + + + + + + + + + Bit Len: + + + + + + + + + + Little Endian + + + + + + + (LSB First) + + + + + + + Signed: + + + + + + + + + + + + + + QFrame::NoFrame + + + Mask: + + + + + + + + + + Bias: + + + + + + + + + + Scale + + + + + + + + + + Stride + + + + + + + + + + Associated Bus + + + + + + + -1 + + + + + + + + + Qt::Vertical + + + + + + + + + Only Points + + + + + + + + + + + + + + Point Style + + + + + + + + + + Line Thickness + + + + + + + 15 + + + 1 + + + + + + + Line Color + + + + + + + true + + + CHANGE + + + true + + + + + + + Fill Color + + + + + + + true + + + CHANGE + + + true + + + + + + + + + + + Add this graph + + + @@ -378,15 +398,6 @@ txtName txtID - txtDataLen - cbIntel - cbSigned - txtMask - txtBias - txtScale - txtStride - colorSwatch - btnAddGraph cbMessages cbSignals btnCopySignal From a3cdbaf685efde66c7c4c84c1840c0eb555d438f Mon Sep 17 00:00:00 2001 From: Joris Pevcevicius Date: Tue, 23 Jul 2024 23:41:51 +0300 Subject: [PATCH 36/57] Fix device deselection after connecting --- connections/connectionwindow.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/connections/connectionwindow.cpp b/connections/connectionwindow.cpp index 5e4df7a..1a99241 100644 --- a/connections/connectionwindow.cpp +++ b/connections/connectionwindow.cpp @@ -336,7 +336,9 @@ void ConnectionWindow::connectionStatus(CANConStatus pStatus) Q_UNUSED(pStatus); qDebug() << "Connectionstatus changed"; + int selIdx = ui->tableConnections->selectionModel()->currentIndex().row(); connModel->refresh(); + ui->tableConnections->selectRow(selIdx); } void ConnectionWindow::setSuspendAll(bool pSuspend) From f2bd1d9d6b97979f73ea3efc03b3d3cbeabf5c71 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 13 Aug 2024 20:59:52 -0400 Subject: [PATCH 37/57] Add a save function to ISO-TP decoder window --- bus_protocols/uds_handler.cpp | 33 ++++++++++++--- bus_protocols/uds_handler.h | 1 + re/isotp_interpreterwindow.cpp | 75 ++++++++++++++++++++++++++++++++++ re/isotp_interpreterwindow.h | 1 + ui/isotp_interpreterwindow.ui | 7 ++++ 5 files changed, 111 insertions(+), 6 deletions(-) diff --git a/bus_protocols/uds_handler.cpp b/bus_protocols/uds_handler.cpp index 697f758..f39b5bb 100644 --- a/bus_protocols/uds_handler.cpp +++ b/bus_protocols/uds_handler.cpp @@ -182,11 +182,11 @@ UDS_HANDLER::~UDS_HANDLER() delete isoHandler; } -void UDS_HANDLER::gotISOTPFrame(ISOTP_MESSAGE msg) +UDS_MESSAGE UDS_HANDLER::tryISOtoUDS(ISOTP_MESSAGE msg, bool *result) { - qDebug() << "UDS handler got ISOTP frame"; const unsigned char *data = reinterpret_cast(msg.payload().constData()); int dataLen = msg.payload().count(); + *result = true; UDS_MESSAGE udsMsg; udsMsg.bus = msg.bus; udsMsg.setExtendedFrameFormat(msg.hasExtendedFrameFormat()); @@ -208,9 +208,17 @@ void UDS_HANDLER::gotISOTPFrame(ISOTP_MESSAGE msg) { udsMsg.service = data[1]; if (dataLen > 2) udsMsg.subFunc = data[2]; - else return; + else + { + *result = false; + return udsMsg; + }; } - else return; + else + { + *result = false; + return udsMsg; + }; udsMsg.payload().remove(0, 2); } else @@ -220,9 +228,22 @@ void UDS_HANDLER::gotISOTPFrame(ISOTP_MESSAGE msg) udsMsg.payload().remove(0, 1); } } - else return; + else + { + *result = false; + }; + return udsMsg; +} - emit newUDSMessage(udsMsg); +void UDS_HANDLER::gotISOTPFrame(ISOTP_MESSAGE msg) +{ + qDebug() << "UDS handler got ISOTP frame"; + UDS_MESSAGE udsMsg; + + bool result; + udsMsg = tryISOtoUDS(msg, &result); + + if (result) emit newUDSMessage(udsMsg); } void UDS_HANDLER::setFlowCtrl(bool state) diff --git a/bus_protocols/uds_handler.h b/bus_protocols/uds_handler.h index 5c5ea4f..b1578ed 100644 --- a/bus_protocols/uds_handler.h +++ b/bus_protocols/uds_handler.h @@ -105,6 +105,7 @@ public: QString getShortDesc(QVector &codeVector, int code); QString getLongDesc(QVector &codeVector, int code); QString getDetailedMessageAnalysis(const UDS_MESSAGE &msg); + UDS_MESSAGE tryISOtoUDS(ISOTP_MESSAGE msg, bool *result); public slots: void gotISOTPFrame(ISOTP_MESSAGE msg); diff --git a/re/isotp_interpreterwindow.cpp b/re/isotp_interpreterwindow.cpp index 5f8bb10..045d608 100644 --- a/re/isotp_interpreterwindow.cpp +++ b/re/isotp_interpreterwindow.cpp @@ -31,6 +31,7 @@ ISOTP_InterpreterWindow::ISOTP_InterpreterWindow(const QVector *frames connect(ui->tableIsoFrames, &QTableWidget::itemSelectionChanged, this, &ISOTP_InterpreterWindow::showDetailView); connect(ui->btnClearList, &QPushButton::clicked, this, &ISOTP_InterpreterWindow::clearList); + connect(ui->btnSaveList, &QPushButton::clicked, this, &ISOTP_InterpreterWindow::saveList); connect(ui->cbUseExtendedAddressing, SIGNAL(toggled(bool)), this, SLOT(useExtendedAddressing(bool))); QStringList headers; @@ -172,6 +173,80 @@ void ISOTP_InterpreterWindow::clearList() //idFilters.clear(); } +/* + * A bit complicated as the list doesn't have the detailed analysis in it. Have to take each list entry + * and process it to get the details then save those details to the file as well. + */ +void ISOTP_InterpreterWindow::saveList() +{ + QString buildString; + QString filename; + QFileDialog dialog(this); + QSettings settings; + + QStringList filters; + filters.append(QString(tr("Text File (*.txt)"))); + + dialog.setFileMode(QFileDialog::AnyFile); + dialog.setNameFilters(filters); + dialog.setViewMode(QFileDialog::Detail); + dialog.setAcceptMode(QFileDialog::AcceptSave); + dialog.setDirectory(settings.value("FrameInfo/LoadSaveDirectory", dialog.directory().path()).toString()); + + if (dialog.exec() == QDialog::Accepted) + { + settings.setValue("FrameInfo/LoadSaveDirectory", dialog.directory().path()); + filename = dialog.selectedFiles()[0]; + if (!filename.contains('.')) filename += ".txt"; + if (dialog.selectedNameFilter() == filters[0]) + { + QFile *outFile = new QFile(filename); + + if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text)) + { + delete outFile; + return; + } + + int rows = messages.count(); + for (int r = 0 ; r < rows; r++) + { + ISOTP_MESSAGE msg = messages.at(r); + const unsigned char *data = reinterpret_cast(msg.payload().constData()); + int dataLen = msg.payload().length(); + + if (msg.reportedLength != dataLen) + { + continue; + } + + buildString.append(QString::number(msg.timeStamp().microSeconds()) + " " + QString::number(msg.frameId(), 16) + " "); + + //buildString.append(tr("Raw Payload: ")); + + for (int i = 0; i < dataLen; i++) + { + buildString.append(Utility::formatNumber(data[i])); + buildString.append(" "); + } + buildString.append("\n\n"); + + UDS_MESSAGE udsMsg; + bool result; + udsMsg = udsDecoder->tryISOtoUDS(msg, &result); + if (result) + buildString.append(udsDecoder->getDetailedMessageAnalysis(udsMsg)); + buildString.append("\n*********************************************************\n"); + outFile->write(buildString.toUtf8()); + } + + outFile->close(); + delete outFile; + } + } +} + + void ISOTP_InterpreterWindow::useExtendedAddressing(bool checked) { decoder->setExtendedAddressing(checked); diff --git a/re/isotp_interpreterwindow.h b/re/isotp_interpreterwindow.h index 14167de..3b3a6b2 100644 --- a/re/isotp_interpreterwindow.h +++ b/re/isotp_interpreterwindow.h @@ -26,6 +26,7 @@ private slots: void showDetailView(); void updatedFrames(int); void clearList(); + void saveList(); void listFilterItemChanged(QListWidgetItem *item); void filterAll(); void filterNone(); diff --git a/ui/isotp_interpreterwindow.ui b/ui/isotp_interpreterwindow.ui index ec1ed3b..6f7f67d 100644 --- a/ui/isotp_interpreterwindow.ui +++ b/ui/isotp_interpreterwindow.ui @@ -40,6 +40,13 @@ + + + + Save List + + + From c5a9c719cc9699c080d718eefa089b437a936c13 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Tue, 20 Aug 2024 21:42:34 -0400 Subject: [PATCH 38/57] Add an application icon to Windows build --- SavvyCAN.pro | 4 ++++ icons/SavvyIcon.ico | Bin 0 -> 59329 bytes 2 files changed, 4 insertions(+) create mode 100644 icons/SavvyIcon.ico diff --git a/SavvyCAN.pro b/SavvyCAN.pro index ceb0aae..ef56a83 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -261,6 +261,10 @@ unix { DISTFILES += SavvyCAN.desktop } +windows { +RC_ICONS=icons/SavvyIcon.ico +} + examplefiles.files=examples examplefiles.path = $$PREFIX/share/savvycan/examples INSTALLS += examplefiles diff --git a/icons/SavvyIcon.ico b/icons/SavvyIcon.ico new file mode 100644 index 0000000000000000000000000000000000000000..c47683cdf7a5cbfb2920912c6b0b3dbf9d1bbead GIT binary patch literal 59329 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

!9_c?&`Y{(9-W3M}a(hn*{L4V~d8L!Bm1o;Z!Q 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

4-F2OfKbSjAkvcb7x{d1I41BU4Br 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 literal 0 HcmV?d00001 From cae66ce3f250f65a2d808621d513ca8c82824c5b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:16:44 +0000 Subject: [PATCH 39/57] Bump actions/download-artifact from 3 to 4.1.7 in /.github/workflows Bumps [actions/download-artifact](https://github.com/actions/download-artifact) from 3 to 4.1.7. - [Release notes](https://github.com/actions/download-artifact/releases) - [Commits](https://github.com/actions/download-artifact/compare/v3...v4.1.7) --- updated-dependencies: - dependency-name: actions/download-artifact dependency-type: direct:production ... Signed-off-by: dependabot[bot] --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 1b16305..0009e87 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -126,7 +126,7 @@ jobs: steps: - uses: actions/checkout@v3 - - uses: actions/download-artifact@v3 + - uses: actions/download-artifact@v4.1.7 - name: Display structure of downloaded files run: zip -r SavvyCAN-Windows_x64_CIBuild.zip SavvyCAN-Windows_x64 From 5c7c439841efd3f44ea9695078da836c824e8cd3 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Wed, 4 Sep 2024 20:24:36 -0400 Subject: [PATCH 40/57] Update build.yml Need to also update actions/upload-artifact or download-artifact v4 won't work. --- .github/workflows/build.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0009e87..a2c7e6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,7 +33,7 @@ jobs: chmod a+x linuxdeployqt-continuous-x86_64.AppImage ./linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/SavvyCAN.desktop -appimage -extra-plugins=iconengines,platformthemes/libqgtk3.so,canbus - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: SavvyCAN-Linux_x64.AppImage path: SavvyCAN-*x86_64.AppImage @@ -62,7 +62,7 @@ jobs: cp -R help/* SavvyCAN.app/Contents/MacOS/help macdeployqt SavvyCAN.app -dmg - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: SavvyCAN-macOS_x64.dmg path: SavvyCAN.dmg @@ -113,7 +113,7 @@ jobs: mkdir package/canbus copy "${Env:Qt5_Dir}/plugins/canbus/*.*" package/canbus/ - - uses: actions/upload-artifact@v3 + - uses: actions/upload-artifact@v4 with: name: SavvyCAN-Windows_x64 path: package From 6fb5299c6ca1552b8ee08b3286781e379aec035c Mon Sep 17 00:00:00 2001 From: Superwofy Date: Fri, 25 Oct 2024 21:51:32 +0100 Subject: [PATCH 41/57] Fix dbc handling of comments and attributes --- dbc/dbchandler.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dbc/dbchandler.cpp b/dbc/dbchandler.cpp index f55118b..b684582 100644 --- a/dbc/dbchandler.cpp +++ b/dbc/dbchandler.cpp @@ -775,7 +775,7 @@ bool DBCFile::parseAttributeLine(QString line) if (foundAttr) { qDebug() << "That message attribute does exist"; - DBC_MESSAGE *foundMsg = messageHandler->findMsgByID(match.captured(2).toUInt() & 0x1FFFFFFFul); + DBC_MESSAGE *foundMsg = messageHandler->findMsgByID(match.captured(2).toULong() & 0x1FFFFFFFul); if (foundMsg) { qDebug() << "It references a valid, registered message"; @@ -807,7 +807,7 @@ bool DBCFile::parseAttributeLine(QString line) if (foundAttr) { qDebug() << "That signal attribute does exist"; - DBC_MESSAGE *foundMsg = messageHandler->findMsgByID(match.captured(2).toUInt() & 0x1FFFFFFFUL); + DBC_MESSAGE *foundMsg = messageHandler->findMsgByID(match.captured(2).toULong() & 0x1FFFFFFFUL); if (foundMsg) { qDebug() << "It references a valid, registered message"; @@ -1019,7 +1019,7 @@ bool DBCFile::loadFile(QString fileName) if (match.hasMatch()) { //qDebug() << "Comment was: " << match.captured(3); - DBC_MESSAGE *msg = messageHandler->findMsgByID(match.captured(1).toUInt()); + DBC_MESSAGE *msg = messageHandler->findMsgByID(match.captured(1).toULong() & 0x1FFFFFFFul); if (msg != nullptr) { DBC_SIGNAL *sig = msg->sigHandler->findSignalByName(match.captured(2)); @@ -1040,7 +1040,7 @@ bool DBCFile::loadFile(QString fileName) if (match.hasMatch()) { //qDebug() << "Comment was: " << match.captured(2); - DBC_MESSAGE *msg = messageHandler->findMsgByID(match.captured(1).toUInt()); + DBC_MESSAGE *msg = messageHandler->findMsgByID(match.captured(1).toULong() & 0x1FFFFFFFul); if (msg != nullptr) { msg->comment = match.captured(2); From 708940110e071056d0a50388e23055b57f2b72dd Mon Sep 17 00:00:00 2001 From: Antonio Date: Sun, 27 Oct 2024 18:43:06 +0100 Subject: [PATCH 42/57] Fix icons output dir --- SavvyCAN.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/SavvyCAN.pro b/SavvyCAN.pro index ef56a83..d608800 100644 --- a/SavvyCAN.pro +++ b/SavvyCAN.pro @@ -270,7 +270,7 @@ examplefiles.path = $$PREFIX/share/savvycan/examples INSTALLS += examplefiles iconfiles.files=icons -iconfiles.path = $$PREFIX/share/icons +iconfiles.path = $$PREFIX/share INSTALLS += iconfiles helpfiles.files=help/* From f4a51cd73c2084d54c5522cdea0cbffbfefd9015 Mon Sep 17 00:00:00 2001 From: mvgalen Date: Fri, 29 Nov 2024 17:55:47 +0100 Subject: [PATCH 43/57] Support for > 8 bytes per message --- framefileio.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/framefileio.cpp b/framefileio.cpp index 79b5527..38ed8c3 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -3784,12 +3784,17 @@ bool FrameFileIO::loadCanDumpFile(QString filename, QVector* frames) if (line.contains('[')) //the expanded format (second one from the above list) { //(1551774790.942758) can1 7A8 [8] F4 DC D1 83 0E 02 00 00 + //(1551774790.942758) can1 7A8 [08] F4 DC D1 83 0E 02 00 00 // 0 1 2 3 4 5 6 7 8 9 10 11 thisFrame.setFrameId(tokens[2].toLong(nullptr, 16)); if (thisFrame.frameId() > 0x7FF) thisFrame.setExtendedFrameFormat(true); else thisFrame.setExtendedFrameFormat(false); thisFrame.setFrameType(QCanBusFrame::DataFrame); - int numBytes = tokens[3].at(1) - '0'; + int numBytes; + if (tokens[3].at(2) == ']') + numBytes = tokens[3].at(1) - '0'; + else + numBytes = (tokens[3].at(1) - '0')*10 + (tokens[3].at(2) - '0'); QByteArray bytes(numBytes, 0); for (int c = 0; c < numBytes; c++) { From dc4b2c4c3a18d069a455755567327148a882c21f Mon Sep 17 00:00:00 2001 From: mvgalen Date: Fri, 29 Nov 2024 19:36:31 +0100 Subject: [PATCH 44/57] add manual trigger --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2c7e6f..5e33c73 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,7 @@ on: push: branches: - "master" - + workflow_dispatch: jobs: buildlinux: name: Linux x64 From 56fcb23a5e5f17e50b794ab7e43f31104c70e29a Mon Sep 17 00:00:00 2001 From: Florian Date: Mon, 3 Feb 2025 00:33:11 +0100 Subject: [PATCH 45/57] FuzzingWindow: Add buttons to set all high/low/auto --- re/fuzzingwindow.cpp | 30 ++++++++++++++++++++++++++++++ re/fuzzingwindow.h | 3 +++ ui/fuzzingwindow.ui | 24 ++++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/re/fuzzingwindow.cpp b/re/fuzzingwindow.cpp index d7c84a4..9a75c3e 100644 --- a/re/fuzzingwindow.cpp +++ b/re/fuzzingwindow.cpp @@ -19,6 +19,9 @@ FuzzingWindow::FuzzingWindow(const QVector *frames, QWidget *parent) : fuzzTimer = new QTimer(); + connect(ui->btnMarkAllHigh, &QPushButton::clicked, this, &FuzzingWindow::markAllHigh); + connect(ui->btnMarkAllLow, &QPushButton::clicked, this, &FuzzingWindow::markAllLow); + connect(ui->btnMarkAllAuto, &QPushButton::clicked, this, &FuzzingWindow::markAllAuto); connect(ui->btnStartStop, &QPushButton::clicked, this, &FuzzingWindow::toggleFuzzing); connect(ui->btnAllFilters, &QPushButton::clicked, this, &FuzzingWindow::setAllFilters); connect(ui->btnNoFilters, &QPushButton::clicked, this, &FuzzingWindow::clearAllFilters); @@ -508,3 +511,30 @@ void FuzzingWindow::redrawGrid() ui->bitfield->setReference(refBytes, false); ui->bitfield->updateData(dataBytes, true); } + +void FuzzingWindow::markAllHigh() +{ + for (int i = 0; i < 512; i++) + { + bitGrid[i] = 2; + } + redrawGrid(); +} + +void FuzzingWindow::markAllLow() +{ + for (int i = 0; i < 512; i++) + { + bitGrid[i] = 0; + } + redrawGrid(); +} + +void FuzzingWindow::markAllAuto() +{ + for (int i = 0; i < 512; i++) + { + bitGrid[i] = 1; + } + redrawGrid(); +} diff --git a/re/fuzzingwindow.h b/re/fuzzingwindow.h index 6b52da4..58cf378 100644 --- a/re/fuzzingwindow.h +++ b/re/fuzzingwindow.h @@ -42,6 +42,9 @@ private slots: void bitfieldClicked(int); void changedNumDataBytes(int newVal); void updatedFrames(int numFrames); + void markAllHigh(); + void markAllLow(); + void markAllAuto(); private: Ui::FuzzingWindow *ui; diff --git a/ui/fuzzingwindow.ui b/ui/fuzzingwindow.ui index 1600b97..813e9a4 100644 --- a/ui/fuzzingwindow.ui +++ b/ui/fuzzingwindow.ui @@ -487,6 +487,27 @@ + + + + All High + + + + + + + All Low + + + + + + + All Auto + + + @@ -534,6 +555,9 @@ listID btnAllFilters btnNoFilters + btnMarkAllHigh + btnMarkAllLow + btnMarkAllAuto btnStartStop From 60b0e65a18698622d79d0e25440a00287b17eb8b Mon Sep 17 00:00:00 2001 From: werner Date: Thu, 13 Feb 2025 20:21:15 +0100 Subject: [PATCH 46/57] Implemented connection save with speeds and can fd at least for bus 0. --- connections/canconfactory.cpp | 2 +- connections/canconnection.cpp | 13 +++++++++++-- connections/canconnection.h | 6 ++++++ connections/connectionwindow.cpp | 27 ++++++++++++++++++++++----- connections/serialbusconnection.cpp | 4 ++-- connections/serialbusconnection.h | 3 ++- 6 files changed, 44 insertions(+), 11 deletions(-) diff --git a/connections/canconfactory.cpp b/connections/canconfactory.cpp index ae2bc6b..774c92c 100644 --- a/connections/canconfactory.cpp +++ b/connections/canconfactory.cpp @@ -14,7 +14,7 @@ CANConnection* CanConFactory::create(type pType, QString pPortName, QString pDri { switch(pType) { case SERIALBUS: - return new SerialBusConnection(pPortName, pDriverName); + return new SerialBusConnection(pPortName, pDriverName, pBusSpeed, pDataRate, pCanFd); case GVRET_SERIAL: if(pPortName.contains(".") && !pPortName.contains("tty") && !pPortName.contains("serial")) return new GVRetSerial(pPortName, true); diff --git a/connections/canconnection.cpp b/connections/canconnection.cpp index 9eac817..e6f0228 100644 --- a/connections/canconnection.cpp +++ b/connections/canconnection.cpp @@ -41,8 +41,13 @@ CANConnection::CANConnection(QString pPort, if (pBusSpeed > 0) mBusData[0].mBus.setSpeed(pBusSpeed); mBusData[0].mBus.setCanFD(pCanFd); - if (pDataRate > 0) mBusData[0].mBus.setDataRate(pDataRate); - + if (pDataRate > 0) { + mBusData[0].mBus.setDataRate(pDataRate); + if (pCanFd) { + mBusData[0].mBus.setCanFD(pCanFd); + } + } + /* if needed, create a thread and move ourself into it */ if(pUseThread) { mThread_p = new QThread(); @@ -210,6 +215,10 @@ int CANConnection::getNumBuses() const{ return mNumBuses; } +int CANConnection::getSerialSpeed() const{ + return mSerialSpeed; +} + bool CANConnection::isConfigured(int pBusId) { if( pBusId < 0 || pBusId >= getNumBuses()) diff --git a/connections/canconnection.h b/connections/canconnection.h index fde8296..b92b229 100644 --- a/connections/canconnection.h +++ b/connections/canconnection.h @@ -52,6 +52,12 @@ public: */ int getNumBuses() const; + /** + * @brief getserialSpeed + * @return returns the serial speed of the device + */ + int getSerialSpeed() const; + /** * @brief getPort * @return returns the port name of the device diff --git a/connections/connectionwindow.cpp b/connections/connectionwindow.cpp index 5e4df7a..a8595bd 100644 --- a/connections/connectionwindow.cpp +++ b/connections/connectionwindow.cpp @@ -551,13 +551,17 @@ void ConnectionWindow::loadConnections() QVector driverNames = settings.value("connections/driverNames").value>(); QVector devTypes = settings.value("connections/types").value>(); + QVector busSpeeds = settings.value("connections/busSpeeds_0").value>(); + QVector DataRates = settings.value("connections/DataRates_0").value>(); + QVector isCanFds = settings.value("connections/isCanFds_0").value>(); + QVector serialSpeeds = settings.value("connections/serialSpeeds").value>(); //don't load the connections if the three setting arrays above aren't all the same size. - if (portNames.count() != driverNames.count() || devTypes.count() != driverNames.count()) return; + if (portNames.count() != driverNames.count() || devTypes.count() != driverNames.count() || busSpeeds.count() != driverNames.count() || isCanFds.count() != driverNames.count() || + DataRates.count() != driverNames.count() || serialSpeeds.count() != driverNames.count() ) return; for(int i = 0 ; i < portNames.count() ; i++) { - //TODO: add serial speed and bus speed to this properly. - CANConnection* conn_p = create((CANCon::type)devTypes[i], portNames[i], driverNames[i], 0, 0, false, 0); + CANConnection* conn_p = create((CANCon::type)devTypes[i], portNames[i], driverNames[i], serialSpeeds[i], busSpeeds[i], isCanFds[i] ? true : false, DataRates[i]); /* add connection to model */ connModel->add(conn_p); } @@ -577,10 +581,19 @@ void ConnectionWindow::saveConnections() QVector driverNames; QVector serialSpeeds; QVector busSpeeds; - + QVector DataRates; + QVector CanFds; + /* save connections */ foreach(CANConnection* conn_p, conns) - { + { CANBus bus; + + if (conn_p->getBusSettings(0, bus)) { + busSpeeds.append(bus.getSpeed()); + CanFds.append(bus.isCanFD() ? 1 : 0); + DataRates.append(bus.getDataRate()); + } + serialSpeeds.append(conn_p->getSerialSpeed()); portNames.append(conn_p->getPort()); devTypes.append(conn_p->getType()); driverNames.append(conn_p->getDriver()); @@ -589,6 +602,10 @@ void ConnectionWindow::saveConnections() settings.setValue("connections/portNames", QVariant::fromValue(portNames)); settings.setValue("connections/types", QVariant::fromValue(devTypes)); settings.setValue("connections/driverNames", QVariant::fromValue(driverNames)); + settings.setValue("connections/busSpeeds_0", QVariant::fromValue(busSpeeds)); + settings.setValue("connections/isCanFds_0", QVariant::fromValue(CanFds)); + settings.setValue("connections/DataRates_0", QVariant::fromValue(DataRates)); + settings.setValue("connections/serialSpeeds", QVariant::fromValue(serialSpeeds)); } void ConnectionWindow::moveConnUp() diff --git a/connections/serialbusconnection.cpp b/connections/serialbusconnection.cpp index 8ebebbf..8938194 100644 --- a/connections/serialbusconnection.cpp +++ b/connections/serialbusconnection.cpp @@ -11,8 +11,8 @@ /**** class definition ****/ /***********************************/ -SerialBusConnection::SerialBusConnection(QString portName, QString driverName) : - CANConnection(portName, driverName, CANCon::SERIALBUS,0 ,0, false, 0 ,1, 4000, true), +SerialBusConnection::SerialBusConnection(QString portName, QString driverName, int pBusSpeed, int pDataRate, bool pCanFd) : + CANConnection(portName, driverName, CANCon::SERIALBUS,0 ,pBusSpeed, pCanFd, pDataRate ,1, 4000, true), mTimer(this) /*NB: set connection as parent of timer to manage it from working thread */ { } diff --git a/connections/serialbusconnection.h b/connections/serialbusconnection.h index f65be43..10c1331 100644 --- a/connections/serialbusconnection.h +++ b/connections/serialbusconnection.h @@ -27,7 +27,8 @@ class SerialBusConnection : public CANConnection Q_OBJECT public: - SerialBusConnection(QString portName, QString driverName); + SerialBusConnection(QString portName, QString driverName, int pBusSpeed, + int pDataRate, bool pCanFd); virtual ~SerialBusConnection(); protected: From b0fba1b6dec5a76264484c41fec73fc7b865f56c Mon Sep 17 00:00:00 2001 From: alessandro negrin Date: Wed, 26 Mar 2025 19:09:16 +0100 Subject: [PATCH 47/57] setting proper system time --- .github/workflows/build.yml | 97 ++--------------------------------- connections/canconmanager.cpp | 3 +- 2 files changed, 7 insertions(+), 93 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2c7e6f..4a48d2d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,72 +1,13 @@ name: Build on: + pull_request: + types: [opened, synchronize, reopened] push: branches: - "master" -jobs: - buildlinux: - name: Linux x64 - runs-on: ubuntu-20.04 - - steps: - - name: Prepare Environment - run: | - sudo apt-get install libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-randr0 libxkbcommon-x11-0 - - - name: Prepare Qt Libraries - uses: jurplel/install-qt-action@v3 - - - name: Clone - uses: actions/checkout@v3 - - - name: Compile - run: | - qmake CONFIG+=release PREFIX=/usr SavvyCAN.pro - make -j`grep -c ^processor /proc/cpuinfo` - - - name: Package - run: | - make INSTALL_ROOT=appdir install - wget -c -nv "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" - chmod a+x linuxdeployqt-continuous-x86_64.AppImage - ./linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/SavvyCAN.desktop -appimage -extra-plugins=iconengines,platformthemes/libqgtk3.so,canbus - - - uses: actions/upload-artifact@v4 - with: - name: SavvyCAN-Linux_x64.AppImage - path: SavvyCAN-*x86_64.AppImage - - buildmacos: - name: macOS x64 - runs-on: macos-12 - - steps: - - name: Prepare macOS Environment - run: | - brew install qt5 - brew link qt5 --force - - - name: Clone - uses: actions/checkout@v3 - - - name: Compile - run: | - qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro - make -j`sysctl kern.aioprocmax | awk '{print $2}'` - - - name: Package - run: | - mkdir -p SavvyCAN.app/Contents/MacOS/help - cp -R help/* SavvyCAN.app/Contents/MacOS/help - macdeployqt SavvyCAN.app -dmg - - - uses: actions/upload-artifact@v4 - with: - name: SavvyCAN-macOS_x64.dmg - path: SavvyCAN.dmg - +jobs: buildwindows: name: Windows x64 runs-on: windows-2019 @@ -121,7 +62,7 @@ jobs: pre-release: name: "pre-release" runs-on: "ubuntu-latest" - needs: [buildwindows, buildmacos, buildlinux] + needs: [buildwindows] steps: - uses: actions/checkout@v3 @@ -137,33 +78,5 @@ jobs: prerelease: true title: "Development Build" files: | - SavvyCAN-Linux_x64.AppImage/SavvyCAN-*x86_64.AppImage SavvyCAN-Windows_x64_CIBuild.zip - SavvyCAN-macOS_x64.dmg/SavvyCAN.dmg - - notify: - name: Notify Discord - runs-on: ubuntu-latest - needs: [buildwindows, buildmacos, buildlinux] - steps: - - id: msg_var - name: remove newlines - run: | - export release_msg=$(cat << EOF - ${{ github.event.head_commit.message }} - EOF - ) - release_msg="${release_msg//$'\n'/'-'}" - release_msg="${release_msg//$'\r'/'-'}" - release_msg="${release_msg//$'('/'-'}" - release_msg="${release_msg//$')'/'-'}" - echo $release_msg - echo "::set-output name=msg::$release_msg" - - - name: Notify Discord - run: | - curl -i -H "Accept: application/json" \ - -H "Content-Type:application/json" \ - -X POST \ - --data "{\"content\": null,\"embeds\": [{\"title\": \"${{ github.repository }} Build Complete\", \"description\": \"${{ steps.msg_var.outputs.msg }} \\nCommit: ${{ github.sha }}\\n[Build Artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)\", \"color\": 5131997, \"author\": { \"name\": \"Github Builds\" } } ]}" \ - https://discord.com/api/webhooks/${{ secrets.WEBHOOK_ID }}/${{ secrets.WEBHOOK_TOKEN }} + diff --git a/connections/canconmanager.cpp b/connections/canconmanager.cpp index 9496b9e..d0f2053 100644 --- a/connections/canconmanager.cpp +++ b/connections/canconmanager.cpp @@ -222,7 +222,8 @@ bool CANConManager::sendFrame(const CANFrame& pFrame) workingFrame.isReceived = false; if (useSystemTime) { - workingFrame.setTimeStamp(QCanBusFrame::TimeStamp(0,QDateTime::currentMSecsSinceEpoch() * 1000)); + qint64 currentTimeMs = QDateTime::currentMSecsSinceEpoch(); + workingFrame.setTimeStamp(QCanBusFrame::TimeStamp(currentTimeMs / 1000, (currentTimeMs % 1000) * 1000)); } else { From 49a5b761bc7b94c976d5255570ef8ede588dca33 Mon Sep 17 00:00:00 2001 From: alessandro negrin Date: Wed, 26 Mar 2025 19:28:26 +0100 Subject: [PATCH 48/57] some other timestamps --- connections/gvretserial.cpp | 6 ++++-- connections/lawicel_serial.cpp | 5 +++-- connections/mqtt_bus.cpp | 3 ++- connections/serialbusconnection.cpp | 3 ++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/connections/gvretserial.cpp b/connections/gvretserial.cpp index dc63ae0..77eeaba 100644 --- a/connections/gvretserial.cpp +++ b/connections/gvretserial.cpp @@ -685,9 +685,11 @@ void GVRetSerial::procRXChar(unsigned char c) buildTimestamp += timeBasis; if (useSystemTime) { - buildTimestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; + buildTimestamp = QDateTime::currentMSecsSinceEpoch(); + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(buildTimestamp / 1000, (buildTimestamp % 1000) * 1000)); + } else { + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); } - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); break; case 4: buildId = c; diff --git a/connections/lawicel_serial.cpp b/connections/lawicel_serial.cpp index ddf9e4d..fd409bc 100644 --- a/connections/lawicel_serial.cpp +++ b/connections/lawicel_serial.cpp @@ -493,7 +493,8 @@ void LAWICELSerial::readSerialData() if (useSystemTime) { - buildTimestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; + buildTimestamp = QDateTime::currentMSecsSinceEpoch(); + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(buildTimestamp / 1000, (buildTimestamp % 1000) * 1000)); } else { @@ -508,8 +509,8 @@ void LAWICELSerial::readSerialData() //Default to system time if timestamps are disabled. buildTimestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; } + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); } - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); switch (mBuildLine[0].toLatin1()) { diff --git a/connections/mqtt_bus.cpp b/connections/mqtt_bus.cpp index 4220e34..a6cc0d2 100644 --- a/connections/mqtt_bus.cpp +++ b/connections/mqtt_bus.cpp @@ -302,7 +302,8 @@ void MQTT_BUS::clientMessageReceived(const QMQTT::Message& message) frame_p->isReceived = true; if (useSystemTime) { - frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000ul)); + qint64 currentTimeMs = QDateTime::currentMSecsSinceEpoch(); + frame_p->setTimeStamp(QCanBusFrame::TimeStamp(currentTimeMs / 1000, (currentTimeMs % 1000) * 1000)); } else frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); diff --git a/connections/serialbusconnection.cpp b/connections/serialbusconnection.cpp index 8ebebbf..b54aa69 100644 --- a/connections/serialbusconnection.cpp +++ b/connections/serialbusconnection.cpp @@ -211,7 +211,8 @@ void SerialBusConnection::framesReceived() frame_p->isReceived = !recFrame.hasLocalEcho(); if (useSystemTime) { - frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000ul)); + qint64 currentTimeMs = QDateTime::currentMSecsSinceEpoch(); + frame_p->setTimeStamp(QCanBusFrame::TimeStamp(currentTimeMs / 1000, (currentTimeMs % 1000) * 1000)); } else frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, (recFrame.timeStamp().seconds() * 1000000ul + recFrame.timeStamp().microSeconds()) - timeBasis)); From 2f70a608e334a27d0577e20a247b7ccc8579966e Mon Sep 17 00:00:00 2001 From: alessandro negrin Date: Wed, 26 Mar 2025 19:39:53 +0100 Subject: [PATCH 49/57] this was missing --- connections/canconmanager.cpp | 3 +-- connections/gvretserial.cpp | 8 ++++---- connections/lawicel_serial.cpp | 3 +-- connections/mqtt_bus.cpp | 3 +-- connections/serialbusconnection.cpp | 3 +-- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/connections/canconmanager.cpp b/connections/canconmanager.cpp index d0f2053..f31e70a 100644 --- a/connections/canconmanager.cpp +++ b/connections/canconmanager.cpp @@ -222,8 +222,7 @@ bool CANConManager::sendFrame(const CANFrame& pFrame) workingFrame.isReceived = false; if (useSystemTime) { - qint64 currentTimeMs = QDateTime::currentMSecsSinceEpoch(); - workingFrame.setTimeStamp(QCanBusFrame::TimeStamp(currentTimeMs / 1000, (currentTimeMs % 1000) * 1000)); + workingFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); } else { diff --git a/connections/gvretserial.cpp b/connections/gvretserial.cpp index 77eeaba..1b3dbc7 100644 --- a/connections/gvretserial.cpp +++ b/connections/gvretserial.cpp @@ -685,8 +685,7 @@ void GVRetSerial::procRXChar(unsigned char c) buildTimestamp += timeBasis; if (useSystemTime) { - buildTimestamp = QDateTime::currentMSecsSinceEpoch(); - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(buildTimestamp / 1000, (buildTimestamp % 1000) * 1000)); + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); } else { buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); } @@ -772,9 +771,10 @@ void GVRetSerial::procRXChar(unsigned char c) buildTimestamp += timeBasis; if (useSystemTime) { - buildTimestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); + } else { + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); } - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); break; case 4: buildId = c; diff --git a/connections/lawicel_serial.cpp b/connections/lawicel_serial.cpp index fd409bc..37a078d 100644 --- a/connections/lawicel_serial.cpp +++ b/connections/lawicel_serial.cpp @@ -493,8 +493,7 @@ void LAWICELSerial::readSerialData() if (useSystemTime) { - buildTimestamp = QDateTime::currentMSecsSinceEpoch(); - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(buildTimestamp / 1000, (buildTimestamp % 1000) * 1000)); + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); } else { diff --git a/connections/mqtt_bus.cpp b/connections/mqtt_bus.cpp index a6cc0d2..86da138 100644 --- a/connections/mqtt_bus.cpp +++ b/connections/mqtt_bus.cpp @@ -302,8 +302,7 @@ void MQTT_BUS::clientMessageReceived(const QMQTT::Message& message) frame_p->isReceived = true; if (useSystemTime) { - qint64 currentTimeMs = QDateTime::currentMSecsSinceEpoch(); - frame_p->setTimeStamp(QCanBusFrame::TimeStamp(currentTimeMs / 1000, (currentTimeMs % 1000) * 1000)); + frame_p->setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); } else frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); diff --git a/connections/serialbusconnection.cpp b/connections/serialbusconnection.cpp index b54aa69..ad21817 100644 --- a/connections/serialbusconnection.cpp +++ b/connections/serialbusconnection.cpp @@ -211,8 +211,7 @@ void SerialBusConnection::framesReceived() frame_p->isReceived = !recFrame.hasLocalEcho(); if (useSystemTime) { - qint64 currentTimeMs = QDateTime::currentMSecsSinceEpoch(); - frame_p->setTimeStamp(QCanBusFrame::TimeStamp(currentTimeMs / 1000, (currentTimeMs % 1000) * 1000)); + frame_p->setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); } else frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, (recFrame.timeStamp().seconds() * 1000000ul + recFrame.timeStamp().microSeconds()) - timeBasis)); From 4905794926bd7017864139ef558ababa97de0e5e Mon Sep 17 00:00:00 2001 From: alessandro negrin Date: Sat, 29 Mar 2025 17:02:34 +0100 Subject: [PATCH 50/57] using proper method --- .github/workflows/build.yml | 68 +++++++++++++++++++++++++++-- connections/canconmanager.cpp | 2 +- connections/canserver.cpp | 2 +- connections/gvretserial.cpp | 4 +- connections/lawicel_serial.cpp | 6 +-- connections/mqtt_bus.cpp | 2 +- connections/serialbusconnection.cpp | 2 +- 7 files changed, 74 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 4a48d2d..0ed6a72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,68 @@ on: branches: - "master" -jobs: +jobs: + buildlinux: + name: Linux x64 + runs-on: ubuntu-20.04 + + steps: + - name: Prepare Environment + run: | + sudo apt-get install libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-randr0 libxkbcommon-x11-0 + + - name: Prepare Qt Libraries + uses: jurplel/install-qt-action@v3 + + - name: Clone + uses: actions/checkout@v3 + + - name: Compile + run: | + qmake CONFIG+=release PREFIX=/usr SavvyCAN.pro + make -j`grep -c ^processor /proc/cpuinfo` + + - name: Package + run: | + make INSTALL_ROOT=appdir install + wget -c -nv "https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage" + chmod a+x linuxdeployqt-continuous-x86_64.AppImage + ./linuxdeployqt-continuous-x86_64.AppImage appdir/usr/share/applications/SavvyCAN.desktop -appimage -extra-plugins=iconengines,platformthemes/libqgtk3.so,canbus + + - uses: actions/upload-artifact@v4 + with: + name: SavvyCAN-Linux_x64.AppImage + path: SavvyCAN-*x86_64.AppImage + + buildmacos: + name: macOS x64 + runs-on: macos-12 + + steps: + - name: Prepare macOS Environment + run: | + brew install qt5 + brew link qt5 --force + + - name: Clone + uses: actions/checkout@v3 + + - name: Compile + run: | + qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro + make -j`sysctl kern.aioprocmax | awk '{print $2}'` + + - name: Package + run: | + mkdir -p SavvyCAN.app/Contents/MacOS/help + cp -R help/* SavvyCAN.app/Contents/MacOS/help + macdeployqt SavvyCAN.app -dmg + + - uses: actions/upload-artifact@v4 + with: + name: SavvyCAN-macOS_x64.dmg + path: SavvyCAN.dmg + buildwindows: name: Windows x64 runs-on: windows-2019 @@ -62,7 +123,7 @@ jobs: pre-release: name: "pre-release" runs-on: "ubuntu-latest" - needs: [buildwindows] + needs: [buildwindows, buildmacos, buildlinux] steps: - uses: actions/checkout@v3 @@ -78,5 +139,6 @@ jobs: prerelease: true title: "Development Build" files: | + SavvyCAN-Linux_x64.AppImage/SavvyCAN-*x86_64.AppImage SavvyCAN-Windows_x64_CIBuild.zip - + SavvyCAN-macOS_x64.dmg/SavvyCAN.dmg diff --git a/connections/canconmanager.cpp b/connections/canconmanager.cpp index f31e70a..9ac931f 100644 --- a/connections/canconmanager.cpp +++ b/connections/canconmanager.cpp @@ -222,7 +222,7 @@ bool CANConManager::sendFrame(const CANFrame& pFrame) workingFrame.isReceived = false; if (useSystemTime) { - workingFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); + workingFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul)); } else { diff --git a/connections/canserver.cpp b/connections/canserver.cpp index a8ad145..22daf59 100644 --- a/connections/canserver.cpp +++ b/connections/canserver.cpp @@ -214,7 +214,7 @@ void CANserver::readNetworkData() frame_p->setFrameType(QCanBusFrame::DataFrame); frame_p->isReceived = true; - frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000ul)); + frame_p->setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul)); frame_p->setPayload(datagram.mid(dataByteLocation, length)); diff --git a/connections/gvretserial.cpp b/connections/gvretserial.cpp index 1b3dbc7..72eb637 100644 --- a/connections/gvretserial.cpp +++ b/connections/gvretserial.cpp @@ -685,7 +685,7 @@ void GVRetSerial::procRXChar(unsigned char c) buildTimestamp += timeBasis; if (useSystemTime) { - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul)); } else { buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); } @@ -771,7 +771,7 @@ void GVRetSerial::procRXChar(unsigned char c) buildTimestamp += timeBasis; if (useSystemTime) { - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul)); } else { buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); } diff --git a/connections/lawicel_serial.cpp b/connections/lawicel_serial.cpp index 37a078d..e45067d 100644 --- a/connections/lawicel_serial.cpp +++ b/connections/lawicel_serial.cpp @@ -493,7 +493,7 @@ void LAWICELSerial::readSerialData() if (useSystemTime) { - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul)); } else { @@ -502,13 +502,13 @@ void LAWICELSerial::readSerialData() { //Four bytes after the end of the data bytes. buildTimestamp = mBuildLine.mid(5 + mBuildLine.mid(4, 1).toInt() * 2, 4).toInt(nullptr, 16) * 1000l; + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); } else { //Default to system time if timestamps are disabled. - buildTimestamp = QDateTime::currentMSecsSinceEpoch() * 1000l; + buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul)); } - buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp)); } switch (mBuildLine[0].toLatin1()) diff --git a/connections/mqtt_bus.cpp b/connections/mqtt_bus.cpp index 86da138..a2e5b66 100644 --- a/connections/mqtt_bus.cpp +++ b/connections/mqtt_bus.cpp @@ -302,7 +302,7 @@ void MQTT_BUS::clientMessageReceived(const QMQTT::Message& message) frame_p->isReceived = true; if (useSystemTime) { - frame_p->setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); + frame_p->setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul)); } else frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, timeStamp)); diff --git a/connections/serialbusconnection.cpp b/connections/serialbusconnection.cpp index ad21817..4a17e3f 100644 --- a/connections/serialbusconnection.cpp +++ b/connections/serialbusconnection.cpp @@ -211,7 +211,7 @@ void SerialBusConnection::framesReceived() frame_p->isReceived = !recFrame.hasLocalEcho(); if (useSystemTime) { - frame_p->setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch())); + frame_p->setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul)); } else frame_p->setTimeStamp(QCanBusFrame::TimeStamp(0, (recFrame.timeStamp().seconds() * 1000000ul + recFrame.timeStamp().microSeconds()) - timeBasis)); From 2cf515566d2ae55deec324671d6ff996e2ae2c6b Mon Sep 17 00:00:00 2001 From: alessandro negrin Date: Sat, 29 Mar 2025 17:49:52 +0100 Subject: [PATCH 51/57] restoring workflows --- .github/workflows/build.yml | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0ed6a72..a2c7e6f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,8 +1,6 @@ name: Build on: - pull_request: - types: [opened, synchronize, reopened] push: branches: - "master" @@ -142,3 +140,30 @@ jobs: SavvyCAN-Linux_x64.AppImage/SavvyCAN-*x86_64.AppImage SavvyCAN-Windows_x64_CIBuild.zip SavvyCAN-macOS_x64.dmg/SavvyCAN.dmg + + notify: + name: Notify Discord + runs-on: ubuntu-latest + needs: [buildwindows, buildmacos, buildlinux] + steps: + - id: msg_var + name: remove newlines + run: | + export release_msg=$(cat << EOF + ${{ github.event.head_commit.message }} + EOF + ) + release_msg="${release_msg//$'\n'/'-'}" + release_msg="${release_msg//$'\r'/'-'}" + release_msg="${release_msg//$'('/'-'}" + release_msg="${release_msg//$')'/'-'}" + echo $release_msg + echo "::set-output name=msg::$release_msg" + + - name: Notify Discord + run: | + curl -i -H "Accept: application/json" \ + -H "Content-Type:application/json" \ + -X POST \ + --data "{\"content\": null,\"embeds\": [{\"title\": \"${{ github.repository }} Build Complete\", \"description\": \"${{ steps.msg_var.outputs.msg }} \\nCommit: ${{ github.sha }}\\n[Build Artifacts](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}#artifacts)\", \"color\": 5131997, \"author\": { \"name\": \"Github Builds\" } } ]}" \ + https://discord.com/api/webhooks/${{ secrets.WEBHOOK_ID }}/${{ secrets.WEBHOOK_TOKEN }} From 1e26228738b955688e17725e159f3da56db176b8 Mon Sep 17 00:00:00 2001 From: werner Date: Thu, 17 Apr 2025 11:46:18 +0200 Subject: [PATCH 52/57] Added fix for reading PCAN extended IDS in standard id range, --- framefileio.cpp | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/framefileio.cpp b/framefileio.cpp index 79b5527..0f3ddf6 100644 --- a/framefileio.cpp +++ b/framefileio.cpp @@ -1454,7 +1454,8 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) QByteArray bytes(numBytes, 0); thisFrame.isReceived = true; thisFrame.bus = 0; - if (thisFrame.frameId() > 0x10000000) + if ((thisFrame.frameId() > 0x10000000) || + (tokens[3].length() >= 8)) { thisFrame.setExtendedFrameFormat(true); } @@ -1506,7 +1507,8 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) //qDebug() << thisFrame.payload().length(); thisFrame.isReceived = true; thisFrame.bus = tokens[2].toInt(); - if (thisFrame.frameId() > 0x10000000) + if ((thisFrame.frameId() > 0x10000000) || + (tokens[4].length() >= 8)) { thisFrame.setExtendedFrameFormat(true); } @@ -1548,7 +1550,8 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) //qDebug() << thisFrame.payload().length(); thisFrame.isReceived = true; thisFrame.bus = 0; - if (thisFrame.frameId() > 0x10000000) + if ((thisFrame.frameId() > 0x10000000) || + (tokens[3].length() >= 8)) { thisFrame.setExtendedFrameFormat(true); } @@ -1600,7 +1603,8 @@ bool FrameFileIO::loadPCANFile(QString filename, QVector* frames) //qDebug() << thisFrame.payload().length(); thisFrame.isReceived = true; thisFrame.bus = tokens[3].toInt(); - if (thisFrame.frameId() > 0x10000000) + if ((thisFrame.frameId() > 0x10000000) || + (tokens[4].length() >= 8)) { thisFrame.setExtendedFrameFormat(true); } From 4533a8f097d2e0eeb76d863f386027636f21b791 Mon Sep 17 00:00:00 2001 From: LHSmicius Date: Sun, 4 May 2025 00:53:08 +0300 Subject: [PATCH 53/57] chore: add installer script for linux --- install-linux.sh | 79 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100755 install-linux.sh diff --git a/install-linux.sh b/install-linux.sh new file mode 100755 index 0000000..20b5ff9 --- /dev/null +++ b/install-linux.sh @@ -0,0 +1,79 @@ +#!/bin/bash + +APP_PATH="/usr/bin/SavvyCAN" +DESKTOP_PATH="/usr/share/applications/savvycan.desktop" +ICON_PATH="/usr/share/pixmaps/SavvyCAN.png" + +require_root() { + if [ "$(id -u)" -ne 0 ]; then + echo "This script must be run as root." + exit 1 + fi +} + +show_help() { + echo "Usage: $0 [option]" + echo "" + echo "Options:" + echo " --help Show this help message" + echo " --uninstall Remove SavvyCAN from the system" + echo " (no option) Install or reinstall SavvyCAN" +} + +install_error() { + echo "SavvyCAN installation failed." + exit 1 +} + +uninstall_error() { + echo "SavvyCAN uninstallation failed." + exit 1 +} + +if [ "$1" = "--help" ]; then + show_help + exit 0 +fi + +require_root + +# Uninstall +if [ "$1" = "--uninstall" ]; then + echo "Uninstalling SavvyCAN..." + trap uninstall_error ERR + rm -f "$APP_PATH" "$DESKTOP_PATH" "$ICON_PATH" + trap - ERR + echo "SavvyCAN has been uninstalled." + exit 0 +fi + +# Check required files +if [ ! -f "SavvyCAN" ]; then + echo "Missing file \"SavvyCAN\". You need to build first." + install_error +fi + +if [ ! -f "SavvyCAN.desktop" ]; then + echo "Missing file \"SavvyCAN.desktop\"." + install_error +fi + +if [ ! -f "icons/SavvyIcon.png" ]; then + echo "Missing file \"icons/SavvyIcon.png\"." + install_error +fi + +# Install +if [ -f "$APP_PATH" ]; then + echo "Re-installing SavvyCAN..." +else + echo "Installing SavvyCAN..." +fi + +trap install_error ERR +install -Dm755 SavvyCAN "$APP_PATH" +install -Dm644 SavvyCAN.desktop "$DESKTOP_PATH" +install -Dm644 icons/SavvyIcon.png "$ICON_PATH" +trap - ERR + +echo "SavvyCAN is installed." From 1c0886bb5815b13540882af9e6670e37cf495bb1 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 5 May 2025 18:26:46 -0400 Subject: [PATCH 54/57] Update build.yml --- .github/workflows/build.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e33c73..fa04916 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -4,7 +4,6 @@ on: push: branches: - "master" - workflow_dispatch: jobs: buildlinux: name: Linux x64 From fe4bca309bf2ba8c62e1b4cc71418a7b06e7fe82 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 5 May 2025 18:43:29 -0400 Subject: [PATCH 55/57] Update build.yml Update workflow runners to newer OS versions as previous versions have been deprecated and no longer work --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fa04916..c4bf898 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,7 +7,7 @@ on: jobs: buildlinux: name: Linux x64 - runs-on: ubuntu-20.04 + runs-on: ubuntu-22.04 steps: - name: Prepare Environment @@ -39,7 +39,7 @@ jobs: buildmacos: name: macOS x64 - runs-on: macos-12 + runs-on: macos-13 steps: - name: Prepare macOS Environment From dfc7e52ec2f88dcc8012ff61e06cba3a440540ff Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 5 May 2025 19:02:04 -0400 Subject: [PATCH 56/57] Update build.yml Hopefully fix linux build, add Apple Silicon build on MacOS14 --- .github/workflows/build.yml | 38 +++++++++++++++++++++++++++++++++---- 1 file changed, 34 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c4bf898..90b2707 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -12,7 +12,7 @@ jobs: steps: - name: Prepare Environment run: | - sudo apt-get install libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-randr0 libxkbcommon-x11-0 + sudo apt-get install libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 libxcb-xinerama0 libxcb-randr0 libxkbcommon-x11-0 libfuse2 - name: Prepare Qt Libraries uses: jurplel/install-qt-action@v3 @@ -37,7 +37,7 @@ jobs: name: SavvyCAN-Linux_x64.AppImage path: SavvyCAN-*x86_64.AppImage - buildmacos: + buildmacos86: name: macOS x64 runs-on: macos-13 @@ -65,6 +65,35 @@ jobs: with: name: SavvyCAN-macOS_x64.dmg path: SavvyCAN.dmg + + buildmacos-arm64: + name: macOS ARM64 + runs-on: macos-14 + + steps: + - name: Prepare macOS Environment + run: | + brew install qt5 + brew link qt5 --force + + - name: Clone + uses: actions/checkout@v3 + + - name: Compile + run: | + qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro + make -j`sysctl kern.aioprocmax | awk '{print $2}'` + + - name: Package + run: | + mkdir -p SavvyCAN.app/Contents/MacOS/help + cp -R help/* SavvyCAN.app/Contents/MacOS/help + macdeployqt SavvyCAN.app -dmg + + - uses: actions/upload-artifact@v4 + with: + name: SavvyCAN-macOS_ARM64.dmg + path: SavvyCAN.dmg buildwindows: name: Windows x64 @@ -120,7 +149,7 @@ jobs: pre-release: name: "pre-release" runs-on: "ubuntu-latest" - needs: [buildwindows, buildmacos, buildlinux] + needs: [buildwindows, buildmacos86, buildmacos-arm64, buildlinux] steps: - uses: actions/checkout@v3 @@ -139,11 +168,12 @@ jobs: SavvyCAN-Linux_x64.AppImage/SavvyCAN-*x86_64.AppImage SavvyCAN-Windows_x64_CIBuild.zip SavvyCAN-macOS_x64.dmg/SavvyCAN.dmg + SavvyCAN-macOS_ARM64.dmg/SavvyCAN.dmg notify: name: Notify Discord runs-on: ubuntu-latest - needs: [buildwindows, buildmacos, buildlinux] + needs: [buildwindows, buildmacos86, buildmacos-arm64, buildlinux] steps: - id: msg_var name: remove newlines From 20adc5e60b9b5302dff5295a76a5ee553647bb43 Mon Sep 17 00:00:00 2001 From: Collin Kidder Date: Mon, 5 May 2025 19:13:47 -0400 Subject: [PATCH 57/57] Update build.yml Oops, need to make x86_64 and ARM64 versions of MacOS build use different names. --- .github/workflows/build.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 90b2707..5961338 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -60,11 +60,12 @@ jobs: mkdir -p SavvyCAN.app/Contents/MacOS/help cp -R help/* SavvyCAN.app/Contents/MacOS/help macdeployqt SavvyCAN.app -dmg + mv SavvyCAN.dmg SavvyCAN_x64.dmg - uses: actions/upload-artifact@v4 with: name: SavvyCAN-macOS_x64.dmg - path: SavvyCAN.dmg + path: SavvyCAN_x64.dmg buildmacos-arm64: name: macOS ARM64 @@ -89,11 +90,12 @@ jobs: mkdir -p SavvyCAN.app/Contents/MacOS/help cp -R help/* SavvyCAN.app/Contents/MacOS/help macdeployqt SavvyCAN.app -dmg + mv SavvyCAN.dmg SavvyCAN_arm64.dmg - uses: actions/upload-artifact@v4 with: name: SavvyCAN-macOS_ARM64.dmg - path: SavvyCAN.dmg + path: SavvyCAN_arm64.dmg buildwindows: name: Windows x64 @@ -167,8 +169,8 @@ jobs: files: | SavvyCAN-Linux_x64.AppImage/SavvyCAN-*x86_64.AppImage SavvyCAN-Windows_x64_CIBuild.zip - SavvyCAN-macOS_x64.dmg/SavvyCAN.dmg - SavvyCAN-macOS_ARM64.dmg/SavvyCAN.dmg + SavvyCAN-macOS_x64.dmg/SavvyCAN_x64.dmg + SavvyCAN-macOS_ARM64.dmg/SavvyCAN_arm64.dmg notify: name: Notify Discord