diff --git a/tools/cabana/SConscript b/tools/cabana/SConscript index 4acbc4fa66..a735dc438e 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', 'route.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'], 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/cabana.cc b/tools/cabana/cabana.cc index 3556321286..6e354da315 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -4,7 +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/streamselector.h" #include "tools/cabana/streams/devicestream.h" #include "tools/cabana/streams/pandastream.h" #include "tools/cabana/streams/replaystream.h" @@ -33,6 +33,8 @@ int main(int argc, char *argv[]) { cmd_parser.addOption({"dbc", "dbc file to open", "dbc"}); cmd_parser.process(app); + QString dbc_file = cmd_parser.isSet("dbc") ? cmd_parser.value("dbc") : ""; + std::unique_ptr op_prefix; std::unique_ptr stream; @@ -45,10 +47,6 @@ int main(int argc, char *argv[]) { } stream.reset(new PandaStream(&app, config)); } else { - // TODO: Remove when OpenpilotPrefix supports ZMQ -#ifndef __APPLE__ - op_prefix.reset(new OpenpilotPrefix()); -#endif uint32_t replay_flags = REPLAY_FLAG_NONE; if (cmd_parser.isSet("ecam")) { replay_flags |= REPLAY_FLAG_ECAM; @@ -66,22 +64,35 @@ int main(int argc, char *argv[]) { route = DEMO_ROUTE; } - auto replay_stream = new ReplayStream(&app); - stream.reset(replay_stream); if (route.isEmpty()) { - if (OpenRouteDialog dlg(nullptr); !dlg.exec()) { + AbstractStream *out_stream = nullptr; + StreamSelector dlg; + dlg.addStreamWidget(ReplayStream::widget(&out_stream)); + dlg.addStreamWidget(PandaStream::widget(&out_stream)); + dlg.addStreamWidget(DeviceStream::widget(&out_stream)); + if (!dlg.exec()) { + return 0; + } + dbc_file = dlg.dbcFile(); + stream.reset(out_stream); + } else { + // TODO: Remove when OpenpilotPrefix supports ZMQ +#ifndef __APPLE__ + op_prefix.reset(new OpenpilotPrefix()); +#endif + auto replay_stream = new ReplayStream(&app); + stream.reset(replay_stream); + if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) { return 0; } - } else if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) { - return 0; } } MainWindow w; // Load DBC - if (cmd_parser.isSet("dbc")) { - w.loadFile(cmd_parser.value("dbc")); + if (!dbc_file.isEmpty()) { + w.loadFile(dbc_file); } w.show(); diff --git a/tools/cabana/mainwin.cc b/tools/cabana/mainwin.cc index 35e067316b..05545a17fc 100644 --- a/tools/cabana/mainwin.cc +++ b/tools/cabana/mainwin.cc @@ -17,7 +17,8 @@ #include #include "tools/cabana/commands.h" -#include "tools/cabana/route.h" +#include "tools/cabana/streamselector.h" +#include "tools/cabana/streams/replaystream.h" static MainWindow *main_win = nullptr; void qLogMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &msg) { @@ -248,12 +249,13 @@ void MainWindow::DBCFileChanged() { } void MainWindow::openRoute() { - OpenRouteDialog dlg(this); + StreamSelector dlg(this); + dlg.addStreamWidget(ReplayStream::widget(&can)); if (dlg.exec()) { center_widget->clear(); charts_widget->removeAll(); statusBar()->showMessage(tr("Route %1 loaded").arg(can->routeName()), 2000); - } else if (dlg.failedToLoad()) { + } else if (dlg.failed()) { close(); } } diff --git a/tools/cabana/route.cc b/tools/cabana/route.cc deleted file mode 100644 index f71c8dc67b..0000000000 --- a/tools/cabana/route.cc +++ /dev/null @@ -1,75 +0,0 @@ -#include "tools/cabana/route.h" - -#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 - QGridLayout *grid_layout = new QGridLayout(); - grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); - grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); - route_edit->setPlaceholderText(tr("Enter remote route name or click browse to select a local route")); - auto file_btn = new QPushButton(tr("Browse..."), this); - grid_layout->addWidget(file_btn, 0, 2); - - grid_layout->addWidget(new QLabel(tr("Video")), 1, 0); - grid_layout->addWidget(choose_video_cb = new QComboBox(this), 1, 1); - QString items[] = {tr("No Video"), tr("Road Camera"), tr("Wide Road Camera"), tr("Driver Camera"), tr("QCamera")}; - for (int i = 0; i < std::size(items); ++i) { - choose_video_cb->addItem(items[i]); - } - choose_video_cb->setCurrentIndex(1); // default is road camera; - - btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); - btn_box->button(QDialogButtonBox::Open)->setEnabled(false); - - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->addLayout(grid_layout); - main_layout->addWidget(btn_box); - main_layout->addStretch(0); - 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 { - uint32_t flags[] = {REPLAY_FLAG_NO_VIPC, REPLAY_FLAG_NONE, REPLAY_FLAG_ECAM, REPLAY_FLAG_DCAM, REPLAY_FLAG_QCAMERA}; - failed_to_load = !dynamic_cast(can)->loadRoute(route, data_dir, flags[choose_video_cb->currentIndex()]); - 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 deleted file mode 100644 index d13897d756..0000000000 --- a/tools/cabana/route.h +++ /dev/null @@ -1,21 +0,0 @@ -#pragma once - -#include -#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; - QComboBox *choose_video_cb; - QDialogButtonBox *btn_box; - bool failed_to_load = false; -}; diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 18d4f2ffc1..19ab3b01e9 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -89,5 +89,16 @@ protected: std::deque> memory_blocks; }; +class AbstractOpenStreamWidget : public QWidget { + Q_OBJECT +public: + AbstractOpenStreamWidget(AbstractStream **stream, QWidget *parent = nullptr) : stream(stream), QWidget(parent) {} + virtual bool open() = 0; + virtual QString title() = 0; + +protected: + AbstractStream **stream = nullptr; +}; + // A global pointer referring to the unique AbstractStream object extern AbstractStream *can; diff --git a/tools/cabana/streams/devicestream.cc b/tools/cabana/streams/devicestream.cc index d0efac6aa3..24fbaf502c 100644 --- a/tools/cabana/streams/devicestream.cc +++ b/tools/cabana/streams/devicestream.cc @@ -1,5 +1,13 @@ #include "tools/cabana/streams/devicestream.h" +#include +#include +#include +#include +#include + +// DeviceStream + DeviceStream::DeviceStream(QObject *parent, QString address) : zmq_address(address), LiveStream(parent) { startStreamThread(); } @@ -26,3 +34,39 @@ void DeviceStream::streamThread() { handleEvent(messages.emplace_back(msg).event); } } + +AbstractOpenStreamWidget *DeviceStream::widget(AbstractStream **stream) { + return new OpenDeviceWidget(stream); +} + +// OpenDeviceWidget + +OpenDeviceWidget::OpenDeviceWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { + QRadioButton *msgq = new QRadioButton(tr("MSGQ")); + QRadioButton *zmq = new QRadioButton(tr("ZMQ")); + ip_address = new QLineEdit(this); + ip_address->setPlaceholderText(tr("Enter device Ip Address")); + QString ip_range = "(?:[0-1]?[0-9]?[0-9]|2[0-4][0-9]|25[0-5])"; + QString pattern("^" + ip_range + "\\." + ip_range + "\\." + ip_range + "\\." + ip_range + "$"); + QRegularExpression re(pattern); + ip_address->setValidator(new QRegularExpressionValidator(re, this)); + + group = new QButtonGroup(this); + group->addButton(msgq, 0); + group->addButton(zmq, 1); + + QFormLayout *form_layout = new QFormLayout(this); + form_layout->addRow(msgq); + form_layout->addRow(zmq, ip_address); + QObject::connect(group, qOverload(&QButtonGroup::buttonToggled), [=](QAbstractButton *button, bool checked) { + ip_address->setEnabled(button == zmq && checked); + }); + zmq->setChecked(true); +} + +bool OpenDeviceWidget::open() { + QString ip = ip_address->text().isEmpty() ? "127.0.0.1" : ip_address->text(); + bool msgq = group->checkedId() == 0; + *stream = new DeviceStream(qApp, msgq ? "" : ip); + return true; +} diff --git a/tools/cabana/streams/devicestream.h b/tools/cabana/streams/devicestream.h index 715dcd17e0..a65f073458 100644 --- a/tools/cabana/streams/devicestream.h +++ b/tools/cabana/streams/devicestream.h @@ -6,7 +6,7 @@ class DeviceStream : public LiveStream { Q_OBJECT public: DeviceStream(QObject *parent, QString address = {}); - + static AbstractOpenStreamWidget *widget(AbstractStream **stream); inline QString routeName() const override { return QString("Live Streaming From %1").arg(zmq_address.isEmpty() ? "127.0.0.1" : zmq_address); } @@ -15,3 +15,16 @@ protected: void streamThread() override; const QString zmq_address; }; + +class OpenDeviceWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenDeviceWidget(AbstractStream **stream); + bool open() override; + QString title() override { return tr("&Device"); } + +private: + QLineEdit *ip_address; + QButtonGroup *group; +}; diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc index ce50671b20..ee3197ff75 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/tools/cabana/streams/pandastream.cc @@ -1,5 +1,9 @@ #include "tools/cabana/streams/pandastream.h" +#include +#include +#include + PandaStream::PandaStream(QObject *parent, PandaStreamConfig config_) : config(config_), LiveStream(parent) { if (config.serial.isEmpty()) { auto serials = Panda::list(); @@ -84,3 +88,32 @@ void PandaStream::streamThread() { panda->send_heartbeat(false); } } + +AbstractOpenStreamWidget *PandaStream::widget(AbstractStream **stream) { + return new OpenPandaWidget(stream); +} + +// OpenPandaWidget + +OpenPandaWidget::OpenPandaWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->addStretch(1); + + QFormLayout *form_layout = new QFormLayout(); + form_layout->addRow(tr("Serial"), serial_edit = new QLineEdit(this)); + serial_edit->setPlaceholderText(tr("Leave empty to use default serial")); + + main_layout->addLayout(form_layout); + main_layout->addStretch(1); +} + +bool OpenPandaWidget::open() { + try { + PandaStreamConfig config = {.serial = serial_edit->text()}; + *stream = new PandaStream(qApp, config); + } catch (std::exception &e) { + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to connect to panda: '%1'").arg(e.what())); + return false; + } + return true; +} diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h index fe90de2068..df7ccded51 100644 --- a/tools/cabana/streams/pandastream.h +++ b/tools/cabana/streams/pandastream.h @@ -18,7 +18,7 @@ class PandaStream : public LiveStream { Q_OBJECT public: PandaStream(QObject *parent, PandaStreamConfig config_ = {}); - + static AbstractOpenStreamWidget *widget(AbstractStream **stream); inline QString routeName() const override { return QString("Live Streaming From Panda %1").arg(config.serial); } @@ -31,3 +31,14 @@ protected: PandaStreamConfig config = {}; }; +class OpenPandaWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenPandaWidget(AbstractStream **stream); + bool open() override; + QString title() override { return tr("&Panda"); } + +private: + QLineEdit *serial_edit; +}; diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 409ba25841..625b002ee8 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -1,5 +1,13 @@ #include "tools/cabana/streams/replaystream.h" +#include +#include +#include +#include +#include + +#include "common/prefix.h" + ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent, false) { QObject::connect(&settings, &Settings::changed, [this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); @@ -50,3 +58,74 @@ void ReplayStream::pause(bool pause) { replay->pause(pause); emit(pause ? paused() : resume()); } + + +AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) { + return new OpenReplayWidget(stream); +} + +// OpenReplayWidget + +static std::unique_ptr op_prefix; + +OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { + // TODO: get route list from api.comma.ai + QGridLayout *grid_layout = new QGridLayout(); + grid_layout->addWidget(new QLabel(tr("Route")), 0, 0); + grid_layout->addWidget(route_edit = new QLineEdit(this), 0, 1); + route_edit->setPlaceholderText(tr("Enter remote route name or click browse to select a local route")); + auto file_btn = new QPushButton(tr("Browse..."), this); + grid_layout->addWidget(file_btn, 0, 2); + + grid_layout->addWidget(new QLabel(tr("Video")), 1, 0); + grid_layout->addWidget(choose_video_cb = new QComboBox(this), 1, 1); + QString items[] = {tr("No Video"), tr("Road Camera"), tr("Wide Road Camera"), tr("Driver Camera"), tr("QCamera")}; + for (int i = 0; i < std::size(items); ++i) { + choose_video_cb->addItem(items[i]); + } + choose_video_cb->setCurrentIndex(1); // default is road camera; + + QVBoxLayout *main_layout = new QVBoxLayout(this); + main_layout->addLayout(grid_layout); + setMinimumWidth(550); + + 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(); + } + }); +} + +bool OpenReplayWidget::open() { + 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 ret = false; + 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 { + // TODO: Remove when OpenpilotPrefix supports ZMQ +#ifndef __APPLE__ + op_prefix.reset(new OpenpilotPrefix()); +#endif + uint32_t flags[] = {REPLAY_FLAG_NO_VIPC, REPLAY_FLAG_NONE, REPLAY_FLAG_ECAM, REPLAY_FLAG_DCAM, REPLAY_FLAG_QCAMERA}; + ReplayStream *replay_stream = *stream ? (ReplayStream *)*stream : new ReplayStream(qApp); + ret = replay_stream->loadRoute(route, data_dir, flags[choose_video_cb->currentIndex()]); + if (!ret) { + if (replay_stream != *stream) { + delete replay_stream; + } + QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); + } else { + *stream = replay_stream; + } + } + return ret; +} diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index ec4691f752..d029c62e4b 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -26,9 +26,23 @@ public: void pause(bool pause) override; const std::vector *rawEvents() const override { return replay->events(); } inline const std::vector> getTimeline() override { return replay->getTimeline(); } + static AbstractOpenStreamWidget *widget(AbstractStream **stream); private: void mergeSegments(); std::unique_ptr replay = nullptr; std::set processed_segments; }; + +class OpenReplayWidget : public AbstractOpenStreamWidget { + Q_OBJECT + +public: + OpenReplayWidget(AbstractStream **stream); + bool open() override; + QString title() override { return tr("&Replay"); } + +private: + QLineEdit *route_edit; + QComboBox *choose_video_cb; +}; diff --git a/tools/cabana/streamselector.cc b/tools/cabana/streamselector.cc new file mode 100644 index 0000000000..bfd6ff24d9 --- /dev/null +++ b/tools/cabana/streamselector.cc @@ -0,0 +1,52 @@ +#include "tools/cabana/streamselector.h" + +#include +#include +#include +#include +#include + +StreamSelector::StreamSelector(QWidget *parent) : QDialog(parent) { + setWindowTitle(tr("Open stream")); + QVBoxLayout *main_layout = new QVBoxLayout(this); + + tab = new QTabWidget(this); + tab->setTabBarAutoHide(true); + main_layout->addWidget(tab); + + QHBoxLayout *dbc_layout = new QHBoxLayout(); + dbc_file = new QLineEdit(this); + dbc_file->setReadOnly(true); + dbc_file->setPlaceholderText(tr("Choose a dbc file to open")); + QPushButton *file_btn = new QPushButton(tr("Browse...")); + dbc_layout->addWidget(new QLabel(tr("dbc File"))); + dbc_layout->addWidget(dbc_file); + dbc_layout->addWidget(file_btn); + main_layout->addLayout(dbc_layout); + + QFrame *line = new QFrame(this); + line->setFrameStyle(QFrame::HLine | QFrame::Sunken); + main_layout->addWidget(line); + + auto btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); + main_layout->addWidget(btn_box); + + QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject); + QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() { + success = ((AbstractOpenStreamWidget *)tab->currentWidget())->open(); + if (success) { + accept(); + } + }); + QObject::connect(file_btn, &QPushButton::clicked, [this]() { + QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)"); + if (!fn.isEmpty()) { + dbc_file->setText(fn); + settings.last_dir = QFileInfo(fn).absolutePath(); + } + }); +} + +void StreamSelector::addStreamWidget(AbstractOpenStreamWidget *w) { + tab->addTab(w, w->title()); +} diff --git a/tools/cabana/streamselector.h b/tools/cabana/streamselector.h new file mode 100644 index 0000000000..ecd0e8530c --- /dev/null +++ b/tools/cabana/streamselector.h @@ -0,0 +1,22 @@ +#pragma once + +#include +#include +#include + +#include "tools/cabana/streams/abstractstream.h" + +class StreamSelector : public QDialog { + Q_OBJECT + +public: + StreamSelector(QWidget *parent = nullptr); + void addStreamWidget(AbstractOpenStreamWidget *w); + QString dbcFile() const { return dbc_file->text(); } + inline bool failed() const { return !success; } + +private: + QLineEdit *dbc_file; + QTabWidget *tab; + bool success = true; +};