bench mode, ClearPilot menu, status window, UI introspection RPC
Bench mode (--bench flag): - bench_onroad.py publishes fake vehicle state as managed process - manager blocks real car processes (pandad, thermald, controlsd, etc.) - bench_cmd.py for setting params and querying UI state - BLOCKER: UI segfaults (SIGSEGV) when OnroadWindow becomes visible without camera frames — need to make CameraWidget handle missing VisionIPC gracefully before bench drive mode works ClearPilot menu: - Replaced grid launcher with sidebar settings panel (ClearPilotPanel) - Sidebar: Home, Dashcam, Debug - Home panel: Status and System Settings buttons - Debug panel: telemetry logging toggle (ParamControl) - Tap from any view (splash, onroad) opens ClearPilotPanel - Back button returns to splash/onroad Status window: - Live system stats refreshing every 1 second - Storage, RAM, load, IP, WiFi, VPN, GPS, time, telemetry status - Tap anywhere to close, returns to previous view - Honors interactive timeout UI introspection RPC: - ZMQ REP server at ipc:///tmp/clearpilot_ui_rpc - Dumps full widget tree with visibility, geometry, stacked indices - bench_cmd dump queries it, detects crash loops via process uptime - ui_dump.py standalone tool Other: - Telemetry toggle wired to TelemetryEnabled param with disk space guard - Telemetry disabled on every manager start - Blinking red circle on onroad UI when telemetry recording - Fixed showDriverView overriding park/drive transitions every frame - Fixed offroadTransition sidebar visibility race in MainWindow - launch_openpilot.sh: cd to /data/openpilot, --bench flag support Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,7 +5,9 @@
|
||||
#include <QStackedWidget>
|
||||
#include <QVBoxLayout>
|
||||
|
||||
#include "common/swaglog.h"
|
||||
#include "selfdrive/ui/qt/util.h"
|
||||
#include "selfdrive/ui/qt/widgets/scrollview.h"
|
||||
|
||||
// HomeWindow: the container for the offroad and onroad UIs
|
||||
|
||||
@@ -25,8 +27,17 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
|
||||
slayout = new QStackedLayout();
|
||||
main_layout->addLayout(slayout);
|
||||
|
||||
home = new OffroadHome(this);
|
||||
QObject::connect(home, &OffroadHome::openSettings, this, &HomeWindow::openSettings);
|
||||
home = new ClearPilotPanel(this);
|
||||
QObject::connect(home, &ClearPilotPanel::openSettings, this, &HomeWindow::openSettings);
|
||||
QObject::connect(home, &ClearPilotPanel::openStatus, this, &HomeWindow::openStatus);
|
||||
QObject::connect(home, &ClearPilotPanel::closePanel, [=]() {
|
||||
// Return to splash or onroad depending on state
|
||||
if (uiState()->scene.started) {
|
||||
slayout->setCurrentWidget(onroad);
|
||||
} else {
|
||||
slayout->setCurrentWidget(ready);
|
||||
}
|
||||
});
|
||||
slayout->addWidget(home);
|
||||
|
||||
onroad = new OnroadWindow(this);
|
||||
@@ -67,10 +78,10 @@ void HomeWindow::updateState(const UIState &s) {
|
||||
// CLEARPILOT: show splash screen when onroad but in park
|
||||
bool parked = s.scene.parked;
|
||||
if (parked && !was_parked_onroad) {
|
||||
// just shifted into park — show splash
|
||||
LOGW("CLP UI: park transition -> showing splash");
|
||||
slayout->setCurrentWidget(ready);
|
||||
} else if (!parked && was_parked_onroad) {
|
||||
// just shifted out of park — show onroad camera
|
||||
LOGW("CLP UI: drive transition -> showing onroad");
|
||||
slayout->setCurrentWidget(onroad);
|
||||
}
|
||||
was_parked_onroad = parked;
|
||||
@@ -90,11 +101,13 @@ void HomeWindow::updateState(const UIState &s) {
|
||||
void HomeWindow::offroadTransition(bool offroad) {
|
||||
sidebar->setVisible(false);
|
||||
if (offroad) {
|
||||
LOGW("CLP UI: offroad transition -> showing splash");
|
||||
was_parked_onroad = false;
|
||||
slayout->setCurrentWidget(ready);
|
||||
} else {
|
||||
// CLEARPILOT: start onroad in splash — updateState will switch to
|
||||
// camera view once the car shifts out of park
|
||||
LOGW("CLP UI: onroad transition -> showing splash (parked)");
|
||||
was_parked_onroad = true;
|
||||
slayout->setCurrentWidget(ready);
|
||||
}
|
||||
@@ -102,34 +115,26 @@ void HomeWindow::offroadTransition(bool offroad) {
|
||||
|
||||
void HomeWindow::showDriverView(bool show, bool started) {
|
||||
if (show) {
|
||||
LOGW("CLP UI: showDriverView(true) -> driver_view");
|
||||
emit closeSettings();
|
||||
slayout->setCurrentWidget(driver_view);
|
||||
sidebar->setVisible(show == false);
|
||||
} else {
|
||||
if (started) {
|
||||
slayout->setCurrentWidget(onroad);
|
||||
sidebar->setVisible(params.getBool("Sidebar"));
|
||||
} else {
|
||||
slayout->setCurrentWidget(home);
|
||||
sidebar->setVisible(show == false);
|
||||
}
|
||||
sidebar->setVisible(false);
|
||||
} else if (!started) {
|
||||
// Offroad, not started — show home menu
|
||||
slayout->setCurrentWidget(home);
|
||||
sidebar->setVisible(false);
|
||||
}
|
||||
// CLEARPILOT: when started, don't touch slayout here —
|
||||
// updateState handles park->splash and drive->onroad transitions
|
||||
}
|
||||
|
||||
void HomeWindow::mousePressEvent(QMouseEvent* e) {
|
||||
// CLEARPILOT todo - tap on main goes straight to settings
|
||||
// Unless we click a debug widget.
|
||||
|
||||
// CLEARPILOT - click ready shows home (no sidebar)
|
||||
if (!onroad->isVisible() && ready->isVisible()) {
|
||||
// CLEARPILOT: tap from any view goes to ClearPilotPanel
|
||||
if (ready->isVisible() || onroad->isVisible()) {
|
||||
LOGW("CLP UI: tap -> showing ClearPilotPanel");
|
||||
sidebar->setVisible(false);
|
||||
slayout->setCurrentWidget(home);
|
||||
}
|
||||
|
||||
// Todo: widgets
|
||||
if (onroad->isVisible()) {
|
||||
emit openSettings();
|
||||
}
|
||||
}
|
||||
|
||||
void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
|
||||
@@ -137,59 +142,149 @@ void HomeWindow::mouseDoubleClickEvent(QMouseEvent* e) {
|
||||
// const SubMaster &sm = *(uiState()->sm);
|
||||
}
|
||||
|
||||
// CLEARPILOT: OffroadHome — clean grid launcher
|
||||
// CLEARPILOT: ClearPilotPanel — settings-style sidebar menu
|
||||
|
||||
OffroadHome::OffroadHome(QWidget* parent) : QFrame(parent) {
|
||||
QVBoxLayout* main_layout = new QVBoxLayout(this);
|
||||
main_layout->setContentsMargins(80, 80, 80, 80);
|
||||
main_layout->setSpacing(0);
|
||||
static const char *clpSidebarBtnStyle = R"(
|
||||
QPushButton {
|
||||
color: grey;
|
||||
border: none;
|
||||
background: none;
|
||||
font-size: 65px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:checked {
|
||||
color: white;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
color: #ADADAD;
|
||||
}
|
||||
)";
|
||||
|
||||
// grid of launcher buttons
|
||||
QGridLayout *grid = new QGridLayout();
|
||||
grid->setSpacing(40);
|
||||
ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
|
||||
// Sidebar
|
||||
QWidget *sidebar_widget = new QWidget;
|
||||
QVBoxLayout *sidebar_layout = new QVBoxLayout(sidebar_widget);
|
||||
sidebar_layout->setContentsMargins(50, 50, 100, 50);
|
||||
|
||||
// Dashcam viewer button
|
||||
QPushButton *dashcam_btn = new QPushButton("Dashcam");
|
||||
dashcam_btn->setFixedSize(400, 300);
|
||||
dashcam_btn->setStyleSheet(R"(
|
||||
// Close button
|
||||
QPushButton *close_btn = new QPushButton("← Back");
|
||||
close_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #333333;
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
border-radius: 25px;
|
||||
background: #292929;
|
||||
font-size: 50px;
|
||||
font-weight: 500;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #555555;
|
||||
color: #ADADAD;
|
||||
}
|
||||
)");
|
||||
grid->addWidget(dashcam_btn, 0, 0);
|
||||
close_btn->setFixedSize(300, 125);
|
||||
sidebar_layout->addSpacing(10);
|
||||
sidebar_layout->addWidget(close_btn, 0, Qt::AlignRight);
|
||||
QObject::connect(close_btn, &QPushButton::clicked, [=]() { emit closePanel(); });
|
||||
|
||||
// Settings button
|
||||
QPushButton *settings_btn = new QPushButton("Settings");
|
||||
settings_btn->setFixedSize(400, 300);
|
||||
settings_btn->setStyleSheet(R"(
|
||||
// Panel content area
|
||||
panel_widget = new QStackedWidget();
|
||||
|
||||
// Define panels: sidebar label -> content widget
|
||||
// Home panel: buttons for Status and System Settings
|
||||
QWidget *home_panel = new QWidget(this);
|
||||
QVBoxLayout *home_layout = new QVBoxLayout(home_panel);
|
||||
home_layout->setContentsMargins(50, 25, 50, 25);
|
||||
home_layout->setSpacing(20);
|
||||
|
||||
QPushButton *status_btn = new QPushButton("Status");
|
||||
status_btn->setFixedHeight(120);
|
||||
status_btn->setStyleSheet(R"(
|
||||
QPushButton {
|
||||
background-color: #333333;
|
||||
background-color: #393939;
|
||||
color: white;
|
||||
border-radius: 20px;
|
||||
font-size: 48px;
|
||||
font-weight: 600;
|
||||
}
|
||||
QPushButton:pressed {
|
||||
background-color: #555555;
|
||||
border-radius: 15px;
|
||||
font-size: 50px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
padding-left: 30px;
|
||||
}
|
||||
QPushButton:pressed { background-color: #4a4a4a; }
|
||||
)");
|
||||
QObject::connect(settings_btn, &QPushButton::clicked, [=]() { emit openSettings(); });
|
||||
grid->addWidget(settings_btn, 0, 1);
|
||||
QObject::connect(status_btn, &QPushButton::clicked, [=]() { emit openStatus(); });
|
||||
home_layout->addWidget(status_btn);
|
||||
|
||||
main_layout->addStretch();
|
||||
main_layout->addLayout(grid);
|
||||
main_layout->addStretch();
|
||||
QPushButton *sysset_btn = new QPushButton("System Settings");
|
||||
sysset_btn->setFixedHeight(120);
|
||||
sysset_btn->setStyleSheet(status_btn->styleSheet());
|
||||
QObject::connect(sysset_btn, &QPushButton::clicked, [=]() { emit openSettings(); });
|
||||
home_layout->addWidget(sysset_btn);
|
||||
|
||||
home_layout->addStretch();
|
||||
|
||||
// Dashcam panel: placeholder
|
||||
QWidget *dashcam_panel = new QWidget(this);
|
||||
QVBoxLayout *dash_layout = new QVBoxLayout(dashcam_panel);
|
||||
dash_layout->setContentsMargins(50, 25, 50, 25);
|
||||
QLabel *dash_label = new QLabel("Dashcam viewer coming soon");
|
||||
dash_label->setStyleSheet("color: grey; font-size: 40px;");
|
||||
dash_label->setAlignment(Qt::AlignCenter);
|
||||
dash_layout->addWidget(dash_label);
|
||||
dash_layout->addStretch();
|
||||
|
||||
// Debug panel
|
||||
ListWidget *debug_panel = new ListWidget(this);
|
||||
debug_panel->setContentsMargins(50, 25, 50, 25);
|
||||
|
||||
auto *telemetry_toggle = new ParamControl("TelemetryEnabled", "Telemetry Logging",
|
||||
"Record telemetry data to CSV in the session log directory. "
|
||||
"Captures only changed values for efficiency.", "", this);
|
||||
debug_panel->addItem(telemetry_toggle);
|
||||
|
||||
// Register panels with sidebar buttons
|
||||
QList<QPair<QString, QWidget *>> panels = {
|
||||
{"Home", home_panel},
|
||||
{"Dashcam", dashcam_panel},
|
||||
{"Debug", debug_panel},
|
||||
};
|
||||
|
||||
for (auto &[name, panel] : panels) {
|
||||
QPushButton *btn = new QPushButton(name);
|
||||
btn->setCheckable(true);
|
||||
btn->setStyleSheet(clpSidebarBtnStyle);
|
||||
btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
|
||||
sidebar_layout->addWidget(btn, 0, Qt::AlignRight);
|
||||
|
||||
ScrollView *panel_frame = new ScrollView(panel, this);
|
||||
panel_widget->addWidget(panel_frame);
|
||||
|
||||
QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() {
|
||||
btn->setChecked(true);
|
||||
panel_widget->setCurrentWidget(w);
|
||||
});
|
||||
}
|
||||
|
||||
// Select Home by default
|
||||
if (auto *first_btn = sidebar_widget->findChild<QPushButton *>(QString(), Qt::FindDirectChildrenOnly)) {
|
||||
// Skip close_btn, find first sidebar btn
|
||||
}
|
||||
panel_widget->setCurrentIndex(0);
|
||||
|
||||
// Main layout: sidebar + panels
|
||||
QHBoxLayout *main_layout = new QHBoxLayout(this);
|
||||
sidebar_widget->setFixedWidth(500);
|
||||
main_layout->addWidget(sidebar_widget);
|
||||
main_layout->addWidget(panel_widget);
|
||||
|
||||
setStyleSheet(R"(
|
||||
OffroadHome {
|
||||
* {
|
||||
color: white;
|
||||
font-size: 50px;
|
||||
}
|
||||
ClearPilotPanel {
|
||||
background-color: black;
|
||||
}
|
||||
QStackedWidget, ScrollView {
|
||||
background-color: #292929;
|
||||
border-radius: 30px;
|
||||
}
|
||||
)");
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
#include <QStackedLayout>
|
||||
#include <QStackedWidget>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
@@ -16,14 +17,19 @@
|
||||
#include "selfdrive/ui/qt/widgets/offroad_alerts.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
class OffroadHome : public QFrame {
|
||||
class ClearPilotPanel : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit OffroadHome(QWidget* parent = 0);
|
||||
explicit ClearPilotPanel(QWidget* parent = 0);
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
void openStatus();
|
||||
void closePanel();
|
||||
|
||||
private:
|
||||
QStackedWidget *panel_widget;
|
||||
};
|
||||
|
||||
class HomeWindow : public QWidget {
|
||||
@@ -35,6 +41,7 @@ public:
|
||||
|
||||
signals:
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
void openStatus();
|
||||
void closeSettings();
|
||||
|
||||
public slots:
|
||||
@@ -49,7 +56,7 @@ protected:
|
||||
|
||||
private:
|
||||
Sidebar *sidebar;
|
||||
OffroadHome *home;
|
||||
ClearPilotPanel *home;
|
||||
OnroadWindow *onroad;
|
||||
DriverViewWindow *driver_view;
|
||||
QStackedLayout *slayout;
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <sstream>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDebug>
|
||||
#include <QMouseEvent>
|
||||
|
||||
@@ -402,6 +403,17 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) {
|
||||
Hardware::set_display_power(true);
|
||||
}
|
||||
|
||||
// CLEARPILOT: blinking red circle when telemetry is recording
|
||||
if (Params().get("TelemetryEnabled") == "1") {
|
||||
// Blink: visible for 500ms, hidden for 500ms
|
||||
int phase = (QDateTime::currentMSecsSinceEpoch() / 500) % 2;
|
||||
if (phase == 0) {
|
||||
p.setPen(Qt::NoPen);
|
||||
p.setBrush(QColor(220, 30, 30));
|
||||
p.drawEllipse(width() - 150, 50, 100, 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Header gradient
|
||||
QLinearGradient bg(0, UI_HEADER_HEIGHT - (UI_HEADER_HEIGHT / 2.5), 0, UI_HEADER_HEIGHT);
|
||||
bg.setColorAt(0, QColor::fromRgbF(0, 0, 0, 0.45));
|
||||
|
||||
Binary file not shown.
@@ -1,6 +1,9 @@
|
||||
#include "selfdrive/ui/qt/window.h"
|
||||
|
||||
#include <QFontDatabase>
|
||||
#include <QMouseEvent>
|
||||
|
||||
#include <zmq.h>
|
||||
|
||||
#include "system/hardware/hw.h"
|
||||
|
||||
@@ -11,6 +14,7 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
|
||||
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);
|
||||
@@ -24,6 +28,11 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
|
||||
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, [=]() {
|
||||
@@ -35,11 +44,14 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
|
||||
if (!offroad) {
|
||||
closeSettings();
|
||||
// CLEARPILOT: just switch to homeWindow, don't show sidebar
|
||||
// HomeWindow::offroadTransition handles the internal view
|
||||
main_layout->setCurrentWidget(homeWindow);
|
||||
}
|
||||
});
|
||||
QObject::connect(device(), &Device::interactiveTimeout, [=]() {
|
||||
if (main_layout->currentWidget() == settingsWindow) {
|
||||
if (main_layout->currentWidget() == settingsWindow ||
|
||||
main_layout->currentWidget() == statusWindow) {
|
||||
closeSettings();
|
||||
}
|
||||
});
|
||||
@@ -63,6 +75,74 @@ MainWindow::MainWindow(QWidget *parent) : QWidget(parent) {
|
||||
}
|
||||
)");
|
||||
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) {
|
||||
@@ -70,6 +150,10 @@ void MainWindow::openSettings(int index, const QString ¶m) {
|
||||
settingsWindow->setCurrentPanel(index, param);
|
||||
}
|
||||
|
||||
void MainWindow::openStatus() {
|
||||
main_layout->setCurrentWidget(statusWindow);
|
||||
}
|
||||
|
||||
void MainWindow::closeSettings() {
|
||||
main_layout->setCurrentWidget(homeWindow);
|
||||
|
||||
@@ -96,3 +180,138 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
|
||||
}
|
||||
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");
|
||||
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]));
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
#pragma once
|
||||
|
||||
#include <QStackedLayout>
|
||||
#include <QLabel>
|
||||
#include <QSocketNotifier>
|
||||
#include <QTimer>
|
||||
#include <QWidget>
|
||||
|
||||
#include "selfdrive/ui/qt/home.h"
|
||||
#include "selfdrive/ui/qt/offroad/onboarding.h"
|
||||
#include "selfdrive/ui/qt/offroad/settings.h"
|
||||
|
||||
class StatusWindow : public QFrame {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit StatusWindow(QWidget *parent = 0);
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
||||
signals:
|
||||
void closeStatus();
|
||||
|
||||
private slots:
|
||||
void refresh();
|
||||
|
||||
private:
|
||||
QLabel *storage_label;
|
||||
QLabel *ram_label;
|
||||
QLabel *load_label;
|
||||
QLabel *ip_label;
|
||||
QLabel *wifi_label;
|
||||
QLabel *vpn_label;
|
||||
QLabel *gps_label;
|
||||
QLabel *time_label;
|
||||
QLabel *telemetry_label;
|
||||
};
|
||||
|
||||
class MainWindow : public QWidget {
|
||||
Q_OBJECT
|
||||
|
||||
@@ -16,13 +46,24 @@ public:
|
||||
private:
|
||||
bool eventFilter(QObject *obj, QEvent *event) override;
|
||||
void openSettings(int index = 0, const QString ¶m = "");
|
||||
void openStatus();
|
||||
void closeSettings();
|
||||
QString dumpWidgetTree(QWidget *w, int depth = 0);
|
||||
|
||||
QStackedLayout *main_layout;
|
||||
HomeWindow *homeWindow;
|
||||
SettingsWindow *settingsWindow;
|
||||
StatusWindow *statusWindow;
|
||||
OnboardingWindow *onboardingWindow;
|
||||
|
||||
// CLEARPILOT: UI introspection RPC
|
||||
void *zmq_ctx = nullptr;
|
||||
void *zmq_sock = nullptr;
|
||||
QSocketNotifier *rpc_notifier = nullptr;
|
||||
|
||||
// FrogPilot variables
|
||||
Params params;
|
||||
|
||||
private slots:
|
||||
void handleRpcRequest();
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user