offroad UI: replace stock home with clean grid launcher

- Strip out date, version, update/alert widgets from OffroadHome
- Replace with grid layout: Dashcam and Settings buttons
- Skip sidebar when tapping splash screen
- Settings button still opens original comma settings
- Dashcam button placeholder (viewer not yet built)
- Add DASHCAM_PROJECT.md with plans for footage viewer

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-11 07:41:47 +00:00
parent 8ae7ef6eaf
commit ed9c14616a
13 changed files with 123 additions and 170 deletions

49
DASHCAM_PROJECT.md Normal file
View File

@@ -0,0 +1,49 @@
# Dashcam Project
## Status
### Completed (2026-04-11)
- Disabled comma training data video encoding (`encoderd`) — CAN/sensor logs still recorded
- Re-enabled FrogPilot OMX screen recorder (H.264 MP4, 1440x720, 2Mbps, hardware encoded)
- Auto-start/stop recording tied to car ignition (`scene.started`)
- `ScreenRecorderDebug` param for bench testing without car connected
- Hidden all recorder UI elements — invisible to driver
- Videos saved to `/data/media/0/videos/YYYYMMDD-HHMMSS.mp4`, 3-minute segments
- Deleter updated: 9 GB free space threshold, rotates oldest videos first
- Cleaned up offroad UI: replaced stock home screen with grid launcher (Settings + Dashcam buttons)
### Next: Dashcam Footage Viewer
Build a native Qt widget accessible from the offroad home screen "Dashcam" button.
**Requirements:**
- List MP4 files from `/data/media/0/videos/` sorted newest first
- Tap a file to play it back using Qt Multimedia (`QMediaPlayer` + `QVideoWidget`)
- Only accessible when offroad (car in park or off)
- Back button to return to offroad home
- No rotation hacks needed — native Qt widget in existing UI tree handles rotation correctly
**Architecture:**
- New widget class (e.g. `DashcamViewer`) added to `selfdrive/ui/qt/`
- Wire into `HomeWindow`'s `QStackedLayout` alongside `home`, `onroad`, `ready`, `driver_view`
- Dashcam button in `OffroadHome` switches `slayout` to the viewer
- Viewer has a file list view and a playback view (sub-stacked layout)
- Back button returns to `OffroadHome`
- Build: add to `qt_src` list in `selfdrive/ui/SConscript`, link `Qt5Multimedia`
**Previous webview attempts (abandoned):**
- Brian tried QWebEngineView for browser-based playback but the WebEngine subprocess renders independently of Qt's widget tree
- Screen rotation (`view.rotate(90)`, Wayland `wl_surface_set_buffer_transform`) did not work for WebEngine content
- Native Qt widget approach avoids this problem entirely
## Key Files
| File | Role |
|------|------|
| `selfdrive/frogpilot/screenrecorder/screenrecorder.cc` | Recording logic, auto-start/stop |
| `selfdrive/frogpilot/screenrecorder/omx_encoder.cc` | OMX H.264 hardware encoder |
| `selfdrive/ui/qt/onroad.cc` | Timer driving frame capture |
| `selfdrive/ui/qt/home.cc` | Offroad home with Dashcam button |
| `system/loggerd/deleter.py` | Storage rotation |
| `/data/media/0/videos/` | Video output directory |

View File

