mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-07-20 20:32:06 +08:00
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
This commit is contained in:
@@ -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":
|
||||
|
||||
+16
-7
@@ -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;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user