#include "selfdrive/ui/qt/window.h" #include #include #include #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(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(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(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, collected on background thread #include #include #include #include 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"); // 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"); layout->addStretch(); setStyleSheet("StatusWindow { background-color: black; }"); connect(&watcher, &QFutureWatcher::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;"); } } void StatusWindow::mousePressEvent(QMouseEvent *e) { emit closeStatus(); }