@@ -5,12 +5,7 @@
#include <QStackedWidget> #include <QStackedWidget>
#include <QVBoxLayout> #include <QVBoxLayout>
#include "selfdrive/ui/qt/offroad/experimental_mode.h"
#include "selfdrive/ui/qt/util.h" #include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/qt/widgets/drive_stats.h"
#include "system/hardware/hw.h"
#include <QWebEngineView>
// HomeWindow: the container for the offroad and onroad UIs // HomeWindow: the container for the offroad and onroad UIs
@@ -102,9 +97,9 @@ void HomeWindow::mousePressEvent(QMouseEvent* e) {
// CLEARPILOT todo - tap on main goes straight to settings // CLEARPILOT todo - tap on main goes straight to settings
// Unless we click a debug widget. // Unless we click a debug widget.
// CLEARPILOT - click ready shows home // CLEARPILOT - click ready shows home (no sidebar)
if (!onroad->isVisible() && ready->isVisible()) { if (!onroad->isVisible() && ready->isVisible()) {
sidebar->setVisible(true); sidebar->setVisible(false);
slayout->setCurrentWidget(home); slayout->setCurrentWidget(home);
} }
@@ -119,132 +114,59 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
// const SubMaster &sm = *(uiState()->sm); // const SubMaster &sm = *(uiState()->sm);
} }
// OffroadHome: the offroad home page // CLEARPILOT: OffroadHome — clean grid launcher
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) { OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
QVBoxLayout* main_layout = new QVBoxLayout(this); QVBoxLayout* main_layout = new QVBoxLayout(this);
main_layout->setContentsMargins(40, 40, 40, 40); main_layout->setContentsMargins(80, 80, 80, 80);
main_layout->setSpacing(0);
// top header // grid of launcher buttons
QHBoxLayout* header_layout = new QHBoxLayout(); QGridLayout *grid = new QGridLayout();
header_layout->setContentsMargins(0, 0, 0, 0); grid->setSpacing(40);
header_layout->setSpacing(16);
update_notif = new QPushButton(tr("UPDATE")); // Dashcam viewer button
update_notif->setVisible(false); QPushButton *dashcam_btn = new QPushButton("Dashcam");
update_notif->setStyleSheet("background-color: #364DEF;"); dashcam_btn->setFixedSize(400, 300);
QObject::connect(update_notif, &QPushButton::clicked, [=]() { center_layout->setCurrentIndex(1); }); dashcam_btn->setStyleSheet(R"(
header_layout->addWidget(update_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); QPushButton {
background-color: #333333;
color: white;
border-radius: 20px;
font-size: 48px;
font-weight: 600;
}
QPushButton:pressed {
background-color: #555555;
}
)");
grid->addWidget(dashcam_btn, 0, 0);
alert_notif = new QPushButton(); // Settings button
alert_notif->setVisible(false); QPushButton *settings_btn = new QPushButton("Settings");
alert_notif->setStyleSheet("background-color: #E22C2C;"); settings_btn->setFixedSize(400, 300);
QObject::connect(alert_notif, &QPushButton::clicked, [=] { center_layout->setCurrentIndex(2); }); settings_btn->setStyleSheet(R"(
header_layout->addWidget(alert_notif, 0, Qt::AlignHCenter | Qt::AlignLeft); QPushButton {
background-color: #333333;
color: white;
border-radius: 20px;
font-size: 48px;
font-weight: 600;
}
QPushButton:pressed {
background-color: #555555;
}
)");
QObject::connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(); });
grid->addWidget(settings_btn, 0, 1);
date = new ElidedLabel(); main_layout->addStretch();
header_layout->addWidget(date, 0, Qt::AlignHCenter | Qt::AlignLeft); main_layout->addLayout(grid);
main_layout->addStretch();
version = new ElidedLabel();
header_layout->addWidget(version, 0, Qt::AlignHCenter | Qt::AlignRight);
main_layout->addLayout(header_layout);
// main content
main_layout->addSpacing(25);
center_layout = new QStackedLayout();
QWidget *home_widget = new QWidget(this);
{
QHBoxLayout *home_layout = new QHBoxLayout(home_widget);
home_layout->setContentsMargins(0, 0, 0, 0);
home_layout->setSpacing(30);
// // // Create a QWebEngineView
// QWebEngineView *web_view = new QWebEngineView();
// web_view->load(QUrl("http://fark.com"));
// // Add the QWebEngineView to the layout
// home_layout->addWidget(web_view);
}
center_layout->addWidget(home_widget);
// add update & alerts widgets
update_widget = new UpdateAlert();
QObject::connect(update_widget, &UpdateAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
center_layout->addWidget(update_widget);
alerts_widget = new OffroadAlert();
QObject::connect(alerts_widget, &OffroadAlert::dismiss, [=]() { center_layout->setCurrentIndex(0); });
center_layout->addWidget(alerts_widget);
main_layout->addLayout(center_layout, 1);
// set up refresh timer
timer = new QTimer(this);
timer->callOnTimeout(this, &OffroadHome::refresh);
setStyleSheet(R"( setStyleSheet(R"(
* {
color: white;
}
OffroadHome { OffroadHome {
background-color: black; background-color: black;
} }
OffroadHome > QPushButton {
padding: 15px 30px;
border-radius: 5px;
font-size: 40px;
font-weight: 500;
}
OffroadHome > QLabel {
font-size: 55px;
}
)"); )");
} }
/* Refresh data on screen every 5 seconds. */
void OffroadHome::showEvent(QShowEvent *event) {
refresh();
timer->start(5 * 1000);
}
void OffroadHome::hideEvent(QHideEvent *event) {
timer->stop();
}
void OffroadHome::refresh() {
QString model = QString::fromStdString(params.get("ModelName"));
date->setText(QLocale(uiState()->language.mid(5)).toString(QDateTime::currentDateTime(), "dddd, MMMM d"));
version->setText(getBrand() + " v" + getVersion().left(14).trimmed() + " - " + model);
// bool updateAvailable = update_widget->refresh();
int alerts = alerts_widget->refresh();
if (alerts > 0 && !alerts_widget->isVisible()) {
alerts_widget->setVisible(true);
} else if (alerts == 0 && alerts_widget->isVisible()) {
alerts_widget->setVisible(false);
}
// pop-up new notification
// CLEARPILOT temp disabled update notifications
// int idx = center_layout->currentIndex();
// if (!updateAvailable && !alerts && false) {
// idx = 0;
// } else if (updateAvailable && (!update_notif->isVisible() || (!alerts && idx == 2))) {
// idx = 1;
// } else if (alerts && (!alert_notif->isVisible() || (!updateAvailable && idx == 1))) {
// idx = 2;
// }
// center_layout->setCurrentIndex(idx);
// CLEARPILOT temp disabled update notifications
// update_notif->setVisible(updateAvailable);
// alert_notif->setVisible(alerts);
alert_notif->setVisible(false);
if (alerts) {
alert_notif->setText(QString::number(alerts) + (alerts > 1 ? tr(" ALERTS") : tr(" ALERT")));
}
}

