diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index a735dc438e..f12888b792 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -32,7 +32,7 @@ cabana_env['QT_MOCHPREFIX'] = os.path.dirname(prev_moc_path) + '/cabana/moc_' cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/pandastream.cc', 'streams/devicestream.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'historylog.cc', 'videowidget.cc', 'signalview.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc', 'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc', - 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) + 'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('cabana', ['cabana.cc', cabana_lib, assets], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if GetOption('test'): diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 2954824fd4..7b2d23a3f9 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -19,6 +19,7 @@ #include "tools/cabana/commands.h" #include "tools/cabana/streamselector.h" #include "tools/cabana/streams/replaystream.h" +#include "tools/cabana/tools/findsignal.h" static MainWindow *main_win = nullptr; void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { @@ -142,6 +143,7 @@ void MainWindow::createActions() { QMenu *tools_menu = menuBar()->addMenu(tr("&Tools")); tools_menu->addAction(tr("Find &Similar Bits"), this, &MainWindow::findSimilarBits); + tools_menu->addAction(tr("&Find Signal"), this, &MainWindow::findSignal); QMenu *help_menu = menuBar()->addMenu(tr("&Help")); help_menu->addAction(tr("Help"), this, &MainWindow::onlineHelp)->setShortcuts(QKeySequence::HelpContents); @@ -592,6 +594,12 @@ void MainWindow::findSimilarBits() { dlg->show(); } +void MainWindow::findSignal() { + FindSignalDlg *dlg = new FindSignalDlg(this); + QObject::connect(dlg, &FindSignalDlg::openMessage, messages_widget, &MessagesWidget::selectMessage); + dlg->show(); +} + void MainWindow::onlineHelp() { if (auto help = findChild()) { help->close(); diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index 0782c316bb..9db999e6c9 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -60,6 +60,7 @@ protected: void updateDownloadProgress(uint64_t cur, uint64_t total, bool success); void setOption(); void findSimilarBits(); + void findSignal(); void undoStackCleanChanged(bool clean); void undoStackIndexChanged(int index); void onlineHelp(); diff --git a/tools/cabana/tools/findsignal.cc b/tools/cabana/tools/findsignal.cc new file mode 100644 index 0000000000..30fa5b4040 --- /dev/null +++ b/tools/cabana/tools/findsignal.cc @@ -0,0 +1,217 @@ +#include "tools/cabana/tools/findsignal.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +// FindSignalModel + +QVariant FindSignalModel::headerData(int section, Qt::Orientation orientation, int role) const { + static QString titles[] = {"Id", "Start Bit, size", "(time, value)"}; + if (role != Qt::DisplayRole) return {}; + return orientation == Qt::Horizontal ? titles[section] : QString::number(section + 1); +} + +QVariant FindSignalModel::data(const QModelIndex &index, int role) const { + if (role == Qt::DisplayRole) { + const auto &s = filtered_signals[index.row()]; + switch (index.column()) { + case 0: return s.id.toString(); + case 1: return QString("%1, %2").arg(s.sig.start_bit).arg(s.sig.size); + case 2: return s.values.join(" "); + } + } + return {}; +} + +void FindSignalModel::search(std::function cmp) { + beginResetModel(); + + std::mutex lock; + const auto prev_sigs = !histories.isEmpty() ? histories.back() : initial_signals; + filtered_signals.clear(); + filtered_signals.reserve(prev_sigs.size()); + QtConcurrent::blockingMap(prev_sigs, [&](auto &s) { + const auto &events = can->events(s.id); + auto first = std::upper_bound(events.cbegin(), events.cend(), s.mono_time, [](uint64_t ts, auto &e) { return ts < e->mono_time; }); + auto it = std::find_if(first, events.cend(), [&](auto e) { return cmp(get_raw_value(e->dat, e->size, s.sig)); }); + if (it != events.cend()) { + auto values = s.values; + values += QString("(%1, %2)").arg((*it)->mono_time / 1e9 - can->routeStartTime(), 0, 'f', 2).arg(get_raw_value((*it)->dat, (*it)->size, s.sig)); + std::lock_guard lk(lock); + filtered_signals.push_back({.id = s.id, .mono_time = (*it)->mono_time, .sig = s.sig, .values = values}); + } + }); + histories.push_back(filtered_signals); + + endResetModel(); +} + +void FindSignalModel::undo() { + if (!histories.isEmpty()) { + beginResetModel(); + histories.pop_back(); + filtered_signals.clear(); + if (!histories.isEmpty()) filtered_signals = histories.back(); + endResetModel(); + } +} + +void FindSignalModel::reset() { + beginResetModel(); + histories.clear(); + filtered_signals.clear(); + initial_signals.clear(); + endResetModel(); +} + +// FindSignalDlg +FindSignalDlg::FindSignalDlg(QWidget *parent) : QDialog(parent, Qt::WindowFlags() | Qt::Window) { + setWindowTitle(tr("Find Signal")); + setAttribute(Qt::WA_DeleteOnClose); + QVBoxLayout *main_layout = new QVBoxLayout(this); + + properties_group = new QGroupBox(tr("Signal Properties")); + QFormLayout *property_layout = new QFormLayout(properties_group); + property_layout->setFieldGrowthPolicy(QFormLayout::FieldsStayAtSizeHint); + + QHBoxLayout *hlayout = new QHBoxLayout(); + hlayout->addWidget(min_size = new QSpinBox); + hlayout->addWidget(new QLabel("-")); + hlayout->addWidget(max_size = new QSpinBox); + hlayout->addWidget(litter_endian = new QCheckBox(tr("Little endian"))); + hlayout->addWidget(is_signed = new QCheckBox(tr("Signed"))); + hlayout->addStretch(0); + min_size->setRange(1, 64); + max_size->setRange(1, 64); + litter_endian->setChecked(true); + property_layout->addRow(tr("Size"), hlayout); + property_layout->addRow(tr("Factor"), factor_edit = new QLineEdit("1.0")); + property_layout->addRow(tr("Offset"), offset_edit = new QLineEdit("0.0")); + + QGroupBox *find_group = new QGroupBox(tr("Find signal"), this); + QVBoxLayout *vlayout = new QVBoxLayout(find_group); + hlayout = new QHBoxLayout(); + hlayout->addWidget(new QLabel(tr("Value"))); + hlayout->addWidget(compare_cb = new QComboBox(this)); + hlayout->addWidget(value1 = new QLineEdit); + hlayout->addWidget(to_label = new QLabel("-")); + hlayout->addWidget(value2 = new QLineEdit); + hlayout->addWidget(undo_btn = new QPushButton(tr("Undo prev find"), this)); + hlayout->addWidget(search_btn = new QPushButton(tr("Find"))); + hlayout->addWidget(reset_btn = new QPushButton(tr("Reset"), this)); + vlayout->addLayout(hlayout); + + compare_cb->addItems({"=", ">", ">=", "!=", "<", "<=", "between"}); + value2->setVisible(false); + to_label->setVisible(false); + undo_btn->setEnabled(false); + reset_btn->setEnabled(false); + + auto double_validator = new QDoubleValidator(this); + double_validator->setLocale(QLocale::C); + for (auto edit : {value1, value2, factor_edit, offset_edit}) { + edit->setValidator(double_validator); + } + + vlayout->addWidget(view = new QTableView(this)); + vlayout->addWidget(stats_label = new QLabel()); + view->setContextMenuPolicy(Qt::CustomContextMenu); + view->horizontalHeader()->setStretchLastSection(true); + view->horizontalHeader()->setSelectionMode(QAbstractItemView::NoSelection); + view->setSelectionBehavior(QAbstractItemView::SelectRows); + view->setModel(model = new FindSignalModel(this)); + + main_layout->addWidget(properties_group); + main_layout->addWidget(find_group); + + setMinimumSize({700, 550}); + QObject::connect(search_btn, &QPushButton::clicked, this, &FindSignalDlg::search); + QObject::connect(undo_btn, &QPushButton::clicked, model, &FindSignalModel::undo); + QObject::connect(model, &QAbstractItemModel::modelReset, this, &FindSignalDlg::modelReset); + QObject::connect(reset_btn, &QPushButton::clicked, model, &FindSignalModel::reset); + QObject::connect(view, &QTableView::customContextMenuRequested, this, &FindSignalDlg::customMenuRequested); + QObject::connect(view, &QTableView::doubleClicked, [this](const QModelIndex &index) { + if (index.isValid()) emit openMessage(model->filtered_signals[index.row()].id); + }); + QObject::connect(compare_cb, qOverload(&QComboBox::currentIndexChanged), [=](int index) { + to_label->setVisible(index == compare_cb->count() - 1); + value2->setVisible(index == compare_cb->count() - 1); + }); +} + +void FindSignalDlg::search() { + if (model->histories.isEmpty()) { + setInitialSignals(); + } + auto v1 = value1->text().toDouble(); + auto v2 = value2->text().toDouble(); + std::function cmp = nullptr; + switch (compare_cb->currentIndex()) { + case 0: cmp = [v1](double v) { return v == v1;}; break; + case 1: cmp = [v1](double v) { return v > v1;};break; + case 2: cmp = [v1](double v) { return v >= v1;};break; + case 3: cmp = [v1](double v) { return v != v1;}; break; + case 4: cmp = [v1](double v) { return v < v1;}; break; + case 5: cmp = [v1](double v) { return v <= v1;}; break; + case 6: cmp = [v1, v2](double v) { return v >= v1 && v <= v2;}; break; + } + properties_group->setEnabled(false); + search_btn->setEnabled(false); + stats_label->setVisible(false); + search_btn->setText("Finding ...."); + QTimer::singleShot(0, [=]() { model->search(cmp); }); +} + +void FindSignalDlg::setInitialSignals() { + cabana::Signal sig{}; + sig.is_little_endian = litter_endian->isChecked(); + sig.is_signed = is_signed->isChecked(); + sig.factor = factor_edit->text().toDouble(); + sig.offset = offset_edit->text().toDouble(); + + model->initial_signals.clear(); + for (auto it = can->last_msgs.cbegin(); it != can->last_msgs.cend(); ++it) { + const int total_size = it.value().dat.size() * 8; + for (int size = min_size->value(); size <= max_size->value(); ++size) { + for (int start = 0; start <= total_size - size; ++start) { + FindSignalModel::SearchSignal s{.id = it.key(), .mono_time = 0, .sig = sig}; + updateSigSizeParamsFromRange(s.sig, start, size); + model->initial_signals.push_back(s); + } + } + } +} + +void FindSignalDlg::modelReset() { + properties_group->setEnabled(model->histories.isEmpty()); + search_btn->setText(model->histories.isEmpty() ? tr("Find") : tr("Find Next")); + reset_btn->setEnabled(!model->histories.isEmpty()); + undo_btn->setEnabled(model->histories.size() > 1); + search_btn->setEnabled(model->rowCount() > 0 || model->histories.isEmpty()); + stats_label->setVisible(true); + stats_label->setText(tr("%1 matches. right click on an item to create signal. double click to open message").arg(model->filtered_signals.size())); +} + +void FindSignalDlg::customMenuRequested(const QPoint &pos) { + if (auto index = view->indexAt(pos); index.isValid()) { + QMenu menu(this); + menu.addAction(tr("Create Signal")); + if (menu.exec(view->mapToGlobal(pos))) { + auto &s = model->filtered_signals[index.row()]; + auto msg = dbc()->msg(s.id); + if (!msg) { + UndoStack::push(new EditMsgCommand(s.id, dbc()->newMsgName(s.id), can->lastMessage(s.id).dat.size())); + msg = dbc()->msg(s.id); + } + s.sig.name = dbc()->newSignalName(s.id); + UndoStack::push(new AddSigCommand(s.id, s.sig)); + emit openMessage(s.id); + } + } +} diff --git a/tools/cabana/tools/findsignal.h b/tools/cabana/tools/findsignal.h new file mode 100644 index 0000000000..217abd82dc --- /dev/null +++ b/tools/cabana/tools/findsignal.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include +#include + +#include "tools/cabana/commands.h" +#include "tools/cabana/settings.h" + +class FindSignalModel : public QAbstractTableModel { +public: + struct SearchSignal { + MessageId id = {}; + uint64_t mono_time = 0; + cabana::Signal sig = {}; + QStringList values; + }; + + FindSignalModel(QObject *parent) : QAbstractTableModel(parent) {} + QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + int columnCount(const QModelIndex &parent = QModelIndex()) const override { return 3; } + int rowCount(const QModelIndex &parent = QModelIndex()) const override { return std::min(filtered_signals.size(), 200); } + void search(std::function cmp); + void reset(); + void undo(); + + QList filtered_signals; + QList initial_signals; + QList> histories; +}; + +class FindSignalDlg : public QDialog { + Q_OBJECT +public: + FindSignalDlg(QWidget *parent); + +signals: + void openMessage(const MessageId &id); + +private: + void search(); + void modelReset(); + void setInitialSignals(); + void customMenuRequested(const QPoint &pos); + + QLineEdit *value1, *value2, *factor_edit, *offset_edit; + QComboBox *compare_cb; + QSpinBox *min_size, *max_size; + QCheckBox *litter_endian, *is_signed; + QPushButton *search_btn, *reset_btn, *undo_btn; + QGroupBox *properties_group; + QTableView *view; + QLabel *to_label, *stats_label; + FindSignalModel *model; +};