From 6a98fd2bfdd46047eb7c27e8f91fa59a437f6b93 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Fri, 14 Apr 2023 10:53:39 +0800 Subject: [PATCH] cabana: support logging live stream (#27884) support logging live stream old-commit-hash: e3a19ff074857876dca3c730f6ddc3c6e191aaf5 --- tools/cabana/cabana.cc | 4 +-- tools/cabana/route.cc | 14 +++++---- tools/cabana/route.h | 2 ++ tools/cabana/settings.cc | 46 +++++++++++++++++++++++++--- tools/cabana/settings.h | 7 +++++ tools/cabana/streams/livestream.cc | 12 +++++++- tools/cabana/streams/replaystream.cc | 4 +-- tools/cabana/streams/replaystream.h | 5 ++- 8 files changed, 75 insertions(+), 19 deletions(-) diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 329b6070ae..10b394cec5 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -57,13 +57,13 @@ int main(int argc, char *argv[]) { route = DEMO_ROUTE; } - auto replay_stream = new ReplayStream(replay_flags, &app); + auto replay_stream = new ReplayStream(&app); stream.reset(replay_stream); if (route.isEmpty()) { if (OpenRouteDialog dlg(nullptr); !dlg.exec()) { return 0; } - } else if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"))) { + } else if (!replay_stream->loadRoute(route, cmd_parser.value("data_dir"), replay_flags)) { return 0; } } diff --git a/tools/cabana/route.cc b/tools/cabana/route.cc index ab322cdf90..bd678203fc 100644 --- a/tools/cabana/route.cc +++ b/tools/cabana/route.cc @@ -2,7 +2,7 @@ #include #include -#include +#include #include #include #include @@ -11,12 +11,13 @@ 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)); + QGridLayout *edit_layout = new QGridLayout(); + edit_layout->addWidget(new QLabel(tr("Route:"), 0, 0)); + edit_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); - edit_layout->addWidget(file_btn); + edit_layout->addWidget(file_btn, 0, 2); + edit_layout->addWidget(no_vipc = new QCheckBox(tr("No video")), 1, 1); btn_box = new QDialogButtonBox(QDialogButtonBox::Open | QDialogButtonBox::Cancel); btn_box->button(QDialogButtonBox::Open)->setEnabled(false); @@ -56,7 +57,8 @@ void OpenRouteDialog::loadRoute() { 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); + uint32_t flags = no_vipc->isChecked() ? REPLAY_FLAG_NO_VIPC : REPLAY_FLAG_NONE; + failed_to_load = !dynamic_cast(can)->loadRoute(route, data_dir, flags); if (failed_to_load) { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); } else { diff --git a/tools/cabana/route.h b/tools/cabana/route.h index ceda71d585..fd360d15d6 100644 --- a/tools/cabana/route.h +++ b/tools/cabana/route.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -14,6 +15,7 @@ public: private: QLineEdit *route_edit; + QCheckBox *no_vipc; QDialogButtonBox *btn_box; bool failed_to_load = false; }; diff --git a/tools/cabana/settings.cc b/tools/cabana/settings.cc index 8fd852e198..4d8c4fcc5a 100644 --- a/tools/cabana/settings.cc +++ b/tools/cabana/settings.cc @@ -3,8 +3,11 @@ #include #include #include +#include #include +#include #include +#include #include "tools/cabana/util.h" @@ -32,6 +35,8 @@ void Settings::save() { s.setValue("theme", theme); s.setValue("sparkline_range", sparkline_range); s.setValue("multiple_lines_bytes", multiple_lines_bytes); + s.setValue("log_livestream", log_livestream); + s.setValue("log_path", log_path); } void Settings::load() { @@ -52,13 +57,20 @@ void Settings::load() { theme = s.value("theme", 0).toInt(); sparkline_range = s.value("sparkline_range", 15).toInt(); multiple_lines_bytes = s.value("multiple_lines_bytes", true).toBool(); + log_livestream = s.value("log_livestream", true).toBool(); + log_path = s.value("log_path").toString(); + if (log_path.isEmpty()) { + log_path = QStandardPaths::writableLocation(QStandardPaths::HomeLocation) + "/cabana_live_stream/"; + } } // SettingsDlg SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { setWindowTitle(tr("Settings")); - QFormLayout *form_layout = new QFormLayout(this); + QVBoxLayout *main_layout = new QVBoxLayout(this); + QGroupBox *groupbox = new QGroupBox("General"); + QFormLayout *form_layout = new QFormLayout(groupbox); theme = new QComboBox(this); theme->setToolTip(tr("You may need to restart cabana after changes theme")); @@ -77,7 +89,10 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { cached_minutes->setSingleStep(1); cached_minutes->setValue(settings.max_cached_minutes); form_layout->addRow(tr("Max Cached Minutes"), cached_minutes); + main_layout->addWidget(groupbox); + groupbox = new QGroupBox("Chart"); + form_layout = new QFormLayout(groupbox); chart_series_type = new QComboBox(this); chart_series_type->addItems({tr("Line"), tr("Step Line"), tr("Scatter")}); chart_series_type->setCurrentIndex(settings.chart_series_type); @@ -88,12 +103,31 @@ SettingsDlg::SettingsDlg(QWidget *parent) : QDialog(parent) { chart_height->setSingleStep(10); chart_height->setValue(settings.chart_height); form_layout->addRow(tr("Chart Height"), chart_height); + main_layout->addWidget(groupbox); - auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); - form_layout->addRow(buttonBox); + log_livestream = new QGroupBox(tr("Enable live stream logging"), this); + log_livestream->setCheckable(true); + QHBoxLayout *path_layout = new QHBoxLayout(log_livestream); + path_layout->addWidget(log_path = new QLineEdit(settings.log_path, this)); + log_path->setReadOnly(true); + auto browse_btn = new QPushButton(tr("B&rowse...")); + path_layout->addWidget(browse_btn); + main_layout->addWidget(log_livestream); - setFixedWidth(360); - connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton *button) { + auto buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel | QDialogButtonBox::Apply); + main_layout->addWidget(buttonBox); + main_layout->addStretch(1); + + QObject::connect(browse_btn, &QPushButton::clicked, [this]() { + QString fn = QFileDialog::getExistingDirectory( + this, tr("Log File Location"), + QStandardPaths::writableLocation(QStandardPaths::HomeLocation), + QFileDialog::ShowDirsOnly | QFileDialog::DontResolveSymlinks); + if (!fn.isEmpty()) { + log_path->setText(fn); + } + }); + QObject::connect(buttonBox, &QDialogButtonBox::clicked, [=](QAbstractButton *button) { auto role = buttonBox->buttonRole(button); if (role == QDialogButtonBox::AcceptRole) { save(); @@ -115,6 +149,8 @@ void SettingsDlg::save() { settings.max_cached_minutes = cached_minutes->value(); settings.chart_series_type = chart_series_type->currentIndex(); settings.chart_height = chart_height->value(); + settings.log_livestream = log_livestream->isChecked(); + settings.log_path = log_path->text(); settings.save(); emit settings.changed(); } diff --git a/tools/cabana/settings.h b/tools/cabana/settings.h index 04070b0909..07aafe1682 100644 --- a/tools/cabana/settings.h +++ b/tools/cabana/settings.h @@ -1,8 +1,11 @@ #pragma once #include +#include #include #include +#include +#include #include #define LIGHT_THEME 1 @@ -25,6 +28,8 @@ public: int theme = 0; int sparkline_range = 15; // 15 seconds bool multiple_lines_bytes = true; + bool log_livestream = true; + QString log_path; QString last_dir; QString last_route_dir; QByteArray geometry; @@ -48,6 +53,8 @@ public: QSpinBox *chart_height; QComboBox *chart_series_type; QComboBox *theme; + QGroupBox *log_livestream; + QLineEdit *log_path; }; extern Settings settings; diff --git a/tools/cabana/streams/livestream.cc b/tools/cabana/streams/livestream.cc index 62ad635152..0fcee4455a 100644 --- a/tools/cabana/streams/livestream.cc +++ b/tools/cabana/streams/livestream.cc @@ -19,6 +19,13 @@ void LiveStream::streamThread() { if (!zmq_address.isEmpty()) { setenv("ZMQ", "1", 1); } + + std::unique_ptr fs; + if (settings.log_livestream) { + std::string path = (settings.log_path + "/" + QDateTime::currentDateTime().toString("yyyy-MM-dd--hh-mm-ss") + "--0").toStdString(); + util::create_directories(path, 0755); + fs.reset(new std::ofstream(path + "/rlog" , std::ios::binary | std::ios::out)); + } std::unique_ptr context(Context::create()); std::string address = zmq_address.isEmpty() ? "127.0.0.1" : zmq_address.toStdString(); std::unique_ptr sock(SubSocket::create(context.get(), "can", address)); @@ -31,9 +38,12 @@ void LiveStream::streamThread() { QThread::msleep(50); continue; } + + if (fs) { + fs->write(msg->getData(), msg->getSize()); + } std::lock_guard lk(lock); handleEvent(messages.emplace_back(msg).event); - // TODO: write stream to log file to replay it with cabana --data_dir flag. } } diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 8bc9af8ab2..409ba25841 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -1,6 +1,6 @@ #include "tools/cabana/streams/replaystream.h" -ReplayStream::ReplayStream(uint32_t replay_flags, QObject *parent) : replay_flags(replay_flags), AbstractStream(parent, false) { +ReplayStream::ReplayStream(QObject *parent) : AbstractStream(parent, false) { QObject::connect(&settings, &Settings::changed, [this]() { if (replay) replay->setSegmentCacheLimit(settings.max_cached_minutes); }); @@ -25,7 +25,7 @@ void ReplayStream::mergeSegments() { } } -bool ReplayStream::loadRoute(const QString &route, const QString &data_dir) { +bool ReplayStream::loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags) { 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); diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index 10dc804716..666f64437e 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -7,9 +7,9 @@ class ReplayStream : public AbstractStream { Q_OBJECT public: - ReplayStream(uint32_t replay_flags, QObject *parent); + ReplayStream(QObject *parent); ~ReplayStream(); - bool loadRoute(const QString &route, const QString &data_dir); + bool loadRoute(const QString &route, const QString &data_dir, uint32_t replay_flags = REPLAY_FLAG_NONE); 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(); } @@ -29,6 +29,5 @@ public: private: void mergeSegments(); std::unique_ptr replay = nullptr; - uint32_t replay_flags = REPLAY_FLAG_NONE; std::set processed_segments; };