- SIGSEGV/SIGABRT crash handler in ui/main.cc prints stack trace to stderr - Fixed onroad crash: guard update_model() against empty model position data (was dereferencing end()-1 on empty list when modeld not running in bench) - Status window: added device temperature and fan speed - Interactive timeout returns to splash/onroad (not ClearPilotPanel) - bench_cmd dump detects crash loops via UI process uptime check - bench_cmd wait_ready timeout increased to 20s - Restored camerad to bench ignore list (not needed for UI testing) - Updated CLAUDE.md with crash debugging procedures Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
339 lines
11 KiB
C++
Executable File
339 lines
11 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 ¶m) {
|
|
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: {
|
|
// 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, refreshed every second
|
|
|
|
#include <QFile>
|
|
#include <QProcess>
|
|
#include <QDateTime>
|
|
|
|
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();
|
|
}
|
|
|
|
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");
|
|
ip_label = makeRow("IP Address");
|
|
wifi_label = makeRow("WiFi");
|
|
vpn_label = makeRow("VPN");
|
|
gps_label = makeRow("GPS");
|
|
telemetry_label = makeRow("Telemetry");
|
|
|
|
layout->addStretch();
|
|
|
|
setStyleSheet("StatusWindow { background-color: black; }");
|
|
|
|
// Refresh every second
|
|
QTimer *timer = new QTimer(this);
|
|
connect(timer, &QTimer::timeout, this, &StatusWindow::refresh);
|
|
timer->start(1000);
|
|
refresh();
|
|
}
|
|
|
|
void StatusWindow::refresh() {
|
|
// Time
|
|
time_label->setText(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
|
|
|
|
// Storage
|
|
QString df = shellCmd("df -h /data | tail -1 | awk '{print $3 \" / \" $2 \" (\" $5 \" used)\"}'");
|
|
storage_label->setText(df);
|
|
|
|
// RAM
|
|
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;
|
|
ram_label->setText(QString("%1 / %2 MB").arg(used / 1024).arg(total / 1024));
|
|
}
|
|
|
|
// Load
|
|
QString loadavg = readFile("/proc/loadavg");
|
|
QStringList parts = loadavg.split(' ');
|
|
if (parts.size() >= 3) {
|
|
load_label->setText(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()) {
|
|
float temp_c = temps.toLong() / 1000.0;
|
|
temp_label->setText(QString("%1°C").arg(temp_c, 0, 'f', 1));
|
|
temp_label->setStyleSheet(temp_c > 70 ? "color: #ff4444; font-size: 38px;" :
|
|
temp_c > 55 ? "color: #ffaa00; font-size: 38px;" :
|
|
"color: white; font-size: 38px;");
|
|
}
|
|
|
|
// Fan speed
|
|
QString fan = shellCmd("cat /sys/class/hwmon/hwmon*/fan1_input 2>/dev/null | head -1");
|
|
if (fan.isEmpty()) {
|
|
// Try reading from deviceState param as fallback
|
|
fan = shellCmd("cat /dev/shm/params/d/LastFanSpeed 2>/dev/null");
|
|
}
|
|
fan_label->setText(fan.isEmpty() ? "—" : fan + " RPM");
|
|
|
|
// IP + WiFi
|
|
QString ip = shellCmd("ip route get 1.1.1.1 2>/dev/null | head -1 | awk '{print $7}'");
|
|
ip_label->setText(ip.isEmpty() ? "No connection" : ip);
|
|
|
|
QString essid = shellCmd("iwconfig wlan0 2>/dev/null | grep -oP 'ESSID:\"\\K[^\"]*'");
|
|
wifi_label->setText(essid.isEmpty() ? "Not connected" : essid);
|
|
|
|
// VPN
|
|
QString tun = shellCmd("ip link show tun0 2>/dev/null | head -1");
|
|
if (tun.contains("UP")) {
|
|
QString vpn_ip = shellCmd("ip addr show tun0 2>/dev/null | grep 'inet ' | awk '{print $2}'");
|
|
vpn_label->setText("Connected (" + vpn_ip + ")");
|
|
vpn_label->setStyleSheet("color: #17c44d; font-size: 38px;");
|
|
} else {
|
|
vpn_label->setText("Not connected");
|
|
vpn_label->setStyleSheet("color: #ff4444; font-size: 38px;");
|
|
}
|
|
|
|
// GPS
|
|
QString gps_raw = shellCmd("cat /data/params/d/LastGPSPosition 2>/dev/null");
|
|
if (gps_raw.isEmpty()) {
|
|
gps_label->setText("No fix");
|
|
gps_label->setStyleSheet("color: #ff4444; font-size: 38px;");
|
|
} else {
|
|
gps_label->setText(gps_raw);
|
|
gps_label->setStyleSheet("color: white; font-size: 38px;");
|
|
}
|
|
|
|
// Telemetry
|
|
QString telem = shellCmd("cat /data/params/d/TelemetryEnabled 2>/dev/null");
|
|
if (telem == "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;");
|
|
}
|
|
}
|
|
|
|
void StatusWindow::mousePressEvent(QMouseEvent *e) {
|
|
emit closeStatus();
|
|
}
|