diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 0c9ad14973..a9922ba9be 100644 --- a/tools/cabana/SConscript +++ b/tools/cabana/SConscript @@ -21,7 +21,7 @@ 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_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/livestream.cc', 'streams/abstractstream.cc', 'streams/replaystream.cc', 'binaryview.cc', 'chartswidget.cc', 'historylog.cc', 'videowidget.cc', 'signaledit.cc', 'dbcmanager.cc', - 'commands.cc', 'messageswidget.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) + 'commands.cc', 'messageswidget.cc', 'route.cc', 'settings.cc', 'util.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) cabana_env.Program('_cabana', ['cabana.cc', cabana_lib, asset_obj], LIBS=cabana_libs, FRAMEWORKS=base_frameworks) if arch == "Darwin": diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index e34e2d0205..e028e383c2 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -4,6 +4,7 @@ #include "common/prefix.h" #include "selfdrive/ui/qt/util.h" #include "tools/cabana/mainwin.h" +#include "tools/cabana/route.h" #include "tools/cabana/streams/livestream.h" #include "tools/cabana/streams/replaystream.h" @@ -26,10 +27,6 @@ int main(int argc, char *argv[]) { cmd_parser.addOption({"no-vipc", "do not output video"}); cmd_parser.addOption({"dbc", "dbc file to open", "dbc"}); cmd_parser.process(app); - const QStringList args = cmd_parser.positionalArguments(); - if (args.empty() && !cmd_parser.isSet("demo") && !cmd_parser.isSet("stream")) { - cmd_parser.showHelp(); - } std::unique_ptr op_prefix; std::unique_ptr stream; @@ -41,7 +38,6 @@ int main(int argc, char *argv[]) { #ifndef __APPLE__ op_prefix.reset(new OpenpilotPrefix()); #endif - const QString route = args.empty() ? DEMO_ROUTE : args.first(); uint32_t replay_flags = REPLAY_FLAG_NONE; if (cmd_parser.isSet("ecam")) { replay_flags |= REPLAY_FLAG_ECAM; @@ -50,9 +46,22 @@ int main(int argc, char *argv[]) { } else if (cmd_parser.isSet("no-vipc")) { replay_flags |= REPLAY_FLAG_NO_VIPC; } - auto replay_stream = new ReplayStream(&app); + + const QStringList args = cmd_parser.positionalArguments(); + QString route; + if (args.size() > 0) { + route = args.first(); + } else if (cmd_parser.isSet("demo")) { + route = DEMO_ROUTE; + } + + auto replay_stream = new ReplayStream(replay_flags, &app); stream.reset(replay_stream); - if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) { + if (route.isEmpty()) { + if (OpenRouteDialog dlg(nullptr); !dlg.exec()) { + return 0; + } + } else if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"))) { return 0; } } diff --git a/tools/cabana/chartswidget.cc b/tools/cabana/chartswidget.cc index 1b8ce657a6..eb14e5a044 100644 --- a/tools/cabana/chartswidget.cc +++ b/tools/cabana/chartswidget.cc @@ -137,7 +137,7 @@ void ChartsWidget::updateState() { if (!is_zoomed) { double pos = (cur_sec - display_range.first) / max_chart_range; if (pos < 0 || pos > 0.8) { - const double min_event_sec = (can->events()->front()->mono_time / (double)1e9) - can->routeStartTime(); + const double min_event_sec = can->events()->empty() ? 0 : (can->events()->front()->mono_time / (double)1e9 - can->routeStartTime()); display_range.first = std::floor(std::max(min_event_sec, cur_sec - max_chart_range * 0.2)); } display_range.second = std::floor(display_range.first + max_chart_range); @@ -157,7 +157,7 @@ void ChartsWidget::updateState() { void ChartsWidget::setMaxChartRange(int value) { max_chart_range = settings.chart_range = value; double current_sec = can->currentSec(); - const double min_event_sec = (can->events()->front()->mono_time / (double)1e9) - can->routeStartTime(); + const double min_event_sec = can->events()->empty() ? 0 : (can->events()->front()->mono_time / (double)1e9 - can->routeStartTime()); // keep current_sec's pos double pos = (current_sec - display_range.first) / (display_range.second - display_range.first); display_range.first = std::floor(std::max(min_event_sec, current_sec - max_chart_range * (1.0 - pos))); diff --git a/tools/cabana/chartswidget.h b/tools/cabana/chartswidget.h index 9b2afd45a9..15da8e53ad 100644 --- a/tools/cabana/chartswidget.h +++ b/tools/cabana/chartswidget.h @@ -97,6 +97,7 @@ public: public slots: void setColumnCount(int n); + void removeAll(); signals: void dock(bool floating); @@ -114,7 +115,6 @@ private: void zoomIn(double min, double max); void zoomReset(); void updateToolBar(); - void removeAll(); void setMaxChartRange(int value); void updateLayout(); void settingChanged(); diff --git a/tools/cabana/detailwidget.cc b/tools/cabana/detailwidget.cc index 3af0fa9fc3..46f7a148f4 100644 --- a/tools/cabana/detailwidget.cc +++ b/tools/cabana/detailwidget.cc @@ -104,6 +104,16 @@ void DetailWidget::showTabBarContextMenu(const QPoint &pt) { } } +void DetailWidget::removeAll() { + msg_id = ""; + tabbar->blockSignals(true); + while (tabbar->count() > 0) { + tabbar->removeTab(0); + } + tabbar->blockSignals(false); + stacked_layout->setCurrentIndex(0); +} + void DetailWidget::setMessage(const QString &message_id) { msg_id = message_id; int index = tabbar->count() - 1; diff --git a/tools/cabana/detailwidget.h b/tools/cabana/detailwidget.h index 3a3f3adf0e..a62e23f226 100644 --- a/tools/cabana/detailwidget.h +++ b/tools/cabana/detailwidget.h @@ -30,6 +30,7 @@ public: DetailWidget(ChartsWidget *charts, QWidget *parent); void setMessage(const QString &message_id); void refresh(); + void removeAll(); QSize minimumSizeHint() const override { return binary_view->minimumSizeHint(); } private: diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 94cf9840f7..dfd38e267d 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -15,6 +15,7 @@ #include #include "tools/cabana/commands.h" +#include "tools/cabana/route.h" static MainWindow *main_win = nullptr; void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { @@ -66,6 +67,11 @@ MainWindow::MainWindow() : QMainWindow() { void MainWindow::createActions() { QMenu *file_menu = menuBar()->addMenu(tr("&File")); + if (!can->liveStreaming()) { + file_menu->addAction(tr("Open Route..."), this, &MainWindow::openRoute); + file_menu->addSeparator(); + } + file_menu->addAction(tr("New DBC File"), this, &MainWindow::newFile)->setShortcuts(QKeySequence::New); file_menu->addAction(tr("Open DBC File..."), this, &MainWindow::openFile)->setShortcuts(QKeySequence::Open); @@ -185,6 +191,17 @@ void MainWindow::DBCFileChanged() { setWindowFilePath(QString("%1").arg(dbc()->name())); } +void MainWindow::openRoute() { + OpenRouteDialog dlg(this); + if (dlg.exec()) { + detail_widget->removeAll(); + charts_widget->removeAll(); + statusBar()->showMessage(tr("Route %1 loaded").arg(can->routeName()), 2000); + } else if (dlg.failedToLoad()) { + close(); + } +} + void MainWindow::newFile() { remindSaveChanges(); dbc()->open("untitled.dbc", ""); diff --git a/tools/cabana/mainwin.h b/tools/cabana/mainwin.h index c26f6973c0..5e627df58b 100644 --- a/tools/cabana/mainwin.h +++ b/tools/cabana/mainwin.h @@ -23,6 +23,7 @@ public: void loadFile(const QString &fn); public slots: + void openRoute(); void newFile(); void openFile(); void openRecentFile(); diff --git a/tools/cabana/messageswidget.cc b/tools/cabana/messageswidget.cc index 9d0fc23e4d..359fe10f50 100644 --- a/tools/cabana/messageswidget.cc +++ b/tools/cabana/messageswidget.cc @@ -3,7 +3,6 @@ #include #include #include -#include #include #include #include @@ -14,7 +13,7 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); // message filter - QLineEdit *filter = new QLineEdit(this); + filter = new QLineEdit(this); filter->setClearButtonEnabled(true); filter->setPlaceholderText(tr("filter messages")); main_layout->addWidget(filter); @@ -41,8 +40,9 @@ MessagesWidget::MessagesWidget(QWidget *parent) : QWidget(parent) { main_layout->addLayout(suppress_layout); // signals/slots - QObject::connect(filter, &QLineEdit::textChanged, model, &MessageListModel::setFilterString); + QObject::connect(filter, &QLineEdit::textEdited, model, &MessageListModel::setFilterString); QObject::connect(can, &AbstractStream::msgsReceived, model, &MessageListModel::msgsReceived); + QObject::connect(can, &AbstractStream::streamStarted, this, &MessagesWidget::reset); QObject::connect(dbc(), &DBCManager::DBCFileChanged, model, &MessageListModel::sortMessages); QObject::connect(dbc(), &DBCManager::msgUpdated, model, &MessageListModel::sortMessages); QObject::connect(dbc(), &DBCManager::msgRemoved, model, &MessageListModel::sortMessages); @@ -83,6 +83,13 @@ void MessagesWidget::updateSuppressedButtons() { } } +void MessagesWidget::reset() { + model->reset(); + filter->clear(); + current_msg_id = ""; + updateSuppressedButtons(); +} + // MessageListModel @@ -175,10 +182,11 @@ void MessageListModel::sortMessages() { void MessageListModel::msgsReceived(const QHash *new_msgs) { int prev_row_count = msgs.size(); - if (filter_str.isEmpty() && msgs.size() != can->can_msgs.size()) { + bool update_all = new_msgs->size() == can->can_msgs.size(); + if (update_all || (filter_str.isEmpty() && msgs.size() != can->can_msgs.size())) { msgs = can->can_msgs.keys(); } - if (msgs.size() != prev_row_count) { + if (update_all || msgs.size() != prev_row_count) { sortMessages(); return; } @@ -215,3 +223,11 @@ void MessageListModel::suppress() { void MessageListModel::clearSuppress() { suppressed_bytes.clear(); } + +void MessageListModel::reset() { + beginResetModel(); + filter_str = ""; + msgs.clear(); + clearSuppress(); + endResetModel(); +} diff --git a/tools/cabana/messageswidget.h b/tools/cabana/messageswidget.h index 1927faa646..81ee36cd6f 100644 --- a/tools/cabana/messageswidget.h +++ b/tools/cabana/messageswidget.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -23,6 +24,7 @@ public: void sortMessages(); void suppress(); void clearSuppress(); + void reset(); QStringList msgs; QSet> suppressed_bytes; @@ -41,6 +43,7 @@ public: QByteArray saveHeaderState() const { return table_widget->horizontalHeader()->saveState(); } bool restoreHeaderState(const QByteArray &state) const { return table_widget->horizontalHeader()->restoreState(state); } void updateSuppressedButtons(); + void reset(); signals: void msgSelectionChanged(const QString &message_id); @@ -48,6 +51,7 @@ signals: protected: QTableView *table_widget; QString current_msg_id; + QLineEdit *filter; MessageListModel *model; QPushButton *suppress_add; QPushButton *suppress_clear; diff --git a/tools/cabana/route.cc b/tools/cabana/route.cc new file mode 100644 index 0000000000..ab322cdf90 --- /dev/null +++ b/tools/cabana/route.cc @@ -0,0 +1,68 @@ +#include "tools/cabana/route.h" + +#include +#include +#include +#include +#include +#include + +#include "tools/cabana/streams/replaystream.h" + +OpenRouteDialog::OpenRouteDialog(QWidget *parent) : QDialog(parent) { + // TODO: get route list from api.comma.ai + QHBoxLayout *edit_layout = new QHBoxLayout; + edit_layout->addWidget(new QLabel(tr("Route:"))); + edit_layout->addWidget(route_edit = new QLineEdit(this)); + route_edit->setPlaceholderText(tr("Enter remote route name or click browse to select a local route")); + auto file_btn = new QPushButton(tr("Browse..."), this); + edit_layout->addWidget(file_btn); + + btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); + btn_box->button(QDialogButtonBox::Open)->setEnabled(false); + + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->addStretch(0); + main_layout->addLayout(edit_layout); + main_layout->addStretch(0); + main_layout->addWidget(btn_box); + setMinimumSize({550, 120}); + + QObject::connect(btn_box, &QDialogButtonBox::accepted, this, &OpenRouteDialog::loadRoute); + QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(route_edit, &QLineEdit::textChanged, [this]() { + btn_box->button(QDialogButtonBox::Open)->setEnabled(!route_edit->text().isEmpty()); + }); + QObject::connect(file_btn, &QPushButton::clicked, [=]() { + QString dir = QFileDialog::getExistingDirectory(this, tr("Open Local Route"), settings.last_route_dir); + if (!dir.isEmpty()) { + route_edit->setText(dir); + settings.last_route_dir = QFileInfo(dir).absolutePath(); + } + }); +} + +void OpenRouteDialog::loadRoute() { + btn_box->setEnabled(false); + + QString route = route_edit->text(); + QString data_dir; + if (int idx = route.lastIndexOf('/'); idx != -1) { + data_dir = route.mid(0, idx + 1); + route = route.mid(idx + 1); + } + + bool is_valid_format = Route::parseRoute(route).str.size() > 0; + if (!is_valid_format) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Invalid route format: '%1'").arg(route)); + } else { + failed_to_load = !dynamic_cast(can)->loadRoute(route, data_dir); + if (failed_to_load) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); + } else { + accept(); + } + } + + btn_box->setEnabled(true); +} diff --git a/tools/cabana/route.h b/tools/cabana/route.h new file mode 100644 index 0000000000..ceda71d585 --- /dev/null +++ b/tools/cabana/route.h @@ -0,0 +1,19 @@ +#pragma once + +#include +#include +#include + +class OpenRouteDialog : public QDialog { + Q_OBJECT + +public: + OpenRouteDialog(QWidget *parent); + void loadRoute(); + inline bool failedToLoad() const { return failed_to_load; } + +private: + QLineEdit *route_edit; + QDialogButtonBox *btn_box; + bool failed_to_load = false; +}; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index 22c7a941ab..6cbd16cabf 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -20,6 +20,7 @@ void Settings::save() { s.setValue("chart_range", chart_range); s.setValue("chart_column_count", chart_column_count); s.setValue("last_dir", last_dir); + s.setValue("last_route_dir", last_route_dir); s.setValue("window_state", window_state); s.setValue("geometry", geometry); s.setValue("video_splitter_state", video_splitter_state); @@ -36,6 +37,7 @@ void Settings::load() { chart_range = s.value("chart_range", 3 * 60).toInt(); chart_column_count = s.value("chart_column_count", 1).toInt(); last_dir = s.value("last_dir", QDir::homePath()).toString(); + last_route_dir = s.value("last_route_dir", QDir::homePath()).toString(); window_state = s.value("window_state").toByteArray(); geometry = s.value("geometry").toByteArray(); video_splitter_state = s.value("video_splitter_state").toByteArray(); diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index 7abad81c29..a302d20077 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -20,6 +20,7 @@ public: int chart_range = 3 * 60; // e minutes int chart_series_type = 0; QString last_dir; + QString last_route_dir; QByteArray geometry; QByteArray video_splitter_state; QByteArray window_state; diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 72c4a13048..b768b94327 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -2,7 +2,7 @@ #include "tools/cabana/dbcmanager.h" -ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent, false) { +ReplayStream::ReplayStream(uint32_t replay_flags, QObject *parent) : replay_flags(replay_flags), AbstractStream(parent, false) { QObject::connect(&settings, &Settings::changed, [this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); }); @@ -16,13 +16,13 @@ static bool event_filter(const Event *e, void *opaque) { return ((ReplayStream *)opaque)->eventFilter(e); } -bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags) { - replay = new Replay(route, {"can", "roadEncodeIdx", "wideRoadEncodeIdx", "carParams"}, {}, nullptr, replay_flags, data_dir, this); +bool ReplayStream::loadRoute(const QString &route, const QString &data_dir) { + replay.reset(new Replay(route, {"can", "roadEncodeIdx", "wideRoadEncodeIdx", "carParams"}, {}, nullptr, replay_flags, data_dir, this)); replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->installEventFilter(event_filter, this); - QObject::connect(replay, &Replay::seekedTo, this, &AbstractStream::seekedTo); - QObject::connect(replay, &Replay::segmentsMerged, this, &AbstractStream::eventsMerged); - QObject::connect(replay, &Replay::streamStarted, this, &AbstractStream::streamStarted); + QObject::connect(replay.get(), &Replay::seekedTo, this, &AbstractStream::seekedTo); + QObject::connect(replay.get(), &Replay::segmentsMerged, this, &AbstractStream::eventsMerged); + QObject::connect(replay.get(), &Replay::streamStarted, this, &AbstractStream::streamStarted); if (replay->load()) { const auto &segments = replay->route()->segments(); if (std::none_of(segments.begin(), segments.end(), [](auto &s) { return s.second.rlog.length() > 0; })) { diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index a9a74e33b5..69fb738ab8 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -8,9 +8,9 @@ class ReplayStream : public AbstractStream { Q_OBJECT public: - ReplayStream(QObject *parent); + ReplayStream(uint32_t replay_flags, QObject *parent); ~ReplayStream(); - bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE); + bool loadRoute(const QString &route, const QString &data_dir); bool eventFilter(const Event *event); void seekTo(double ts) override { replay->seekTo(std::max(double(0), ts), false); }; inline QString routeName() const override { return replay->route()->name(); } @@ -28,5 +28,6 @@ public: inline const std::vector> getTimeline() override { return replay->getTimeline(); } private: - Replay *replay = nullptr; + std::unique_ptr replay = nullptr; + uint32_t replay_flags = REPLAY_FLAG_NONE; };