diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index e921cd3d00..598f2c592b 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -232,6 +232,7 @@ jobs: ./selfdrive/loggerd/tests/test_logger &&\ ./system/proclogd/tests/test_proclog && \ ./tools/replay/tests/test_replay && \ + ./tools/cabana/tests/test_cabana && \ ./system/camerad/test/ae_gray_test && \ coverage xml" - name: "Upload coverage to Codecov" diff --git a/tools/cabana/.gitignore b/tools/cabana/.gitignore index d7a552eabb..73879ab05d 100644 --- a/tools/cabana/.gitignore +++ b/tools/cabana/.gitignore @@ -4,4 +4,4 @@ moc_* _cabana settings car_fingerprint_to_dbc.json - +tests/_test_cabana diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 4e4e11dbd8..b7321e1f8d 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -18,5 +18,9 @@ cabana_env = qt_env.Clone() prev_moc_path = cabana_env['QT_MOCHPREFIX'] cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_' cabana_env.Execute('./generate_dbc_json.py --out car_fingerprint_to_dbc.json') -cabana_env.Program('_cabana', ['cabana.cc', 'mainwin.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', +cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', 'canmessages.cc', 'messageswidget.cc', 'settings.cc', 'detailwidget.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) +cabana_env.Program('_cabana', ['cabana.cc', cabana_lib], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) + +if GetOption('test'): + cabana_env.Program('tests/_test_cabana', ['tests/test_runner.cc', 'tests/test_cabana.cc', cabana_lib], LIBS=[cabana_libs]) diff --git a/tools/cabana/dbcmanager.cc b/tools/cabana/dbcmanager.cc index c3fbab0349..479b14afec 100644 --- a/tools/cabana/dbcmanager.cc +++ b/tools/cabana/dbcmanager.cc @@ -28,8 +28,25 @@ void DBCManager::open(const QString &name, const QString &content) { emit DBCFileChanged(); } -void save(const QString &dbc_file_name) { - // TODO: save DBC to file +QString DBCManager::generateDBC() { + if (!dbc) return {}; + + QString dbc_string; + for (auto &m : dbc->msgs) { + dbc_string += QString("BO_ %1 %2: %3 XXX\n").arg(m.address).arg(m.name.c_str()).arg(m.size); + for (auto &sig : m.sigs) { + dbc_string += QString(" SG_ %1 : %2|%3@%4%5 (%6,%7) [0|0] \"\" XXX\n") + .arg(sig.name.c_str()) + .arg(sig.start_bit) + .arg(sig.size) + .arg(sig.is_little_endian ? '1' : '0') + .arg(sig.is_signed ? '-' : '+') + .arg(sig.factor, 0, 'g', 20) + .arg(sig.offset); + } + dbc_string += "\n"; + } + return dbc_string; } void DBCManager::updateMsg(const QString &id, const QString &name, uint32_t size) { diff --git a/tools/cabana/dbcmanager.h b/tools/cabana/dbcmanager.h index 9e64c4ed53..913445d44e 100644 --- a/tools/cabana/dbcmanager.h +++ b/tools/cabana/dbcmanager.h @@ -13,8 +13,7 @@ public: void open(const QString &dbc_file_name); void open(const QString &name, const QString &content); - void save(const QString &dbc_file_name); - + QString generateDBC(); void addSignal(const QString &id, const Signal &sig); void updateSignal(const QString &id, const QString &sig_name, const Signal &sig); void removeSignal(const QString &id, const QString &sig_name); @@ -24,6 +23,7 @@ public: inline QString name() const { return dbc_name; } void updateMsg(const QString &id, const QString &name, uint32_t size); + inline const DBC *getDBC() const { return dbc; } inline const Msg *msg(const QString &id) const { return msg(addressFromId(id)); } inline const Msg *msg(uint32_t address) const { auto it = msg_map.find(address); diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 4975061cc5..4d97bba589 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -3,6 +3,8 @@ #include #include #include +#include +#include #include #include #include @@ -69,9 +71,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { QObject::connect(can, &CANMessages::updated, [this]() { model->updateState(); }); QObject::connect(dbc_combo, SIGNAL(activated(const QString &)), SLOT(loadDBCFromName(const QString &))); QObject::connect(load_from_paste, &QPushButton::clicked, this, &MessagesWidget::loadDBCFromPaste); - QObject::connect(save_btn, &QPushButton::clicked, [=]() { - // TODO: save DBC to file - }); + QObject::connect(save_btn, &QPushButton::clicked, this, &MessagesWidget::saveDBC); QObject::connect(table_widget->selectionModel(), &QItemSelectionModel::currentChanged, [=](const QModelIndex ¤t, const QModelIndex &previous) { if (current.isValid()) { emit msgSelectionChanged(current.data(Qt::UserRole).toString()); @@ -79,7 +79,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { }); QFile json_file("./car_fingerprint_to_dbc.json"); - if(json_file.open(QIODevice::ReadOnly)) { + if (json_file.open(QIODevice::ReadOnly)) { fingerprint_to_dbc = QJsonDocument::fromJson(json_file.readAll()); } } @@ -103,14 +103,20 @@ void MessagesWidget::loadDBCFromPaste() { void MessagesWidget::loadDBCFromFingerprint() { auto fingerprint = can->carFingerprint(); - if (!fingerprint.isEmpty() && dbc()->name().isEmpty()) { + if (!fingerprint.isEmpty() && dbc()->name().isEmpty()) { auto dbc_name = fingerprint_to_dbc[fingerprint]; - if (dbc_name != QJsonValue::Undefined) { + if (dbc_name != QJsonValue::Undefined) { loadDBCFromName(dbc_name.toString()); } } } +void MessagesWidget::saveDBC() { + SaveDBCDialog dlg(this); + dlg.dbc_edit->setText(dbc()->generateDBC()); + dlg.exec(); +} + // MessageListModel QVariant MessageListModel::headerData(int section, Qt::Orientation orientation, int role) const { @@ -224,6 +230,8 @@ void MessageListModel::sort(int column, Qt::SortOrder order) { } } +// LoadDBCDialog + LoadDBCDialog::LoadDBCDialog(QWidget *parent) : QDialog(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); dbc_edit = new QTextEdit(this); @@ -233,7 +241,48 @@ LoadDBCDialog::LoadDBCDialog(QWidget *parent) : QDialog(parent) { auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); main_layout->addWidget(buttonBox); - setFixedWidth(640); - connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); - connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + setMinimumSize({640, 480}); + QObject::connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + QObject::connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +// SaveDBCDialog + +SaveDBCDialog::SaveDBCDialog(QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Save DBC")); + QVBoxLayout *main_layout = new QVBoxLayout(this); + dbc_edit = new QTextEdit(this); + dbc_edit->setAcceptRichText(false); + main_layout->addWidget(dbc_edit); + + QPushButton *copy_to_clipboard = new QPushButton(tr("Copy To Clipboard"), this); + QPushButton *save_as = new QPushButton(tr("Save As"), this); + + QHBoxLayout *btn_layout = new QHBoxLayout(); + btn_layout->addStretch(); + btn_layout->addWidget(copy_to_clipboard); + btn_layout->addWidget(save_as); + main_layout->addLayout(btn_layout); + setMinimumSize({640, 480}); + + QObject::connect(copy_to_clipboard, &QPushButton::clicked, this, &SaveDBCDialog::copytoClipboard); + QObject::connect(save_as, &QPushButton::clicked, this, &SaveDBCDialog::saveAs); +} + +void SaveDBCDialog::copytoClipboard() { + dbc_edit->selectAll(); + dbc_edit->copy(); + QDialog::accept(); +} + +void SaveDBCDialog::saveAs() { + QString file_name = QFileDialog::getSaveFileName(this, tr("Save File"), + QDir::homePath() + "/untitled.dbc", tr("DBC (*.dbc)")); + if (!file_name.isEmpty()) { + QFile file(file_name); + if (file.open(QIODevice::WriteOnly)) { + file.write(dbc_edit->toPlainText().toUtf8()); + } + QDialog::accept(); + } } diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index fcd4939dac..a3d4d860b2 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -17,6 +17,16 @@ public: QTextEdit *dbc_edit; }; +class SaveDBCDialog : public QDialog { + Q_OBJECT + +public: + SaveDBCDialog(QWidget *parent); + void copytoClipboard(); + void saveAs(); + QTextEdit *dbc_edit; +}; + class MessageListModel : public QAbstractTableModel { Q_OBJECT @@ -52,6 +62,7 @@ public slots: void loadDBCFromName(const QString &name); void loadDBCFromFingerprint(); void loadDBCFromPaste(); + void saveDBC(); signals: void msgSelectionChanged(const QString &message_id); diff --git a/tools/cabana/tests/test_cabana b/tools/cabana/tests/test_cabana new file mode 100755 index 0000000000..bac242fbdd --- /dev/null +++ b/tools/cabana/tests/test_cabana @@ -0,0 +1,4 @@ +#!/bin/sh +cd "$(dirname "$0")" +export LD_LIBRARY_PATH="../../../opendbc/can:$LD_LIBRARY_PATH" +exec ./_test_cabana "$1" diff --git a/tools/cabana/tests/test_cabana.cc b/tools/cabana/tests/test_cabana.cc new file mode 100644 index 0000000000..d0aa2cbb4f --- /dev/null +++ b/tools/cabana/tests/test_cabana.cc @@ -0,0 +1,35 @@ + +#include "catch2/catch.hpp" +#include "tools/cabana/dbcmanager.h" + +TEST_CASE("DBCManager::generateDBC") { + DBCManager dbc_origin(nullptr); + dbc_origin.open("toyota_new_mc_pt_generated"); + QString dbc_string = dbc_origin.generateDBC(); + + DBCManager dbc_from_generated(nullptr); + dbc_from_generated.open("", dbc_string); + + auto dbc = dbc_origin.getDBC(); + auto new_dbc = dbc_from_generated.getDBC(); + REQUIRE(dbc->msgs.size() == new_dbc->msgs.size()); + for (int i = 0; i < dbc->msgs.size(); ++i) { + REQUIRE(dbc->msgs[i].name == new_dbc->msgs[i].name); + REQUIRE(dbc->msgs[i].address == new_dbc->msgs[i].address); + REQUIRE(dbc->msgs[i].size == new_dbc->msgs[i].size); + REQUIRE(dbc->msgs[i].sigs.size() == new_dbc->msgs[i].sigs.size()); + auto &sig = dbc->msgs[i].sigs; + auto &new_sig = new_dbc->msgs[i].sigs; + for (int j = 0; j < sig.size(); ++j) { + REQUIRE(sig[j].name == new_sig[j].name); + REQUIRE(sig[j].start_bit == new_sig[j].start_bit); + REQUIRE(sig[j].msb == new_sig[j].msb); + REQUIRE(sig[j].lsb == new_sig[j].lsb); + REQUIRE(sig[j].size == new_sig[j].size); + REQUIRE(sig[j].is_signed == new_sig[j].is_signed); + REQUIRE(sig[j].factor == new_sig[j].factor); + REQUIRE(sig[j].offset == new_sig[j].offset); + REQUIRE(sig[j].is_little_endian == new_sig[j].is_little_endian); + } + } +} diff --git a/tools/cabana/tests/test_runner.cc b/tools/cabana/tests/test_runner.cc new file mode 100644 index 0000000000..b20ac86c64 --- /dev/null +++ b/tools/cabana/tests/test_runner.cc @@ -0,0 +1,10 @@ +#define CATCH_CONFIG_RUNNER +#include "catch2/catch.hpp" +#include + +int main(int argc, char **argv) { + // unit tests for Qt + QCoreApplication app(argc, argv); + const int res = Catch::Session().run(argc, argv); + return (res < 0xff ? res : 0xff); +}