View File

@@ -24,24 +24,6 @@ public:
signals: signals:
void openSettings(int index = 0, const QString &param = ""); void openSettings(int index = 0, const QString &param = "");
private:
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent *event) override;
void refresh();
Params params;
QTimer* timer;
ElidedLabel* version;
QStackedLayout* center_layout;
UpdateAlert *update_widget;
OffroadAlert* alerts_widget;
QPushButton* alert_notif;
QPushButton* update_notif;
// FrogPilot variables
ElidedLabel* date;
}; };
class HomeWindow : public QWidget { class HomeWindow : public QWidget {

View File

@@ -818,15 +818,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation>تحديث</translation> <translation type="vanished">تحديث</translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> التنبهات</translation> <translation type="vanished"> التنبهات</translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> تنبيه</translation> <translation type="vanished"> تنبيه</translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -771,15 +771,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation>Aktualisieren</translation> <translation type="vanished">Aktualisieren</translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> HINWEISE</translation> <translation type="vanished"> HINWEISE</translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> HINWEIS</translation> <translation type="vanished"> HINWEIS</translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -814,15 +814,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation>MISE À JOUR</translation> <translation type="vanished">MISE À JOUR</translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> ALERTES</translation> <translation type="vanished"> ALERTES</translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> ALERTE</translation> <translation type="vanished"> ALERTE</translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -770,15 +770,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation></translation> <translation type="vanished"></translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -813,15 +813,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation></translation> <translation type="vanished"></translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -814,15 +814,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation>ATUALIZAÇÃO</translation> <translation type="vanished">ATUALIZAÇÃO</translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> ALERTAS</translation> <translation type="vanished"> ALERTAS</translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> ALERTA</translation> <translation type="vanished"> ALERTA</translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -813,15 +813,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation></translation> <translation type="vanished"></translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -770,15 +770,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation>GÜNCELLE</translation> <translation type="vanished">GÜNCELLE</translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> UYARILAR</translation> <translation type="vanished"> UYARILAR</translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> UYARI</translation> <translation type="vanished"> UYARI</translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -813,15 +813,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation></translation> <translation type="vanished"></translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
</context> </context>
<context> <context>

View File

@@ -813,15 +813,15 @@
<name>OffroadHome</name> <name>OffroadHome</name>
<message> <message>
<source>UPDATE</source> <source>UPDATE</source>
<translation></translation> <translation type="vanished"></translation>
</message> </message>
<message> <message>
<source> ALERTS</source> <source> ALERTS</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
<message> <message>
<source> ALERT</source> <source> ALERT</source>
<translation> </translation> <translation type="vanished"> </translation>
</message> </message>
</context> </context>
<context> <context>