Files
clearpilot/selfdrive/ui/qt/window.cc
Brian Hanson 9ac334b7cf
Some checks failed
prebuilt / build prebuilt (push) Has been cancelled
badges / create badges (push) Has been cancelled
fix: dashcamd OMX crash on restart, add dashcam status indicator
- Reset OMX subsystem (Deinit/Init) on dashcamd startup to clear stale
  encoder state from previous unclean exits
- Validate OMX output buffers before memcpy to prevent segfault
- Validate VisionBuf frame data before encoding
- Add dashcam row to status window showing recording state and disk usage

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:44:13 -05:00

400 lines
13 KiB
C++
Executable File

#include "selfdrive/ui/qt/window.h"
#include <QFontDatabase>
#include <QMouseEvent>
#include <zmq.h>
#include "system/hardware/hw.h"
MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
main_layout = new QStackedLayout(this);
main_layout->setMargin(0);
homeWindow = new HomeWindow(this);
main_layout->addWidget(homeWindow);
QObject::connect(homeWindow, &HomeWindow::openSettings, this, &MainWindow::openSettings);
QObject::connect(homeWindow, &HomeWindow::openStatus, this, &MainWindow::openStatus);
QObject::connect(homeWindow, &HomeWindow::closeSettings, this, &MainWindow::closeSettings);
settingsWindow = new SettingsWindow(this);
main_layout->addWidget(settingsWindow);
QObject::connect(settingsWindow, &SettingsWindow::closeSettings, this, &MainWindow::closeSettings);
QObject::connect(settingsWindow, &SettingsWindow::reviewTrainingGuide, [=]() {
onboardingWindow->showTrainingGuide();
main_layout->setCurrentWidget(onboardingWindow);
});
QObject::connect(settingsWindow, &SettingsWindow::showDriverView, [=] {
homeWindow->showDriverView(true);
});
// CLEARPILOT: Status window
statusWindow = new StatusWindow(this);
main_layout->addWidget(statusWindow);
QObject::connect(statusWindow, &StatusWindow::closeStatus, this, &MainWindow::closeSettings);
onboardingWindow = new OnboardingWindow(this);
main_layout->addWidget(onboardingWindow);
QObject::connect(onboardingWindow, &OnboardingWindow::onboardingDone, [=]() {
main_layout->setCurrentWidget(homeWindow);
});
if (!onboardingWindow->completed()) {
main_layout->setCurrentWidget(onboardingWindow);
}
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
if (!offroad) {
// CLEARPILOT: just switch to homeWindow, don't show sidebar
// HomeWindow::offroadTransition handles the internal view
main_layout->setCurrentWidget(homeWindow);
}
});
QObject::connect(device(), &Device::interactiveTimeout, [=]() {
// CLEARPILOT: on timeout, return to splash/onroad (not ClearPilotPanel)
if (main_layout->currentWidget() != homeWindow) {
main_layout->setCurrentWidget(homeWindow);
}
homeWindow->offroadTransition(!uiState()->scene.started);
});
// load fonts
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Black.ttf");
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Bold.ttf");
QFontDatabase::addApplicationFont("../assets/fonts/Inter-ExtraBold.ttf");
QFontDatabase::addApplicationFont("../assets/fonts/Inter-ExtraLight.ttf");
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Medium.ttf");
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Regular.ttf");
QFontDatabase::addApplicationFont("../assets/fonts/Inter-SemiBold.ttf");
QFontDatabase::addApplicationFont("../assets/fonts/Inter-Thin.ttf");
QFontDatabase::addApplicationFont("../assets/fonts/JetBrainsMono-Medium.ttf");
// no outline to prevent the focus rectangle
setStyleSheet(R"(
* {
font-family: Inter;
outline: none;
}
)");
setAttribute(Qt::WA_NoSystemBackground);
// CLEARPILOT: UI introspection RPC server
zmq_ctx = zmq_ctx_new();
zmq_sock = zmq_socket(zmq_ctx, ZMQ_REP);
zmq_bind(zmq_sock, "ipc:///tmp/clearpilot_ui_rpc");
int fd;
size_t fd_sz = sizeof(fd);
zmq_getsockopt(zmq_sock, ZMQ_FD, &fd, &fd_sz);
rpc_notifier = new QSocketNotifier(fd, QSocketNotifier::Read, this);
connect(rpc_notifier, &QSocketNotifier::activated, this, &MainWindow::handleRpcRequest);
}
void MainWindow::handleRpcRequest() {
int events = 0;
size_t events_sz = sizeof(events);
zmq_getsockopt(zmq_sock, ZMQ_EVENTS, &events, &events_sz);
if (!(events & ZMQ_POLLIN)) return;
char buf[256];
int rc = zmq_recv(zmq_sock, buf, sizeof(buf) - 1, ZMQ_DONTWAIT);
if (rc < 0) return;
buf[rc] = 0;
QString response;
if (strcmp(buf, "dump") == 0) {
response = dumpWidgetTree(this);
} else {
response = "unknown command";
}
QByteArray resp = response.toUtf8();
zmq_send(zmq_sock, resp.data(), resp.size(), 0);
}
QString MainWindow::dumpWidgetTree(QWidget *w, int depth) {
QString result;
QString indent(depth * 2, ' ');
QString className = w->metaObject()->className();
QString name = w->objectName().isEmpty() ? "(no name)" : w->objectName();
bool visible = w->isVisible();
QRect geo = w->geometry();
result += QString("%1%2 [%3] vis=%4 geo=%5,%6 %7x%8")
.arg(indent, className, name)
.arg(visible ? "Y" : "N")
.arg(geo.x()).arg(geo.y()).arg(geo.width()).arg(geo.height());
// Show stacked layout/widget current index
if (auto *sl = w->findChild<QStackedLayout *>(QString(), Qt::FindDirectChildrenOnly)) {
QWidget *cur = sl->currentWidget();
QString curClass = cur ? cur->metaObject()->className() : "null";
result += QString(" stack_cur=%1/%2(%3)").arg(sl->currentIndex()).arg(sl->count()).arg(curClass);
}
if (auto *sw = qobject_cast<QStackedWidget *>(w)) {
QWidget *cur = sw->currentWidget();
QString curClass = cur ? cur->metaObject()->className() : "null";
result += QString(" stack_cur=%1/%2(%3)").arg(sw->currentIndex()).arg(sw->count()).arg(curClass);
}
result += "\n";
for (QObject *child : w->children()) {
QWidget *cw = qobject_cast<QWidget *>(child);
if (cw && depth < 4) {
result += dumpWidgetTree(cw, depth + 1);
}
}
return result;
}
void MainWindow::openSettings(int index, const QString &param) {
main_layout->setCurrentWidget(settingsWindow);
settingsWindow->setCurrentPanel(index, param);
}
void MainWindow::openStatus() {
main_layout->setCurrentWidget(statusWindow);
}
void MainWindow::closeSettings() {
main_layout->setCurrentWidget(homeWindow);
if (uiState()->scene.started) {
homeWindow->showSidebar(params.getBool("Sidebar"));
}
}
bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
bool ignore = false;
switch (event->type()) {
case QEvent::TouchBegin:
case QEvent::TouchUpdate:
case QEvent::TouchEnd:
case QEvent::MouseButtonPress:
case QEvent::MouseMove: {
// CLEARPILOT: tap while screen-off (mode 3) -> wake to auto-normal (mode 0)
if (!device()->isAwake()) {
Params pmem{"/dev/shm/params"};
if (pmem.getInt("ScreenDisplayMode") == 3) {
pmem.putInt("ScreenDisplayMode", 0);
}
}
// ignore events when device is awakened by resetInteractiveTimeout
ignore = !device()->isAwake();
device()->resetInteractiveTimeout(uiState()->scene.screen_timeout, uiState()->scene.screen_timeout_onroad);
break;
}
default:
break;
}
return ignore;
}
// CLEARPILOT: Status window — live system stats, collected on background thread
#include <QFile>
#include <QProcess>
#include <QDateTime>
#include <QtConcurrent>
static QString readFile(const QString &path) {
QFile f(path);
if (f.open(QIODevice::ReadOnly)) return QString(f.readAll()).trimmed();
return "";
}
static QString shellCmd(const QString &cmd) {
QProcess p;
p.start("bash", QStringList() << "-c" << cmd);
p.waitForFinished(1000);
return QString(p.readAllStandardOutput()).trimmed();
}
static StatusWindow::StatusData collectStatus() {
StatusWindow::StatusData d;
d.time = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss");
d.storage = shellCmd("df -h /data | tail -1 | awk '{print $3 \" / \" $2 \" (\" $5 \" used)\"}'");
// RAM from /proc/meminfo (no subprocess needed)
QString meminfo = readFile("/proc/meminfo");
long total = 0, avail = 0;
for (const QString &line : meminfo.split('\n')) {
if (line.startsWith("MemTotal:")) total = line.split(QRegExp("\\s+"))[1].toLong();
if (line.startsWith("MemAvailable:")) avail = line.split(QRegExp("\\s+"))[1].toLong();
}
if (total > 0) {
long used = total - avail;
d.ram = QString("%1 / %2 MB").arg(used / 1024).arg(total / 1024);
}
// Load from /proc/loadavg
QString loadavg = readFile("/proc/loadavg");
QStringList parts = loadavg.split(' ');
if (parts.size() >= 3) {
d.load = QString("%1 %2 %3").arg(parts[0], parts[1], parts[2]);
}
// Temperature
QString temps = shellCmd("cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -rn | head -1");
if (!temps.isEmpty()) {
d.temp_c = temps.toLong() / 1000.0f;
d.temp = QString("%1\u00B0C").arg(d.temp_c, 0, 'f', 1);
}
// Fan
QString fan = shellCmd("cat /sys/class/hwmon/hwmon*/fan1_input 2>/dev/null | head -1");
if (fan.isEmpty()) fan = readFile("/dev/shm/params/d/LastFanSpeed");
d.fan = fan.isEmpty() ? QString::fromUtf8("\u2014") : fan + " RPM";
// Network
d.ip = shellCmd("ip route get 1.1.1.1 2>/dev/null | head -1 | awk '{print $7}'");
d.wifi = shellCmd("iwconfig wlan0 2>/dev/null | grep -oP 'ESSID:\"\\K[^\"]*'");
// VPN
QString tun = shellCmd("ip link show tun0 2>/dev/null | head -1");
if (tun.contains("UP")) {
d.vpn_status = "up";
d.vpn_ip = shellCmd("ip addr show tun0 2>/dev/null | grep 'inet ' | awk '{print $2}'");
}
// GPS
d.gps = readFile("/data/params/d/LastGPSPosition");
// Telemetry
d.telemetry = readFile("/data/params/d/TelemetryEnabled");
// Dashcam
QString dashcam_pid = shellCmd("pgrep -x dashcamd");
d.dashcam_status = dashcam_pid.isEmpty() ? "stopped" : "recording";
d.dashcam_size = shellCmd("du -sh /data/media/0/videos 2>/dev/null | awk '{print $1}'");
// Panda: checked on UI thread in applyResults() via scene.pandaType
return d;
}
StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) {
QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(50, 60, 50, 40);
layout->setSpacing(0);
// Status rows
auto makeRow = [&](const QString &label) -> QLabel* {
QHBoxLayout *row = new QHBoxLayout();
row->setContentsMargins(20, 0, 20, 0);
QLabel *name = new QLabel(label);
name->setStyleSheet("color: grey; font-size: 38px;");
name->setFixedWidth(350);
row->addWidget(name);
QLabel *value = new QLabel("");
value->setStyleSheet("color: white; font-size: 38px;");
row->addWidget(value);
row->addStretch();
layout->addLayout(row);
layout->addSpacing(12);
return value;
};
time_label = makeRow("Time");
storage_label = makeRow("Storage");
ram_label = makeRow("Memory");
load_label = makeRow("Load");
temp_label = makeRow("Temperature");
fan_label = makeRow("Fan Speed");
panda_label = makeRow("Panda");
ip_label = makeRow("IP Address");
wifi_label = makeRow("WiFi");
vpn_label = makeRow("VPN");
gps_label = makeRow("GPS");
telemetry_label = makeRow("Telemetry");
dashcam_label = makeRow("Dashcam");
layout->addStretch();
setStyleSheet("StatusWindow { background-color: black; }");
connect(&watcher, &QFutureWatcher<StatusData>::finished, this, &StatusWindow::applyResults);
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &StatusWindow::kickRefresh);
timer->start(1000);
}
void StatusWindow::kickRefresh() {
if (!isVisible() || collecting) return;
collecting = true;
watcher.setFuture(QtConcurrent::run(collectStatus));
}
void StatusWindow::applyResults() {
collecting = false;
StatusData d = watcher.result();
time_label->setText(d.time);
storage_label->setText(d.storage);
if (!d.ram.isEmpty()) ram_label->setText(d.ram);
if (!d.load.isEmpty()) load_label->setText(d.load);
if (!d.temp.isEmpty()) {
temp_label->setText(d.temp);
temp_label->setStyleSheet(d.temp_c > 70 ? "color: #ff4444; font-size: 38px;" :
d.temp_c > 55 ? "color: #ffaa00; font-size: 38px;" :
"color: white; font-size: 38px;");
}
fan_label->setText(d.fan);
// Panda: same check as sidebar — read scene.pandaType on UI thread
if (uiState()->scene.pandaType != cereal::PandaState::PandaType::UNKNOWN) {
panda_label->setText("Connected");
panda_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else {
panda_label->setText("Not connected");
panda_label->setStyleSheet("color: #ff4444; font-size: 38px;");
}
ip_label->setText(d.ip.isEmpty() ? "No connection" : d.ip);
wifi_label->setText(d.wifi.isEmpty() ? "Not connected" : d.wifi);
if (d.vpn_status == "up") {
vpn_label->setText("Connected (" + d.vpn_ip + ")");
vpn_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else {
vpn_label->setText("Not connected");
vpn_label->setStyleSheet("color: #ff4444; font-size: 38px;");
}
if (d.gps.isEmpty()) {
gps_label->setText("No fix");
gps_label->setStyleSheet("color: #ff4444; font-size: 38px;");
} else {
gps_label->setText(d.gps);
gps_label->setStyleSheet("color: white; font-size: 38px;");
}
if (d.telemetry == "1") {
telemetry_label->setText("Enabled");
telemetry_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else {
telemetry_label->setText("Disabled");
telemetry_label->setStyleSheet("color: grey; font-size: 38px;");
}
if (d.dashcam_status == "recording") {
QString text = "Recording";
if (!d.dashcam_size.isEmpty()) text += " (" + d.dashcam_size + ")";
dashcam_label->setText(text);
dashcam_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else {
dashcam_label->setText("Stopped");
dashcam_label->setStyleSheet("color: #ff4444; font-size: 38px;");
}
}
void StatusWindow::mousePressEvent(QMouseEvent *e) {
emit closeStatus();
}