From 897a62dcfcb23e23a4707a34c1c1d65cd72cf367 Mon Sep 17 00:00:00 2001 From: Dean Lee Date: Mon, 30 Oct 2023 01:05:59 +0800 Subject: [PATCH] cabana: support multiple cameras (#30339) support multiple cameras old-commit-hash: fe4ad9670179b6f823b7332d5a903a7049a3ac03 --- tools/cabana/cabana.cc | 12 +++---- tools/cabana/streams/abstractstream.h | 3 -- tools/cabana/streams/replaystream.cc | 29 ++++++++-------- tools/cabana/streams/replaystream.h | 6 ++-- tools/cabana/videowidget.cc | 49 +++++++++++++++++++-------- tools/cabana/videowidget.h | 4 +++ 6 files changed, 61 insertions(+), 42 deletions(-) diff --git a/tools/cabana/cabana.cc b/tools/cabana/cabana.cc index 33403a2bff..205d795776 100644 --- a/tools/cabana/cabana.cc +++ b/tools/cabana/cabana.cc @@ -26,6 +26,7 @@ int main(int argc, char *argv[]) { cmd_parser.addOption({"demo", "use a demo route instead of providing your own"}); cmd_parser.addOption({"qcam", "load qcamera"}); cmd_parser.addOption({"ecam", "load wide road camera"}); + cmd_parser.addOption({"dcam", "load driver camera"}); cmd_parser.addOption({"stream", "read can messages from live streaming"}); cmd_parser.addOption({"panda", "read can messages from panda"}); cmd_parser.addOption({"panda-serial", "read can messages from panda with given serial", "panda-serial"}); @@ -60,13 +61,10 @@ int main(int argc, char *argv[]) { stream = new SocketCanStream(&app, config); } else { uint32_t replay_flags = REPLAY_FLAG_NONE; - if (cmd_parser.isSet("ecam")) { - replay_flags |= REPLAY_FLAG_ECAM; - } else if (cmd_parser.isSet("qcam")) { - replay_flags |= REPLAY_FLAG_QCAMERA; - } else if (cmd_parser.isSet("no-vipc")) { - replay_flags |= REPLAY_FLAG_NO_VIPC; - } + if (cmd_parser.isSet("ecam")) replay_flags |= REPLAY_FLAG_ECAM; + if (cmd_parser.isSet("qcam")) replay_flags |= REPLAY_FLAG_QCAMERA; + if (cmd_parser.isSet("dcam")) replay_flags |= REPLAY_FLAG_DCAM; + if (cmd_parser.isSet("no-vipc")) replay_flags |= REPLAY_FLAG_NO_VIPC; const QStringList args = cmd_parser.positionalArguments(); QString route; diff --git a/tools/cabana/streams/abstractstream.h b/tools/cabana/streams/abstractstream.h index 4a17affebe..02ebc4b5d1 100644 --- a/tools/cabana/streams/abstractstream.h +++ b/tools/cabana/streams/abstractstream.h @@ -3,7 +3,6 @@ #include #include #include -#include #include #include @@ -70,7 +69,6 @@ public: virtual double currentSec() const = 0; virtual double totalSeconds() const { return lastEventMonoTime() / 1e9 - routeStartTime(); } const CanData &lastMessage(const MessageId &id); - virtual VisionStreamType visionStreamType() const { return VISION_STREAM_ROAD; } virtual const Route *route() const { return nullptr; } virtual void setSpeed(float speed) {} virtual double getSpeed() { return 1; } @@ -79,7 +77,6 @@ public: const MessageEventsMap &eventsMap() const { return events_; } const std::vector &allEvents() const { return all_events_; } const std::vector &events(const MessageId &id) const; - virtual const std::vector> getTimeline() { return {}; } signals: void paused(); diff --git a/tools/cabana/streams/replaystream.cc b/tools/cabana/streams/replaystream.cc index 8a6b806a8a..ccf3e7ca03 100644 --- a/tools/cabana/streams/replaystream.cc +++ b/tools/cabana/streams/replaystream.cc @@ -35,7 +35,8 @@ void ReplayStream::mergeSegments() { } 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.reset(new Replay(route, {"can", "roadEncodeIdx", "driverEncodeIdx", "wideRoadEncodeIdx", "carParams"}, + {}, {}, nullptr, replay_flags, data_dir, this)); replay->setSegmentCacheLimit(settings.max_cached_minutes); replay->installEventFilter(event_filter, this); QObject::connect(replay.get(), &Replay::seekedTo, this, &AbstractStream::seekedTo); @@ -84,25 +85,21 @@ AbstractOpenStreamWidget *ReplayStream::widget(AbstractStream **stream) { OpenReplayWidget::OpenReplayWidget(AbstractStream **stream) : AbstractOpenStreamWidget(stream) { // TODO: get route list from api.comma.ai - QGridLayout *grid_layout = new QGridLayout(); + 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); - 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; + 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))); + camera_layout->addStretch(1); + grid_layout->addItem(camera_layout, 1, 1); - 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()) { @@ -124,9 +121,13 @@ bool OpenReplayWidget::open() { 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}; auto replay_stream = std::make_unique(qApp); - if (replay_stream->loadRoute(route, data_dir, flags[choose_video_cb->currentIndex()])) { + uint32_t flags = REPLAY_FLAG_NONE; + if (cameras[1]->isChecked()) flags |= REPLAY_FLAG_DCAM; + if (cameras[2]->isChecked()) flags |= REPLAY_FLAG_ECAM; + if (flags == REPLAY_FLAG_NONE && !cameras[0]->isChecked()) flags = REPLAY_FLAG_NO_VIPC; + + if (replay_stream->loadRoute(route, data_dir, flags)) { *stream = replay_stream.release(); } else { QMessageBox::warning(nullptr, tr("Warning"), tr("Failed to load route: '%1'").arg(route)); diff --git a/tools/cabana/streams/replaystream.h b/tools/cabana/streams/replaystream.h index d69922a432..7f8f8a4d3a 100644 --- a/tools/cabana/streams/replaystream.h +++ b/tools/cabana/streams/replaystream.h @@ -1,9 +1,9 @@ #pragma once +#include #include #include #include -#include #include #include "common/prefix.h" @@ -21,7 +21,6 @@ public: inline QString routeName() const override { return replay->route()->name(); } inline QString carFingerprint() const override { return replay->carFingerprint().c_str(); } double totalSeconds() const override { return replay->totalSeconds(); } - inline VisionStreamType visionStreamType() const override { return replay->hasFlag(REPLAY_FLAG_ECAM) ? VISION_STREAM_WIDE_ROAD : VISION_STREAM_ROAD; } inline QDateTime beginDateTime() const { return replay->route()->datetime(); } inline double routeStartTime() const override { return replay->routeStartTime() / (double)1e9; } inline double currentSec() const override { return replay->currentSeconds(); } @@ -31,7 +30,6 @@ public: inline Replay *getReplay() const { return replay.get(); } inline bool isPaused() const override { return replay->isPaused(); } void pause(bool pause) override; - inline const std::vector> getTimeline() override { return replay->getTimeline(); } static AbstractOpenStreamWidget *widget(AbstractStream **stream); signals: @@ -54,5 +52,5 @@ public: private: QLineEdit *route_edit; - QComboBox *choose_video_cb; + std::vector cameras; }; diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index f7f566507c..7afcc7b938 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -33,11 +33,9 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { main_layout->addLayout(createPlaybackController()); setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - QObject::connect(can, &AbstractStream::paused, this, &VideoWidget::updatePlayBtnState); QObject::connect(can, &AbstractStream::resume, this, &VideoWidget::updatePlayBtnState); QObject::connect(can, &AbstractStream::updated, this, &VideoWidget::updateState); - QObject::connect(&settings, &Settings::changed, this, &VideoWidget::updatePlayBtnState); updatePlayBtnState(); setWhatsThis(tr(R"( @@ -131,10 +129,15 @@ QWidget *VideoWidget::createCameraWidget() { QWidget *w = new QWidget(this); QVBoxLayout *l = new QVBoxLayout(w); l->setContentsMargins(0, 0, 0, 0); + l->setSpacing(0); + + l->addWidget(camera_tab = new TabBar(w)); + camera_tab->setAutoHide(true); + camera_tab->setExpanding(false); QStackedLayout *stacked = new QStackedLayout(); stacked->setStackingMode(QStackedLayout::StackAll); - stacked->addWidget(cam_widget = new CameraWidget("camerad", can->visionStreamType(), false)); + stacked->addWidget(cam_widget = new CameraWidget("camerad", VISION_STREAM_ROAD, false)); cam_widget->setMinimumHeight(MIN_VIDEO_HEIGHT); cam_widget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::MinimumExpanding); stacked->addWidget(alert_label = new InfoLabel(this)); @@ -146,11 +149,34 @@ QWidget *VideoWidget::createCameraWidget() { setMaximumTime(can->totalSeconds()); QObject::connect(slider, &QSlider::sliderReleased, [this]() { can->seekTo(slider->currentSecond()); }); QObject::connect(slider, &Slider::updateMaximumTime, this, &VideoWidget::setMaximumTime, Qt::QueuedConnection); - QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); QObject::connect(static_cast(can), &ReplayStream::qLogLoaded, slider, &Slider::parseQLog); + QObject::connect(cam_widget, &CameraWidget::clicked, []() { can->pause(!can->isPaused()); }); + QObject::connect(cam_widget, &CameraWidget::vipcAvailableStreamsUpdated, this, &VideoWidget::vipcAvailableStreamsUpdated); + QObject::connect(camera_tab, &QTabBar::currentChanged, [this](int index) { + if (index != -1) cam_widget->setStreamType((VisionStreamType)camera_tab->tabData(index).toInt()); + }); return w; } +void VideoWidget::vipcAvailableStreamsUpdated(std::set streams) { + static const QString stream_names[] = { + [VISION_STREAM_ROAD] = "Road camera", + [VISION_STREAM_WIDE_ROAD] = "Wide road camera", + [VISION_STREAM_DRIVER] = "Driver camera"}; + + for (int i = 0; i < streams.size(); ++i) { + if (camera_tab->count() <= i) { + camera_tab->addTab(QString()); + } + int type = *std::next(streams.begin(), i); + camera_tab->setTabText(i, stream_names[type]); + camera_tab->setTabData(i, type); + } + while (camera_tab->count() > streams.size()) { + camera_tab->removeTab(camera_tab->count() - 1); + } +} + void VideoWidget::loopPlaybackClicked() { auto replay = qobject_cast(can)->getReplay(); if (replay->hasFlag(REPLAY_FLAG_NO_LOOP)) { @@ -172,12 +198,8 @@ void VideoWidget::updateTimeRange(double min, double max, bool is_zoomed) { skip_to_end_btn->setEnabled(!is_zoomed); return; } - - if (!is_zoomed) { - min = 0; - max = maximum_time; - } - slider->setTimeRange(min, max); + is_zoomed ? slider->setTimeRange(min, max) + : slider->setTimeRange(0, maximum_time); } QString VideoWidget::formatTime(double sec, bool include_milliseconds) { @@ -262,7 +284,7 @@ void Slider::paintEvent(QPaintEvent *ev) { double min = minimum() / factor; double max = maximum() / factor; - for (auto [begin, end, type] : qobject_cast(can)->getTimeline()) { + for (auto [begin, end, type] : qobject_cast(can)->getReplay()->getTimeline()) { if (begin > max || end < min) continue; r.setLeft(((std::max(min, begin) - min) / (max - min)) * width()); @@ -282,8 +304,7 @@ void Slider::paintEvent(QPaintEvent *ev) { void Slider::mousePressEvent(QMouseEvent *e) { QSlider::mousePressEvent(e); if (e->button() == Qt::LeftButton && !isSliderDown()) { - int value = minimum() + ((maximum() - minimum()) * e->x()) / width(); - setValue(value); + setValue(minimum() + ((maximum() - minimum()) * e->x()) / width()); emit sliderReleased(); } } @@ -294,7 +315,7 @@ void Slider::mouseMoveEvent(QMouseEvent *e) { QPixmap thumb = thumbnail(seconds); if (!thumb.isNull()) { int x = std::clamp(pos - thumb.width() / 2, THUMBNAIL_MARGIN, width() - thumb.width() - THUMBNAIL_MARGIN + 1); - int y = -thumb.height() - THUMBNAIL_MARGIN - 6; + int y = -thumb.height() - THUMBNAIL_MARGIN; thumbnail_label.showPixmap(mapToParent(QPoint(x, y)), utils::formatSeconds(seconds), thumb, alertInfo(seconds)); } else { thumbnail_label.hide(); diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 68e0461117..c2fdb41df8 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -2,9 +2,11 @@ #include #include +#include #include #include +#include #include #include "selfdrive/ui/qt/widgets/cameraview.h" @@ -70,6 +72,7 @@ protected: QWidget *createCameraWidget(); QHBoxLayout *createPlaybackController(); void loopPlaybackClicked(); + void vipcAvailableStreamsUpdated(std::set streams); CameraWidget *cam_widget; double maximum_time = 0; @@ -82,4 +85,5 @@ protected: ToolButton *skip_to_end_btn = nullptr; InfoLabel *alert_label = nullptr; Slider *slider = nullptr; + QTabBar *camera_tab = nullptr; };