Merge branch 'master' of github.com:collin80/SavvyCAN

This commit is contained in:
Collin Kidder
2026-05-14 21:03:14 -04:00
25 changed files with 7821 additions and 83 deletions
+8 -1
View File
@@ -54,10 +54,13 @@ jobs:
run: |
qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro
make -j`sysctl kern.aioprocmax | awk '{print $2}'`
lrelease translations/*.ts
- name: Package
run: |
mkdir -p SavvyCAN.app/Contents/MacOS/translations
mkdir -p SavvyCAN.app/Contents/MacOS/help
cp -R translations/*.qm SavvyCAN.app/Contents/MacOS/translations
cp -R help/* SavvyCAN.app/Contents/MacOS/help
macdeployqt SavvyCAN.app -dmg
mv SavvyCAN.dmg SavvyCAN_x64.dmg
@@ -84,10 +87,13 @@ jobs:
run: |
qmake CONFIG+=release CONFIG+=sdk_no_version_check SavvyCAN.pro
make -j`sysctl kern.aioprocmax | awk '{print $2}'`
lrelease translations/*.ts
- name: Package
run: |
mkdir -p SavvyCAN.app/Contents/MacOS/translations
mkdir -p SavvyCAN.app/Contents/MacOS/help
cp -R translations/*.qm SavvyCAN.app/Contents/MacOS/translations
cp -R help/* SavvyCAN.app/Contents/MacOS/help
macdeployqt SavvyCAN.app -dmg
mv SavvyCAN.dmg SavvyCAN_arm64.dmg
@@ -120,6 +126,7 @@ jobs:
mkdir package
copy "release/SavvyCAN.exe" package/
mkdir package/help
copy translations/*.qm package/translations/
copy help/*.md package/help/
mkdir package/help/images
copy help/images/*.* package/help/images/
+4
View File
@@ -7,6 +7,9 @@ build/
*.o
*.obj
# Compiled translations
*.qm
# Precompiled Headers
*.gch
*.pch
@@ -36,6 +39,7 @@ SavvyCAN
**/moc_*
**/ui_*.h
**/qrc_*.cpp
.qtc_clangd
SavvyCAN.pro.user.*
.qmake.stash
Makefile
+1 -1
View File
@@ -48,7 +48,7 @@ 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 does compile with QT6. Support for QT6 is approximately "beta" quality. Most all functions should work, please send issues if found.
NOTE: There is a work-in-progress branch QT6WIP that can compile with QT6 (Tested with Qt 6.9 on 2026-03-11). 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
+4
View File
@@ -1,11 +1,15 @@
[Desktop Entry]
Type=Application
Name=SavvyCAN
Name[pt_BR]=SavvyCAN
GenericName=CANBus reverse engineering tool
GenericName[pt_BR]=Ferramenta Rede CAN de engenharia reversa
Comment=Facilitates reverse engineering of canbus captures
Comment[pt_BR]=Facilitar engenharia reversa de capturas de Rede CAN
Exec=SavvyCAN
Icon=SavvyCAN
Terminal=false
Categories=Development;Electronics;IDE;
MimeType=text/x-application;
Keywords=embedded electronics;electronics;canbus;reverse engineering;
Keywords[pt_BR]=eletronica embarcada;eletronicos;canbus;engenharia reversa;
+6
View File
@@ -279,3 +279,9 @@ INSTALLS += helpfiles
INSTALLS += target
TRANSLATIONS += \
translations/SavvyCAN_en.ts \
translations/SavvyCAN_pt_BR.ts
DISTFILES += \
translations/pt_BR.qph
+38 -17
View File
@@ -3,6 +3,8 @@
#include <QFile>
#include <QApplication>
#include <QPalette>
#include <QColor>
#include <QBrush>
#include <QDateTime>
#include <QSettings>
#include "utility.h"
@@ -76,6 +78,7 @@ CANFrameModel::CANFrameModel(QObject *parent)
overwriteDups = false;
filtersPersistDuringClear = false;
useHexMode = true;
useColorsByCanId = false;
timeStyle = TS_MICROS;
timeOffset = 0;
needFilterRefresh = false;
@@ -101,6 +104,16 @@ void CANFrameModel::setHexMode(bool mode)
}
}
void CANFrameModel::setUseColorsByCanId(bool mode)
{
if (useColorsByCanId != mode)
{
this->beginResetModel();
useColorsByCanId = mode;
this->endResetModel();
}
}
void CANFrameModel::setTimeStyle(TimeStyle newStyle)
{
if (timeStyle != newStyle)
@@ -401,7 +414,6 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const
{
QString tempString;
CANFrame thisFrame;
static bool rowFlip = false;
QVariant ts;
if (!index.isValid())
@@ -425,9 +437,31 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const
return msg->bgColor;
}
}
rowFlip = (index.row() % 2);
if (rowFlip) return QApplication::palette().color(QPalette::Base);
else return QApplication::palette().color(QPalette::AlternateBase);
if (useColorsByCanId)
{
QString frameStr = Utility::formatCANID(thisFrame.frameId(), thisFrame.hasExtendedFrameFormat());
// Color rows by the value in the "category" column
return QBrush(Utility::colorForString(frameStr));
}
return (index.row() % 2) ?
QApplication::palette().color(QPalette::Base) :
QApplication::palette().color(QPalette::AlternateBase);
}
if (role == Qt::ForegroundRole)
{
if (dbcHandler != nullptr && interpretFrames && !ignoreDBCColors)
{
DBC_MESSAGE *msg = dbcHandler->findMessage(thisFrame);
if (msg != nullptr)
{
return msg->fgColor;
}
}
return QApplication::palette().color(QPalette::WindowText);
}
if (role == Qt::TextAlignmentRole)
@@ -448,19 +482,6 @@ QVariant CANFrameModel::data(const QModelIndex &index, int role) const
}
}
if (role == Qt::ForegroundRole)
{
if (dbcHandler != nullptr && interpretFrames && !ignoreDBCColors)
{
DBC_MESSAGE *msg = dbcHandler->findMessage(thisFrame);
if (msg != nullptr)
{
return msg->fgColor;
}
}
return QApplication::palette().color(QPalette::WindowText);
}
if (role == Qt::DisplayRole) {
switch (Column(index.column()))
{
+2
View File
@@ -47,6 +47,7 @@ public:
bool getInterpretMode();
void setOverwriteMode(bool);
void setHexMode(bool);
void setUseColorsByCanId(bool);
void setClearMode(bool mode);
void setTimeStyle(TimeStyle newStyle);
void setIgnoreDBCColors(bool mode);
@@ -94,6 +95,7 @@ private:
QString timeFormat;
TimeStyle timeStyle;
bool useHexMode;
bool useColorsByCanId;
bool needFilterRefresh;
bool ignoreDBCColors;
int64_t timeOffset;
+38 -22
View File
@@ -17,6 +17,7 @@ LAWICELSerial::LAWICELSerial(QString portName, int serialSpeed, int lawicelSpeed
serial = nullptr;
isAutoRestart = false;
rebuildLocalTimeBasis();
readSettings();
}
@@ -203,6 +204,13 @@ bool LAWICELSerial::piSendFrame(const CANFrame& frame)
/****************************************************************/
void LAWICELSerial::rebuildLocalTimeBasis()
{
timeBasis = 0;
lastHWTimestamp = -1;
wrapAdder = 0;
}
void LAWICELSerial::readSettings()
{
QSettings settings;
@@ -254,6 +262,8 @@ void LAWICELSerial::deviceConnected()
{
sendDebug("Connecting to LAWICEL Device!");
rebuildLocalTimeBasis();
QByteArray output;
output.clear();
@@ -474,8 +484,6 @@ void LAWICELSerial::readSerialData()
QByteArray data;
unsigned char c;
QString debugBuild;
CANFrame buildFrame;
QByteArray buildData;
if (serial) data = serial->readAll();
@@ -491,41 +499,49 @@ void LAWICELSerial::readSerialData()
{
qDebug() << "Got CR!";
if (useSystemTime)
{
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul));
}
else
CANFrame buildFrame;
QByteArray buildData;
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, QDateTime::currentMSecsSinceEpoch() * 1000ll));
if (!useSystemTime)
{
bool isFD = false;
char offset;
char len_index;
switch(mBuildLine[0].toLatin1()) {
case 't':
case 'b':
case 'd':
isFD = true;
[[fallthrough]];
case 't':
offset = 5;
len_index = 4;
break;
case 'T':
break;
case 'B':
case 'D':
isFD = true;
[[fallthrough]];
case 'T':
offset = 10;
len_index = 9;
break;
break;
default:
offset = 5;
len_index = 4;
break;
break;
}
//If total length is greater than command, header and data, timestamps must be enabled.
if (data.length() > (offset + mBuildLine.mid(len_index, 1).toInt() * 2 + 1))
int dlc = mBuildLine.mid(len_index, 1).toInt();
int byteCount = isFD ? (int)dlc_code_to_bytes(dlc) : dlc;
int frameStringLen = offset + byteCount * 2;
if (mBuildLine.length() > frameStringLen + 1)
{
//Four bytes after the end of the data bytes.
buildTimestamp = mBuildLine.mid(offset + mBuildLine.mid(len_index, 1).toInt() * 2, 4).toInt(nullptr, 16);
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, buildTimestamp));
}
else
{
//Default to system time if timestamps are disabled.
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp::fromMicroSeconds(QDateTime::currentMSecsSinceEpoch() * 1000ul));
qint64 hardwareMs = mBuildLine.mid(frameStringLen, 4).toInt(nullptr, 16);
if (lastHWTimestamp >= 0 && hardwareMs < lastHWTimestamp) { wrapAdder += 60000; }
lastHWTimestamp = hardwareMs;
qint64 unwrappedMs = wrapAdder + hardwareMs;
if (timeBasis == 0) { timeBasis = QDateTime::currentMSecsSinceEpoch() - unwrappedMs; }
buildFrame.setTimeStamp(QCanBusFrame::TimeStamp(0, (timeBasis + unwrappedMs) * 1000ll));
}
}
+3 -2
View File
@@ -60,8 +60,9 @@ protected:
bool isAutoRestart;
QSerialPort *serial;
int framesRapid;
CANFrame buildFrame;
qint64 buildTimestamp;
qint64 timeBasis;
qint64 lastHWTimestamp;
qint64 wrapAdder;
bool can0Enabled;
bool can0ListenOnly;
bool canFd;
+7 -1
View File
@@ -66,6 +66,13 @@ public:
QString descript;
};
class DBC_VAL_TABLE
{
public:
QString name;
QList<DBC_VAL_ENUM_ENTRY> valList;
};
class DBC_NODE
{
public:
@@ -176,4 +183,3 @@ public:
#endif // DBC_CLASSES_H
+126 -27
View File
@@ -16,6 +16,28 @@
DBCHandler* DBCHandler::instance = nullptr;
static bool parseValueDescriptions(QString tokenString, QList<DBC_VAL_ENUM_ENTRY> &values)
{
QRegularExpression regex;
QRegularExpressionMatch match;
DBC_VAL_ENUM_ENTRY val;
values.clear();
tokenString = tokenString.trimmed();
while (tokenString.length() > 2)
{
regex.setPattern("^([+-]?\\d+) \\\"(.*?)\\\"(.*)");
match = regex.match(tokenString);
if (!match.hasMatch()) return false;
val.value = match.captured(1).toInt();
val.descript = match.captured(2);
values.append(val);
tokenString = match.captured(3).trimmed();
}
return values.count() > 0;
}
DBC_SIGNAL* DBCSignalHandler::findSignalByIdx(int idx)
{
if (sigs.count() == 0) return nullptr;
@@ -316,6 +338,8 @@ DBCFile::DBCFile(const DBCFile& cpy) : QObject()
assocBuses = cpy.assocBuses;
dbc_nodes.clear();
dbc_nodes.append(cpy.dbc_nodes);
dbc_value_tables.clear();
dbc_value_tables.append(cpy.dbc_value_tables);
dbc_attributes.clear();
dbc_attributes.append(cpy.dbc_attributes);
isDirty = cpy.isDirty;
@@ -331,6 +355,8 @@ DBCFile& DBCFile::operator=(const DBCFile& cpy)
assocBuses = cpy.assocBuses;
dbc_nodes.clear();
dbc_nodes.append(cpy.dbc_nodes);
dbc_value_tables.clear();
dbc_value_tables.append(cpy.dbc_value_tables);
dbc_attributes.clear();
dbc_attributes.append(cpy.dbc_attributes);
}
@@ -382,6 +408,19 @@ DBC_NODE* DBCFile::findNodeByNameAndComment(QString fullname)
return nullptr;
}
DBC_VAL_TABLE* DBCFile::findValueTableByName(QString name)
{
if (dbc_value_tables.length() == 0) return nullptr;
for (int i = 0; i < dbc_value_tables.length(); i++)
{
if (name.compare(dbc_value_tables[i].name, Qt::CaseInsensitive) == 0)
{
return &dbc_value_tables[i];
}
}
return nullptr;
}
QString DBCFile::getFullFilename()
{
return filePath + fileName;
@@ -712,6 +751,29 @@ bool DBCFile::parseSignalValueTypeLine(QString line)
}
bool DBCFile::parseValueTableLine(QString line)
{
QRegularExpression regex;
QRegularExpressionMatch match;
regex.setPattern("^VAL\\_TABLE\\_ ([-\\w]+) (.*);");
match = regex.match(line);
//captured 1 is the table name
//captured 2 is a series of values in the form (number "text")
if (match.hasMatch())
{
DBC_VAL_TABLE table;
table.name = match.captured(1);
if (!parseValueDescriptions(match.captured(2), table.valList)) return false;
DBC_VAL_TABLE *existingTable = findValueTableByName(table.name);
if (existingTable) existingTable->valList = table.valList;
else dbc_value_tables.append(table);
return true;
}
return false;
}
bool DBCFile::parseValueLine(QString line)
{
QRegularExpression regex;
@@ -733,25 +795,18 @@ bool DBCFile::parseValueLine(QString line)
if (sig != nullptr)
{
QString tokenString = match.captured(3);
DBC_VAL_ENUM_ENTRY val;
while (tokenString.length() > 2)
QList<DBC_VAL_ENUM_ENTRY> values;
DBC_VAL_TABLE *valueTable = findValueTableByName(tokenString.trimmed());
if (valueTable)
{
regex.setPattern("(\\d+) \\\"(.*?)\\\"(.*)");
match = regex.match(tokenString);
if (match.hasMatch())
{
val.value = match.captured(1).toULong() & 0x1FFFFFFFul;
val.descript = match.captured(2);
//qDebug() << "sig val " << val.value << " desc " <<val.descript;
sig->valList.append(val);
int rightSize = tokenString.length() - match.captured(1).length() - match.captured(2).length() - 4;
if (rightSize > 0) tokenString = tokenString.right(rightSize);
else tokenString = "";
//qDebug() << "New token string: " << tokenString;
}
else tokenString = "";
sig->valList = valueTable->valList;
return true;
}
if (parseValueDescriptions(tokenString, values))
{
sig->valList.append(values);
return true;
}
return true;
}
}
}
@@ -919,6 +974,8 @@ bool DBCFile::loadFile(QString fileName)
QString fileBaseName = QFileInfo(fileName).baseName();
bool inMultilineBU = false;
bool inMultilineValTable = false;
QString multilineValTable;
qDebug() << "DBC File: " << fileName;
@@ -931,6 +988,7 @@ bool DBCFile::loadFile(QString fileName)
qDebug() << "Starting DBC load";
dbc_nodes.clear();
dbc_value_tables.clear();
messageHandler->removeAllMessages();
messageHandler->setMatchingCriteria(EXACT);
messageHandler->setFilterLabeling(false);
@@ -962,6 +1020,18 @@ bool DBCFile::loadFile(QString fileName)
else inMultilineBU = false;
}
if (inMultilineValTable)
{
multilineValTable.append(" " + line);
if (line.endsWith(";"))
{
parseValueTableLine(multilineValTable);
multilineValTable.clear();
inMultilineValTable = false;
}
continue;
}
if (!inMultilineBU)
{
if (line.startsWith("BO_ ")) //defines a message
@@ -1064,6 +1134,19 @@ bool DBCFile::loadFile(QString fileName)
}
}
}
if (line.startsWith("VAL_TABLE_ "))
{
if (line.endsWith(";"))
{
parseValueTableLine(line);
}
else
{
multilineValTable = line;
inMultilineValTable = true;
}
}
//VAL_ (1090) (VCUPresentParkLightOC) (1 "Error present" 0 "Error not present") ;
if (line.startsWith("VAL_ "))
{
@@ -1347,8 +1430,8 @@ bool DBCFile::saveFile(QString fileName)
int msgNumber = 1;
int sigNumber = 1;
QFile *outFile = new QFile(fileName);
QString nodesOutput, msgOutput, commentsOutput, valuesOutput, extMultiplexOutput;
QString defaultsOutput, attrValOutput;
QString nodesOutput, msgOutput, commentsOutput, valuesOutput, valueTableOutput, extMultiplexOutput;
QString defaultsOutput, attrValOutput, sigValTypeOutput;
bool hasExtendedMultiplexing = false;
if (!outFile->open(QIODevice::WriteOnly | QIODevice::Text))
@@ -1433,6 +1516,23 @@ bool DBCFile::saveFile(QString fileName)
nodesOutput.append("\n");
outFile->write(nodesOutput.toUtf8());
for (int x = 0; x < dbc_value_tables.count(); x++)
{
DBC_VAL_TABLE table = dbc_value_tables[x];
valueTableOutput.append("VAL_TABLE_ " + table.name);
for (int v = 0; v < table.valList.count(); v++)
{
DBC_VAL_ENUM_ENTRY val = table.valList[v];
valueTableOutput.append(" " + QString::number(val.value) + " \"" + val.descript + "\"");
}
valueTableOutput.append(";\n");
}
if (!valueTableOutput.isEmpty())
{
outFile->write(valueTableOutput.toUtf8());
valueTableOutput.clear();
}
//Go through all messages one at at time issuing the message line then all signals in there too.
for (int x = 0; x < messageHandler->getCount(); x++)
{
@@ -1512,17 +1612,11 @@ bool DBCFile::saveFile(QString fileName)
else msgOutput.append("0+");
break;
case SIGNED_INT:
case SP_FLOAT:
case DP_FLOAT:
if (sig->intelByteOrder) msgOutput.append("1-");
else msgOutput.append("0-");
break;
case SP_FLOAT:
if (sig->intelByteOrder) msgOutput.append("5-");
else msgOutput.append("2-");
break;
case DP_FLOAT:
if (sig->intelByteOrder) msgOutput.append("6-");
else msgOutput.append("3-");
break;
case STRING:
msgOutput.append("4-");
break;
@@ -1533,6 +1627,8 @@ bool DBCFile::saveFile(QString fileName)
msgOutput.append(" (" + QString::number(sig->factor) + "," + QString::number(sig->bias) + ") [" +
QString::number(sig->min) + "|" + QString::number(sig->max) + "] \"" + sig->unitName
+ "\" " + sig->receiver->name + "\n");
if (sig->valType == SP_FLOAT || sig->valType == DP_FLOAT)
sigValTypeOutput.append("SIG_VALTYPE_ " + QString::number(ID) + " " + sig->name + " : " + QString::number(sig->valType == SP_FLOAT ? 1 : 2) + ";\n");
if (sig->comment.length() > 0)
{
commentsOutput.append("CM_ SG_ " + QString::number(ID) + " " + sig->name + " \"" + sig->comment + "\";\n");
@@ -1685,11 +1781,14 @@ bool DBCFile::saveFile(QString fileName)
outFile->write(attrValOutput.toUtf8());
outFile->write(valuesOutput.toUtf8());
outFile->write(extMultiplexOutput.toUtf8());
if (!sigValTypeOutput.isEmpty())
outFile->write(sigValTypeOutput.toUtf8());
attrValOutput.clear();
defaultsOutput.clear();
valuesOutput.clear();
extMultiplexOutput.clear();
sigValTypeOutput.clear();
outFile->close();
delete outFile;
+3
View File
@@ -75,6 +75,7 @@ public:
DBC_NODE *findNodeByName(QString name);
DBC_NODE *findNodeByNameAndComment(QString fullname);
DBC_NODE *findNodeByIdx(int idx);
DBC_VAL_TABLE *findValueTableByName(QString name);
DBC_ATTRIBUTE *findAttributeByName(QString name, DBC_ATTRIBUTE_TYPE type = ATTR_TYPE_ANY);
DBC_ATTRIBUTE *findAttributeByIdx(int idx);
void findAttributesByType(DBC_ATTRIBUTE_TYPE typ, QList<DBC_ATTRIBUTE> *list);
@@ -93,6 +94,7 @@ public:
DBCMessageHandler *messageHandler;
QList<DBC_NODE> dbc_nodes;
QList<DBC_VAL_TABLE> dbc_value_tables;
QList<DBC_ATTRIBUTE> dbc_attributes;
private:
QString fileName;
@@ -105,6 +107,7 @@ private:
DBC_SIGNAL* parseSignalLine(QString line, DBC_MESSAGE *msg);
bool parseSignalMultiplexValueLine(QString line);
DBC_MESSAGE* parseMessageLine(QString line);
bool parseValueTableLine(QString line);
bool parseValueLine(QString line);
bool parseSignalValueTypeLine(QString line);
bool parseAttributeLine(QString line);
+26 -3
View File
@@ -1,5 +1,8 @@
#include "mainwindow.h"
#include <QApplication>
#include <QTranslator>
#include <QLocale>
#include <QDebug>
class SavvyCANApplication : public QApplication
{
@@ -39,11 +42,31 @@ int main(int argc, char *argv[])
a.setOrganizationName("EVTV");
a.setApplicationName("SavvyCAN");
a.setOrganizationDomain("evtv.me");
QSettings::setDefaultFormat(QSettings::IniFormat);
QSettings settings;
QString localeString = settings.value("Main/Language").toString();
if (localeString.isEmpty()) {
QLocale sysLocale = QLocale::system();
localeString = sysLocale.name();
settings.setValue("Main/Language", localeString);
}
QTranslator translator;
QLocale locale(localeString);
QString lang = locale.name();
QString shortLang = locale.name().left(2);
if (QString translationDir = QCoreApplication::applicationDirPath() + "/translations"; !translator.load("SavvyCAN_" + lang, translationDir)) {
translator.load("SavvyCAN_" + shortLang, translationDir);
}
a.installTranslator(&translator);
qInfo() << "Locale Value is:" << locale.name();
a.mainWindow = new MainWindow();
QSettings settings;
int fontSize = settings.value("Main/FontSize", 9).toUInt();
QFont sysFont = QFont(); //get default font
sysFont.setPointSize(fontSize);
@@ -52,8 +75,8 @@ int main(int argc, char *argv[])
a.mainWindow->show();
int retCode = a.exec();
delete a.mainWindow; a.mainWindow = NULL;
return retCode;
}
+53
View File
@@ -3,6 +3,10 @@
#include "helpwindow.h"
#include <qevent.h>
#include <QDebug>
#include <QDir>
#include <QRegExp>
#include <QCoreApplication>
#include <QDebug>
#include "simplecrypt.h"
//using this simple encryption library to obfuscate stored password a bit. It's not super secure but better than
@@ -10,6 +14,42 @@
//whatever key, you'd see it. Just behave yourselves
SimpleCrypt crypto(Q_UINT64_C(0xdeadbeefface6285));
static QStringList availableLanguageCodes()
{
QStringList codes;
QDir transDir(QCoreApplication::applicationDirPath() + "/translations");
QStringList list = transDir.entryList(QStringList() << "SavvyCAN_*.qm");
//* just for test purposes
if (list.isEmpty()) {
// maybe only ts files exist during development
list = transDir.entryList(QStringList() << "SavvyCAN_*.ts");
}
for (QString file : list) {
QRegExp re("SavvyCAN_(.*)\\.(qm|ts)");
if (re.indexIn(file) != -1) {
codes << re.cap(1);
}
}
return codes;
}
void MainSettingsDialog::populateLanguageCombo()
{
ui->comboLanguage->clear();
QStringList codes = availableLanguageCodes();
if (codes.isEmpty()) {
codes << "en"; // fallback
}
for (const QString &code : codes) {
QLocale locale(code);
QString name = locale.nativeLanguageName();
qInfo() << code;
if (name.isEmpty())
name = code;
ui->comboLanguage->addItem(name, code);
}
}
MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
QDialog(parent),
ui(new Ui::MainSettingsDialog)
@@ -17,6 +57,8 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
QSettings settings;
ui->setupUi(this);
populateLanguageCombo();
//TODO: This is still hard coded to support only two buses. Sometimes there is none, sometimes 1, sometimes much more than 2. Fix this.
ui->comboSendingBus->addItem(tr("None"));
ui->comboSendingBus->addItem(tr("0"));
@@ -90,7 +132,15 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
ui->cbUseFiltered->setChecked(settings.value("Main/UseFiltered", false).toBool());
ui->cbUseOpenGL->setChecked(settings.value("Main/UseOpenGL", false).toBool());
ui->cbFilterLabeling->setChecked(settings.value("Main/FilterLabeling", true).toBool());
ui->cbFilterLabeling->setChecked(settings.value("Main/FilterLabeling", true).toBool());
ui->cbIgnoreDBCColors->setChecked(settings.value("Main/IgnoreDBCColors", false).toBool());
ui->cbColorsByCanId->setChecked(settings.value("Main/ColorsByCanId", false).toBool());
if (QString savedLang = settings.value("Main/Language").toString(); !savedLang.isEmpty()) {
int idx = ui->comboLanguage->findData(savedLang);
if (idx >= 0)
ui->comboLanguage->setCurrentIndex(idx);
}
int maxFramesDefault;
if (QSysInfo::WordSize > 32)
@@ -108,6 +158,7 @@ MainSettingsDialog::MainSettingsDialog(QWidget *parent) :
ui->spinBytesPerLine->setValue(settings.value("Main/BytesPerLine", 8).toInt());
//just for simplicity they all call the same function and that function updates all settings at once
connect(ui->comboLanguage, SIGNAL(currentIndexChanged(int)), this, SLOT(updateSettings()));
connect(ui->cbDisplayHex, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
connect(ui->cbFlowAutoRef, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
connect(ui->cbFlowUseTimestamp, SIGNAL(toggled(bool)), this, SLOT(updateSettings()));
@@ -177,6 +228,7 @@ void MainSettingsDialog::updateSettings()
{
QSettings settings;
settings.setValue("Main/Language", ui->comboLanguage->currentData().toString());
settings.setValue("Main/UseHex", ui->cbDisplayHex->isChecked());
settings.setValue("FlowView/AutoRef", ui->cbFlowAutoRef->isChecked());
settings.setValue("FlowView/UseTimestamp", ui->cbFlowUseTimestamp->isChecked());
@@ -208,6 +260,7 @@ void MainSettingsDialog::updateSettings()
settings.setValue("Main/MaximumFrames", ui->spinMaximumFrames->value());
settings.setValue("Main/BytesPerLine", ui->spinBytesPerLine->value());
settings.setValue("Main/FontFixedWidth", ui->cbFontFixedWidth->isChecked());
settings.setValue("Main/ColorsByCanId", ui->cbColorsByCanId->isChecked());
settings.sync();
emit updatedSettings();
+1
View File
@@ -25,6 +25,7 @@ public slots:
private:
Ui::MainSettingsDialog *ui;
void populateLanguageCombo();
void closeEvent(QCloseEvent *event);
bool eventFilter(QObject *obj, QEvent *event);
};
+73 -9
View File
@@ -10,6 +10,7 @@
#include "utility.h"
#include "filterutility.h"
#include <QClipboard>
/*
Some notes on things I'd like to put into the program but haven't put on github (yet)
@@ -36,7 +37,7 @@ MainWindow::MainWindow(QWidget *parent) :
#endif
useHex = true;
useColorsByCanId = false;
selfRef = this;
this->setWindowTitle("Savvy CAN V" + QString::number(VERSION) + " [Built " + QString(__DATE__) +"]");
@@ -143,6 +144,11 @@ MainWindow::MainWindow(QWidget *parent) :
connect(ui->canFramesView, &QAbstractItemView::clicked, this, &MainWindow::gridClicked);
connect(ui->canFramesView, &QAbstractItemView::doubleClicked, this, &MainWindow::gridDoubleClicked);
ui->canFramesView->setContextMenuPolicy(Qt::CustomContextMenu);
copyAct = new QAction(tr("Copy"), this);
copyAct->setShortcut(QKeySequence::Copy);
connect(copyAct, &QAction::triggered, this, &MainWindow::copyFromTable);
ui->canFramesView->addAction(copyAct);
connect(ui->canFramesView, &QAbstractItemView::customContextMenuRequested, this, &MainWindow::gridContextMenuRequest);
connect(model, &CANFrameModel::updatedFiltersList, this, &MainWindow::updateFilterList);
@@ -415,6 +421,9 @@ void MainWindow::readUpdateableSettings()
model->setHexMode(useHex);
Utility::decimalMode = !useHex;
useColorsByCanId = settings.value("Main/ColorsByCanId", false).toBool();
model->setUseColorsByCanId(useColorsByCanId);
bool tempBool;
TimeStyle ts = TS_MICROS;
tempBool = settings.value("Main/TimeSeconds", false).toBool();
@@ -741,20 +750,75 @@ void MainWindow::gridContextMenuRequest(QPoint pos)
{
QModelIndex idx = ui->canFramesView->indexAt(pos); //figure out where in the view we clicked (row, column)
qDebug() << "Pos: " << pos << " Row :" << idx.row() << " Col: " << idx.column();
qDebug() << "Data: " << idx.data();
if (!idx.isValid()) return;
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(copyAct);
if (idx.column() == 8) //we're over the DATA column
{
contextMenuPosition = pos;
QMenu *menu = new QMenu(this);
menu->setAttribute(Qt::WA_DeleteOnClose);
menu->addAction(tr("Add to a new graphing window"), this, SLOT(setupAddToNewGraph()));
menu->addSeparator();
menu->addAction(tr("Add to a new graphing window"), this, SLOT(setupAddToNewGraph()));
menu->addAction(tr("Add to latest graphing window"), this, SLOT(setupSendToLatestGraphWindow()));
menu->popup(ui->canFramesView->mapToGlobal(pos));
}
menu->popup(ui->canFramesView->viewport()->mapToGlobal(pos));
}
void MainWindow::copyFromTable()
{
copySelection();
}
void MainWindow::copySelection()
{
QItemSelectionModel *selectionModel = ui->canFramesView->selectionModel();
QModelIndexList selectedIndexes = selectionModel->selectedIndexes();
if(selectedIndexes.isEmpty())
return;
// QModelIndex::operator< sorts by row and then by column.
std::sort(selectedIndexes.begin(), selectedIndexes.end());
QString selectedText;
int currentRow = -1;
int lastRow = selectedIndexes.last().row();
for(const QModelIndex &current : selectedIndexes)
{
if (currentRow != -1 && current.row() != currentRow)
{
// remove last tab
if (selectedText.endsWith(QLatin1Char('\t')))
selectedText.chop(1);
selectedText.append(QLatin1Char('\n'));
}
currentRow = current.row();
QString cellText = current.data(Qt::DisplayRole).toString();
// Replace newlines within a cell to avoid breaking the table structure in Excel
cellText.replace(QLatin1Char('\n'), QLatin1String(" "));
selectedText.append(cellText);
if (current.row() != lastRow || current != selectedIndexes.last())
{
selectedText.append(QLatin1Char('\t'));
}
}
// remove last tab if it exists
if (selectedText.endsWith(QLatin1Char('\t')))
selectedText.chop(1);
QClipboard *clipboard = QApplication::clipboard();
clipboard->setText(selectedText);
}
QString MainWindow::getSignalNameFromPosition(QPoint pos)
+4
View File
@@ -110,6 +110,7 @@ private slots:
void gridClicked(QModelIndex);
void gridDoubleClicked(QModelIndex);
void gridContextMenuRequest(QPoint pos);
void copyFromTable();
void setupAddToNewGraph();
void setupSendToLatestGraphWindow();
void interpretToggled(bool);
@@ -147,6 +148,7 @@ signals:
private:
Ui::MainWindow *ui;
QAction *copyAct;
static MainWindow *selfRef;
//canbus related data
@@ -160,6 +162,7 @@ private:
int rxFrames;
bool inhibitFilterUpdate;
bool useHex;
bool useColorsByCanId;
bool allowCapture;
bool ignoreDBCColors;
bool CSVAbsTime;
@@ -213,6 +216,7 @@ private:
//private methods
QString getSignalNameFromPosition(QPoint pos);
uint32_t getMessageIDFromPosition(QPoint pos);
void copySelection();
void handleSaveDecodedMethod(bool csv);
void saveDecodedTextFile(QString);
void saveDecodedTextFileAsColumns(QString);
+13
View File
@@ -0,0 +1,13 @@
# Scripts
To run this scripts, first you must authorize your terminal to run:
```bash
chmod +x script/*.sh
```
Then you can run by using this command:
```bash
./scripts/the/script/you/want/to/run
```
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
PROJECT_ROOT="$(pwd)"
# To add this tool in Qt Creator:
# Category: Linguist
# Name: Release Translations (lrelease)
# Executable: lrelease
# Arguments: SavvyCAN.pro
# Working Directory: %{ActiveProject:Path}
echo "Generating .qm files..."
lrelease -verbose "$PROJECT_ROOT"/SavvyCAN.pro
echo ".qm files generated."
+16
View File
@@ -0,0 +1,16 @@
#!/bin/bash
PROJECT_ROOT="$(pwd)"
# To add this tool in Qt Creator:
# Category: Linguist
# Name: Update Translations (lupdate)
# Executable: lupdate
# Arguments: -project SavvyCAN.pro
# Working Directory: %{ActiveProject:Path}
echo "Updating translation files..."
lupdate -verbose "$PROJECT_ROOT"/SavvyCAN.pro
echo "Translation files updated."
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+47
View File
@@ -0,0 +1,47 @@
<!DOCTYPE QPH>
<QPH sourcelanguage="en" language="pt_BR">
<phrase>
<source>New</source>
<target>Novo</target>
</phrase>
<phrase>
<source>Save</source>
<target>Salvar</target>
</phrase>
<phrase>
<source>Cancel</source>
<target>Cancelar</target>
</phrase>
<phrase>
<source>Connect</source>
<target>Conectar</target>
</phrase>
<phrase>
<source>Disconnect</source>
<target>Desconectar</target>
</phrase>
<phrase>
<source>Yes</source>
<target>Sim</target>
</phrase>
<phrase>
<source>No</source>
<target>Não</target>
</phrase>
<phrase>
<source>Bus</source>
<target>Barramento</target>
</phrase>
<phrase>
<source>Frame</source>
<target>Frame</target>
</phrase>
<phrase>
<source>Frames</source>
<target>Frames</target>
</phrase>
<phrase>
<source>Scan</source>
<target>Varredura</target>
</phrase>
</QPH>
+21
View File
@@ -53,6 +53,20 @@
</item>
</layout>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout_8">
<item>
<widget class="QLabel" name="label_13">
<property name="text">
<string>Language</string>
</property>
</widget>
</item>
<item>
<widget class="QComboBox" name="comboLanguage"/>
</item>
</layout>
</item>
</layout>
</widget>
</item>
@@ -97,6 +111,13 @@
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbColorsByCanId">
<property name="text">
<string>Color rows by CAN ID</string>
</property>
</widget>
</item>
<item>
<widget class="QCheckBox" name="cbUseOpenGL">
<property name="text">
+70
View File
@@ -10,6 +10,9 @@
#include <QRect>
#include <QComboBox>
#include <QStandardItemModel>
#include <QColor>
#include <QString>
#include <array>
//#include <QDesktopWidget>
enum TimeStyle
@@ -20,6 +23,44 @@ enum TimeStyle
TS_CLOCK
};
// 32 dark, saturated colors — all designed for readability on white backgrounds.
// Minimum contrast ratio ≥ 4.5:1 against white (WCAG AA).
static constexpr std::array<QRgb, 32> kRowPalette = {{
0xFFC62828, // 0 Red 800
0xFFE53935, // 1 Red 600
0xFFD84315, // 2 Deep Orange 800
0xFFEF6C00, // 3 Orange 800
0xFFF9A825, // 4 Amber 800
0xFF558B2F, // 5 Green 800
0xFF2E7D32, // 6 Green 900
0xFF00695C, // 7 Teal 800
0xFF00838F, // 8 Cyan 800
0xFF0277BD, // 9 Blue 800
0xFF1565C0, // 10 Blue 900
0xFF283593, // 11 Indigo 900
0xFF4527A0, // 12 Deep Purple 800
0xFF6A1B9A, // 13 Purple 900
0xFF880E4F, // 14 Pink 900
0xFFAD1457, // 15 Pink 800
0xFF37474F, // 16 Blue Grey 800
0xFF4E342E, // 17 Brown 800
0xFF827717, // 18 Lime 900
0xFF1B5E20, // 19 Green 900 alt
0xFF006064, // 20 Cyan 900
0xFF01579B, // 21 Light Blue 900
0xFF0D47A1, // 22 Blue 900 alt
0xFF311B92, // 23 Deep Purple 900
0xFF4A148C, // 24 Purple 900 alt
0xFFB71C1C, // 25 Red 900
0xFFBF360C, // 26 Deep Orange 900
0xFFE65100, // 27 Orange 900
0xFF33691E, // 28 Light Green 900
0xFF004D40, // 29 Teal 900
0xFF263238, // 30 Blue Grey 900
0xFF5D4037, // 31 Brown 700
}};
class Utility
{
public:
@@ -344,6 +385,35 @@ public:
return result;
}
// FNV-1a 32-bit hash — fast, good distribution, no dependencies.
static quint32 hashString(const QString &s)
{
quint32 hash = 2166136261u;
for (const QChar ch : s) {
hash ^= static_cast<quint32>(ch.unicode());
hash *= 16777619u;
}
return hash;
}
// Returns a background QColor deterministically chosen from the palette.
// The same string always maps to the same color.
static QColor colorForString(const QString &s)
{
const quint32 idx = hashString(s) % static_cast<quint32>(kRowPalette.size());
// Use a light tint (alpha ~18%) as the row background so text stays readable.
QColor base(kRowPalette[idx]);
base.setAlpha(46); // ~18% — comfortable on white, still clearly tinted
return base;
}
// Opaque variant — useful for badges, tags, or legend swatches.
static QColor solidColorForString(const QString &s)
{
const quint32 idx = hashString(s) % static_cast<quint32>(kRowPalette.size());
return QColor(kRowPalette[idx]);
}
};
#endif // UTILITY_H