cabana: add dialog to open route from remote or local (#27183)

* add OpenRouteDialog

* cleanup

* failed_to_load

* fix load

* clear message list and stream after open new route

* show message

* remove all tabs and charts after open

* use textEdited

* check route format

* cleanup loadRoute
old-commit-hash: 29d9d03759
beeps
Dean Lee 2 years ago committed by GitHub
parent b5b6a6a925
commit 7af5741071
  1. 2
      tools/cabana/SConscript
  2. 23
      tools/cabana/cabana.cc
  3. 4
      tools/cabana/chartswidget.cc
  4. 2
      tools/cabana/chartswidget.h
  5. 10
      tools/cabana/detailwidget.cc
  6. 1
      tools/cabana/detailwidget.h
  7. 17
      tools/cabana/mainwin.cc
  8. 1
      tools/cabana/mainwin.h
  9. 26
      tools/cabana/messageswidget.cc
  10. 4
      tools/cabana/messageswidget.h
  11. 68
      tools/cabana/route.cc
  12. 19
      tools/cabana/route.h
  13. 2
      tools/cabana/settings.cc
  14. 1
      tools/cabana/settings.h
  15. 12
      tools/cabana/streams/replaystream.cc
  16. 7
      tools/cabana/streams/replaystream.h

@ -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":

@ -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<OpenpilotPrefix> op_prefix;
std::unique_ptr<AbstractStream> 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;
}
}

@ -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)));

@ -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();

@ -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;

@ -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:

@ -15,6 +15,7 @@
#include <QWidgetAction>
#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", "");

@ -23,6 +23,7 @@ public:
void loadFile(const QString &fn);
public slots:
void openRoute();
void newFile();
void openFile();
void openRecentFile();

@ -3,7 +3,6 @@
#include <QApplication>
#include <QFontDatabase>
#include <QHBoxLayout>
#include <QLineEdit>
#include <QPainter>
#include <QPushButton>
#include <QVBoxLayout>
@ -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<QString, CanData> *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();
}

@ -2,6 +2,7 @@
#include <QAbstractTableModel>
#include <QHeaderView>
#include <QLineEdit>
#include <QSet>
#include <QStyledItemDelegate>
#include <QTableView>
@ -23,6 +24,7 @@ public:
void sortMessages();
void suppress();
void clearSuppress();
void reset();
QStringList msgs;
QSet<std::pair<QString, int>> 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;

@ -0,0 +1,68 @@
#include "tools/cabana/route.h"
#include <QButtonGroup>
#include <QFileDialog>
#include <QHBoxLayout>
#include <QLabel>
#include <QMessageBox>
#include <QPushButton>
#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<ReplayStream *>(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);
}

@ -0,0 +1,19 @@
#pragma once
#include <QDialogButtonBox>
#include <QLineEdit>
#include <QDialog>
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;
};

@ -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();

@ -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;

@ -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; })) {

@ -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<std::tuple<int, int, TimelineType>> getTimeline() override { return replay->getTimeline(); }
private:
Replay *replay = nullptr;
std::unique_ptr<Replay> replay = nullptr;
uint32_t replay_flags = REPLAY_FLAG_NONE;
};

Loading…
Cancel
Save