mirror of
https://github.com/dragonpilot/dragonpilot.git
synced 2026-08-05 16:25:41 +08:00
cabana: Implement Remote Route Browsing Feature (#32332)
browse remote routes
This commit is contained in:
@@ -26,7 +26,7 @@ cabana_env.Command(assets, assets_src, f"rcc $SOURCES -o $TARGET")
|
||||
cabana_env.Depends(assets, Glob('/assets/*', exclude=[assets, assets_src, "assets/assets.o"]))
|
||||
|
||||
cabana_lib = cabana_env.Library("cabana_lib", ['mainwin.cc', 'streams/socketcanstream.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',
|
||||
'streams/routes.cc', 'dbc/dbc.cc', 'dbc/dbcfile.cc', 'dbc/dbcmanager.cc',
|
||||
'utils/export.cc', 'utils/util.cc',
|
||||
'chart/chartswidget.cc', 'chart/chart.cc', 'chart/signalselector.cc', 'chart/tiplabel.cc', 'chart/sparkline.cc',
|
||||
'commands.cc', 'messageswidget.cc', 'streamselector.cc', 'settings.cc', 'detailwidget.cc', 'tools/findsimilarbits.cc', 'tools/findsignal.cc'], LIBS=cabana_libs, FRAMEWORKS=base_frameworks)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <QPushButton>
|
||||
|
||||
#include "common/timing.h"
|
||||
#include "tools/cabana/streams/routes.h"
|
||||
|
||||
ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent) {
|
||||
unsetenv("ZMQ");
|
||||
@@ -107,29 +108,36 @@ AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) {
|
||||
// OpenReplayWidget
|
||||
|
||||
OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) {
|
||||
// TODO: get route list from api.comma.ai
|
||||
QGridLayout *grid_layout = new QGridLayout(this);
|
||||
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);
|
||||
route_edit->setPlaceholderText(tr("Enter route name or browse for local/remote route"));
|
||||
auto browse_remote_btn = new QPushButton(tr("Remote route..."), this);
|
||||
grid_layout->addWidget(browse_remote_btn, 0, 2);
|
||||
auto browse_local_btn = new QPushButton(tr("Local route..."), this);
|
||||
grid_layout->addWidget(browse_local_btn, 0, 3);
|
||||
|
||||
grid_layout->addWidget(new QLabel(tr("Camera")), 1, 0);
|
||||
QHBoxLayout *camera_layout = new QHBoxLayout();
|
||||
for (auto c : {tr("Road camera"), tr("Driver camera"), tr("Wide road camera")})
|
||||
camera_layout->addWidget(cameras.emplace_back(new QCheckBox(c, this)));
|
||||
cameras[0]->setChecked(true);
|
||||
camera_layout->addStretch(1);
|
||||
grid_layout->addItem(camera_layout, 1, 1);
|
||||
|
||||
setMinimumWidth(550);
|
||||
QObject::connect(file_btn, &QPushButton::clicked, [=]() {
|
||||
QObject::connect(browse_local_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();
|
||||
}
|
||||
});
|
||||
QObject::connect(browse_remote_btn, &QPushButton::clicked, [this]() {
|
||||
RoutesDialog route_dlg(this);
|
||||
if (route_dlg.exec()) {
|
||||
route_edit->setText(route_dlg.route());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool OpenReplayWidget::open() {
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
#include "tools/cabana/streams/routes.h"
|
||||
|
||||
#include <QDateTime>
|
||||
#include <QDialogButtonBox>
|
||||
#include <QFormLayout>
|
||||
#include <QJsonArray>
|
||||
#include <QJsonDocument>
|
||||
#include <QListWidget>
|
||||
#include <QMessageBox>
|
||||
#include <QPainter>
|
||||
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
// The RouteListWidget class extends QListWidget to display a custom message when empty
|
||||
class RouteListWidget : public QListWidget {
|
||||
public:
|
||||
RouteListWidget(QWidget *parent = nullptr) : QListWidget(parent) {}
|
||||
void setEmptyText(const QString &text) {
|
||||
empty_text_ = text;
|
||||
viewport()->update();
|
||||
}
|
||||
void paintEvent(QPaintEvent *event) override {
|
||||
QListWidget::paintEvent(event);
|
||||
if (count() == 0) {
|
||||
QPainter painter(viewport());
|
||||
painter.drawText(viewport()->rect(), Qt::AlignCenter, empty_text_);
|
||||
}
|
||||
}
|
||||
QString empty_text_ = tr("No items");
|
||||
};
|
||||
|
||||
RoutesDialog::RoutesDialog(QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle(tr("Remote routes"));
|
||||
|
||||
QFormLayout *layout = new QFormLayout(this);
|
||||
layout->addRow(tr("Device"), device_list_ = new QComboBox(this));
|
||||
layout->addRow(tr("Duration"), period_selector_ = new QComboBox(this));
|
||||
layout->addRow(route_list_ = new RouteListWidget(this));
|
||||
auto button_box = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
|
||||
layout->addRow(button_box);
|
||||
|
||||
device_list_->addItem(tr("Loading..."));
|
||||
// Populate period selector with predefined durations
|
||||
period_selector_->addItem(tr("Last week"), 7);
|
||||
period_selector_->addItem(tr("Last 2 weeks"), 14);
|
||||
period_selector_->addItem(tr("Last month"), 30);
|
||||
period_selector_->addItem(tr("Last 6 months"), 180);
|
||||
|
||||
// Connect signals and slots
|
||||
connect(device_list_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
|
||||
connect(period_selector_, QOverload<int>::of(&QComboBox::currentIndexChanged), this, &RoutesDialog::fetchRoutes);
|
||||
connect(route_list_, &QListWidget::itemDoubleClicked, this, &QDialog::accept);
|
||||
QObject::connect(button_box, &QDialogButtonBox::accepted, this, &QDialog::accept);
|
||||
QObject::connect(button_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
|
||||
// Send request to fetch devices
|
||||
HttpRequest *http = new HttpRequest(this, !Hardware::PC());
|
||||
QObject::connect(http, &HttpRequest::requestDone, this, &RoutesDialog::parseDeviceList);
|
||||
http->sendRequest(CommaApi::BASE_URL + "/v1/me/devices/");
|
||||
}
|
||||
|
||||
void RoutesDialog::parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err) {
|
||||
if (success) {
|
||||
device_list_->clear();
|
||||
auto devices = QJsonDocument::fromJson(json.toUtf8()).array();
|
||||
for (const QJsonValue &device : devices) {
|
||||
QString dongle_id = device["dongle_id"].toString();
|
||||
device_list_->addItem(dongle_id, dongle_id);
|
||||
}
|
||||
} else {
|
||||
bool unauthorized = (err == QNetworkReply::ContentAccessDenied || err == QNetworkReply::AuthenticationRequiredError);
|
||||
QMessageBox::warning(this, tr("Error"), unauthorized ? tr("Unauthorized, Authenticate with tools/lib/auth.py") : tr("Network error"));
|
||||
reject();
|
||||
}
|
||||
sender()->deleteLater();
|
||||
}
|
||||
|
||||
void RoutesDialog::fetchRoutes() {
|
||||
if (device_list_->currentIndex() == -1 || device_list_->currentData().isNull())
|
||||
return;
|
||||
|
||||
route_list_->clear();
|
||||
route_list_->setEmptyText(tr("Loading..."));
|
||||
|
||||
HttpRequest *http = new HttpRequest(this, !Hardware::PC());
|
||||
QObject::connect(http, &HttpRequest::requestDone, this, &RoutesDialog::parseRouteList);
|
||||
|
||||
// Construct URL with selected device and date range
|
||||
auto dongle_id = device_list_->currentData().toString();
|
||||
QDateTime current = QDateTime::currentDateTime();
|
||||
QString url = QString("%1/v1/devices/%2/routes_segments?start=%3&end=%4")
|
||||
.arg(CommaApi::BASE_URL).arg(dongle_id)
|
||||
.arg(current.addDays(-(period_selector_->currentData().toInt())).toMSecsSinceEpoch())
|
||||
.arg(current.toMSecsSinceEpoch());
|
||||
http->sendRequest(url);
|
||||
}
|
||||
|
||||
void RoutesDialog::parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err) {
|
||||
if (success) {
|
||||
for (const QJsonValue &route : QJsonDocument::fromJson(json.toUtf8()).array()) {
|
||||
uint64_t start_time = route["start_time_utc_millis"].toDouble();
|
||||
uint64_t end_time = route["end_time_utc_millis"].toDouble();
|
||||
auto datetime = QDateTime::fromMSecsSinceEpoch(start_time);
|
||||
auto item = new QListWidgetItem(QString("%1 %2min").arg(datetime.toString()).arg((end_time - start_time) / (1000 * 60)));
|
||||
item->setData(Qt::UserRole, route["fullname"].toString());
|
||||
route_list_->addItem(item);
|
||||
}
|
||||
// Select first route if available
|
||||
if (route_list_->count() > 0) route_list_->setCurrentRow(0);
|
||||
} else {
|
||||
QMessageBox::warning(this, tr("Error"), tr("Failed to fetch routes. Check your network connection."));
|
||||
reject();
|
||||
}
|
||||
route_list_->setEmptyText(tr("No items"));
|
||||
sender()->deleteLater();
|
||||
}
|
||||
|
||||
void RoutesDialog::accept() {
|
||||
if (auto current_item = route_list_->currentItem()) {
|
||||
route_ = current_item->data(Qt::UserRole).toString();
|
||||
}
|
||||
QDialog::accept();
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
#pragma once
|
||||
|
||||
#include <QComboBox>
|
||||
#include <QDialog>
|
||||
|
||||
#include "selfdrive/ui/qt/api.h"
|
||||
|
||||
class RouteListWidget;
|
||||
|
||||
class RoutesDialog : public QDialog {
|
||||
Q_OBJECT
|
||||
public:
|
||||
RoutesDialog(QWidget *parent);
|
||||
QString route() const { return route_; }
|
||||
|
||||
protected:
|
||||
void accept() override;
|
||||
void parseDeviceList(const QString &json, bool success, QNetworkReply::NetworkError err);
|
||||
void parseRouteList(const QString &json, bool success, QNetworkReply::NetworkError err);
|
||||
void fetchRoutes();
|
||||
|
||||
QComboBox *device_list_;
|
||||
QComboBox *period_selector_;
|
||||
RouteListWidget *route_list_;
|
||||
QString route_;
|
||||
};
|
||||
@@ -13,12 +13,8 @@
|
||||
|
||||
StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDialog(parent) {
|
||||
setWindowTitle(tr("Open stream"));
|
||||
QVBoxLayout *main_layout = new QVBoxLayout(this);
|
||||
|
||||
QWidget *w = new QWidget(this);
|
||||
QVBoxLayout *layout = new QVBoxLayout(w);
|
||||
QVBoxLayout *layout = new QVBoxLayout(this);
|
||||
tab = new QTabWidget(this);
|
||||
tab->setTabBarAutoHide(true);
|
||||
layout->addWidget(tab);
|
||||
|
||||
QHBoxLayout *dbc_layout = new QHBoxLayout();
|
||||
@@ -35,9 +31,8 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial
|
||||
line->setFrameStyle(QFrame::HLine | QFrame::Sunken);
|
||||
layout->addWidget(line);
|
||||
|
||||
main_layout->addWidget(w);
|
||||
auto btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel);
|
||||
main_layout->addWidget(btn_box);
|
||||
layout->addWidget(btn_box);
|
||||
|
||||
addStreamWidget(ReplayStream::widget(stream));
|
||||
addStreamWidget(PandaStream::widget(stream));
|
||||
@@ -48,14 +43,11 @@ StreamSelector::StreamSelector(AbstractStream **stream, QWidget *parent) : QDial
|
||||
|
||||
QObject::connect(btn_box, &QDialogButtonBox::rejected, this, &QDialog::reject);
|
||||
QObject::connect(btn_box, &QDialogButtonBox::accepted, [=]() {
|
||||
btn_box->button(QDialogButtonBox::Open)->setEnabled(false);
|
||||
w->setEnabled(false);
|
||||
setEnabled(false);
|
||||
if (((AbstractOpenStreamWidget *)tab->currentWidget())->open()) {
|
||||
accept();
|
||||
} else {
|
||||
btn_box->button(QDialogButtonBox::Open)->setEnabled(true);
|
||||
w->setEnabled(true);
|
||||
}
|
||||
setEnabled(true);
|
||||
});
|
||||
QObject::connect(file_btn, &QPushButton::clicked, [this]() {
|
||||
QString fn = QFileDialog::getOpenFileName(this, tr("Open File"), settings.last_dir, "DBC (*.dbc)");
|
||||
|
||||
Reference in New Issue
Block a user