UI overhaul, VPN management, controlsd fix, screen recorder removal
- Fix controlsd crash: self.state.name AttributeError when state is int - Move telemetry tlog import to module level in carstate.py (was per-frame) - Remove FrogPilot screen recorder from UI (was crashing OMX on init) - Ready screen: boot logo background, 8-bit READY! sprite, error states (panda not connected, car not recognized) with 10s startup grace period - ClearPilot menu: always opens to General, QButtonGroup for sidebar, System Status uses ButtonControl, VPN toggle with process control - Sidebar hidden on construction (no flash before splash) - Status window: threaded data collection (QtConcurrent), panda detection via scene.pandaType (SPI, not USB), only refreshes when visible - VPN: moved to system/clearpilot/, SIGTERM graceful shutdown, keepalive ping through tunnel, killall openvpn on disable, launched from launch_openpilot.sh instead of continue.sh - Disable gpsd and dashcamd temporarily for perf testing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,6 +13,7 @@ from openpilot.selfdrive.car.hyundai.hyundaicanfd import CanBus
|
||||
from openpilot.selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CAN_GEARS, CAMERA_SCC_CAR, \
|
||||
CANFD_CAR, Buttons, CarControllerParams
|
||||
from openpilot.selfdrive.car.interfaces import CarStateBase
|
||||
from openpilot.selfdrive.clearpilot.telemetry import tlog
|
||||
|
||||
PREV_BUTTON_SAMPLES = 8
|
||||
CLUSTER_SAMPLE_RATE = 20 # frames
|
||||
@@ -421,8 +422,6 @@ class CarState(CarStateBase):
|
||||
self.params_memory.put_float("CarSpeedLimit", self.calculate_speed_limit(cp, cp_cam) * speed_factor)
|
||||
|
||||
# CLEARPILOT: telemetry logging
|
||||
from openpilot.selfdrive.clearpilot.telemetry import tlog
|
||||
|
||||
speed_limit_bus = cp if self.CP.flags & HyundaiFlags.CANFD_HDA2 else cp_cam
|
||||
scc = cp_cam.vl["SCC_CONTROL"] if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else cp.vl["SCC_CONTROL"]
|
||||
cluster = speed_limit_bus.vl["CLUSTER_SPEED_LIMIT"]
|
||||
|
||||
Binary file not shown.
BIN
selfdrive/clearpilot/theme/clearpilot/images/ready_text.png
Normal file
BIN
selfdrive/clearpilot/theme/clearpilot/images/ready_text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.4 KiB |
@@ -632,7 +632,7 @@ class Controls:
|
||||
|
||||
# CLEARPILOT: log engagement state for debugging cruise desync issues
|
||||
tlog("engage", {
|
||||
"state": self.state.name,
|
||||
"state": self.state.name if hasattr(self.state, 'name') else str(self.state),
|
||||
"enabled": self.enabled,
|
||||
"active": self.active,
|
||||
"cruise_enabled": CS.cruiseState.enabled,
|
||||
|
||||
@@ -83,8 +83,9 @@ def manager_init(frogpilot_functions) -> None:
|
||||
params_storage = Params("/persist/params")
|
||||
params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START)
|
||||
|
||||
# CLEARPILOT: always start with telemetry disabled
|
||||
# CLEARPILOT: always start with telemetry disabled, VPN enabled
|
||||
params.put("TelemetryEnabled", "0")
|
||||
params.put("VpnEnabled", "1")
|
||||
params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION)
|
||||
params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
|
||||
if is_release_branch():
|
||||
|
||||
@@ -85,7 +85,7 @@ procs = [
|
||||
PythonProcess("deleter", "system.loggerd.deleter", always_run),
|
||||
PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)),
|
||||
# PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI),
|
||||
PythonProcess("gpsd", "system.clearpilot.gpsd", qcomgps, enabled=TICI),
|
||||
# PythonProcess("gpsd", "system.clearpilot.gpsd", qcomgps, enabled=TICI), # DISABLED: testing perf
|
||||
# PythonProcess("ugpsd", "system.ugpsd", only_onroad, enabled=TICI),
|
||||
#PythonProcess("navd", "selfdrive.navd.navd", only_onroad),
|
||||
PythonProcess("pandad", "selfdrive.boardd.pandad", always_run),
|
||||
@@ -110,7 +110,7 @@ procs = [
|
||||
PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run),
|
||||
|
||||
# ClearPilot processes
|
||||
NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], dashcam_should_run),
|
||||
# NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], dashcam_should_run), # DISABLED: testing perf
|
||||
PythonProcess("telemetryd", "selfdrive.clearpilot.telemetryd", always_run),
|
||||
PythonProcess("bench_onroad", "selfdrive.clearpilot.bench_onroad", always_run, enabled=BENCH_MODE),
|
||||
]
|
||||
|
||||
@@ -39,7 +39,7 @@ qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc",
|
||||
"qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc",
|
||||
"qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc",
|
||||
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc",
|
||||
"../frogpilot/screenrecorder/omx_encoder.cc", "../frogpilot/screenrecorder/screenrecorder.cc",
|
||||
"../frogpilot/screenrecorder/omx_encoder.cc", # kept for dashcamd .o dependency
|
||||
"qt/ready.cc"]
|
||||
|
||||
# build translation files
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "selfdrive/ui/qt/home.h"
|
||||
|
||||
#include <cstdlib>
|
||||
#include <QHBoxLayout>
|
||||
#include <QMouseEvent>
|
||||
#include <QStackedWidget>
|
||||
@@ -26,6 +27,7 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
|
||||
main_layout->setSpacing(0);
|
||||
|
||||
sidebar = new Sidebar(this);
|
||||
sidebar->setVisible(false);
|
||||
main_layout->addWidget(sidebar);
|
||||
QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings);
|
||||
QObject::connect(sidebar, &Sidebar::openOnroad, this, &HomeWindow::showOnroad);
|
||||
@@ -52,6 +54,7 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
|
||||
// CLEARPILOT
|
||||
ready = new ReadyWindow(this);
|
||||
slayout->addWidget(ready);
|
||||
slayout->setCurrentWidget(ready); // show splash immediately, not ClearPilotPanel
|
||||
|
||||
driver_view = new DriverViewWindow(this);
|
||||
connect(driver_view, &DriverViewWindow::done, [=] {
|
||||
@@ -139,6 +142,7 @@ void HomeWindow::mousePressEvent(QMouseEvent* e) {
|
||||
if (ready->isVisible() || onroad->isVisible()) {
|
||||
LOGW("CLP UI: tap -> showing ClearPilotPanel");
|
||||
sidebar->setVisible(false);
|
||||
home->resetToGeneral();
|
||||
slayout->setCurrentWidget(home);
|
||||
}
|
||||
}
|
||||
@@ -166,18 +170,7 @@ static const char *clpSidebarBtnStyle = R"(
|
||||
}
|
||||
)";
|
||||
|
||||
static const char *clpActionBtnStyle = R"(
|
||||
QPushButton {
|
||||
background-color: #393939;
|
||||
color: white;
|
||||
border-radius: 15px;
|
||||
font-size: 50px;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
padding-left: 30px;
|
||||
}
|
||||
QPushButton:pressed { background-color: #4a4a4a; }
|
||||
)";
|
||||
// clpActionBtnStyle removed — no longer used
|
||||
|
||||
// Shutdown timer: param value -> display label
|
||||
static QString shutdownLabel(int val) {
|
||||
@@ -220,10 +213,8 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
|
||||
general_panel->setContentsMargins(50, 25, 50, 25);
|
||||
|
||||
// Status button
|
||||
QPushButton *status_btn = new QPushButton("Status");
|
||||
status_btn->setFixedHeight(120);
|
||||
status_btn->setStyleSheet(clpActionBtnStyle);
|
||||
QObject::connect(status_btn, &QPushButton::clicked, [=]() { emit openStatus(); });
|
||||
auto *status_btn = new ButtonControl("System Status", "VIEW", "");
|
||||
connect(status_btn, &ButtonControl::clicked, [=]() { emit openStatus(); });
|
||||
general_panel->addItem(status_btn);
|
||||
|
||||
// Reset Calibration
|
||||
@@ -364,6 +355,18 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
|
||||
"Captures only changed values for efficiency.", "", this);
|
||||
debug_panel->addItem(telemetry_toggle);
|
||||
|
||||
auto *vpn_toggle = new ParamControl("VpnEnabled", "VPN",
|
||||
"Connect to vpn.hanson.xyz for remote SSH access. "
|
||||
"Disabling kills the active tunnel and stops reconnection attempts.", "", this);
|
||||
QObject::connect(vpn_toggle, &ToggleControl::toggleFlipped, [](bool on) {
|
||||
if (on) {
|
||||
std::system("sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &'");
|
||||
} else {
|
||||
std::system("sudo pkill -TERM -f vpn-monitor.sh; sudo killall openvpn");
|
||||
}
|
||||
});
|
||||
debug_panel->addItem(vpn_toggle);
|
||||
|
||||
// ── Register panels with sidebar buttons ──
|
||||
QList<QPair<QString, QWidget *>> panels = {
|
||||
{"General", general_panel},
|
||||
@@ -372,31 +375,34 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
|
||||
{"Debug", debug_panel},
|
||||
};
|
||||
|
||||
nav_group = new QButtonGroup(this);
|
||||
nav_group->setExclusive(true);
|
||||
|
||||
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);
|
||||
nav_group->addButton(btn);
|
||||
|
||||
// Network panel handles its own scrolling/margins
|
||||
if (name == "Network") {
|
||||
panel_widget->addWidget(panel);
|
||||
QObject::connect(btn, &QPushButton::clicked, [=, w = panel]() {
|
||||
btn->setChecked(true);
|
||||
panel_widget->setCurrentWidget(w);
|
||||
});
|
||||
} else {
|
||||
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 General by default
|
||||
nav_group->buttons().first()->setChecked(true);
|
||||
panel_widget->setCurrentIndex(0);
|
||||
|
||||
// Main layout: sidebar + panels
|
||||
@@ -425,3 +431,8 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
|
||||
#poweroff_btn:pressed { background-color: #FF2424; }
|
||||
)");
|
||||
}
|
||||
|
||||
void ClearPilotPanel::resetToGeneral() {
|
||||
panel_widget->setCurrentIndex(0);
|
||||
nav_group->buttons().first()->setChecked(true);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <QButtonGroup>
|
||||
#include <QFrame>
|
||||
#include <QLabel>
|
||||
#include <QPushButton>
|
||||
@@ -28,8 +29,12 @@ signals:
|
||||
void openStatus();
|
||||
void closePanel();
|
||||
|
||||
public:
|
||||
void resetToGeneral();
|
||||
|
||||
private:
|
||||
QStackedWidget *panel_widget;
|
||||
QButtonGroup *nav_group;
|
||||
};
|
||||
|
||||
class HomeWindow : public QWidget {
|
||||
|
||||
@@ -316,10 +316,7 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* par
|
||||
QHBoxLayout *buttons_layout = new QHBoxLayout();
|
||||
buttons_layout->setSpacing(0);
|
||||
|
||||
// Neokii screen recorder
|
||||
recorder_btn = new ScreenRecorder(this);
|
||||
recorder_btn->setVisible(false);
|
||||
// buttons_layout->addWidget(recorder_btn);
|
||||
// Neokii screen recorder — DISABLED: using dashcamd instead
|
||||
|
||||
QVBoxLayout *top_right_layout = new QVBoxLayout();
|
||||
top_right_layout->setSpacing(0);
|
||||
@@ -1074,8 +1071,7 @@ void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) {
|
||||
drawSLCConfirmation(p);
|
||||
}
|
||||
|
||||
// CLEARPILOT: screen recorder runs invisibly, no UI button shown
|
||||
recorder_btn->setVisible(false);
|
||||
// CLEARPILOT: screen recorder disabled, using dashcamd instead
|
||||
}
|
||||
|
||||
void AnnotatedCameraWidget::drawLeadInfo(QPainter &p) {
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "selfdrive/ui/ui.h"
|
||||
#include "selfdrive/ui/qt/widgets/cameraview.h"
|
||||
|
||||
#include "selfdrive/frogpilot/screenrecorder/screenrecorder.h"
|
||||
// #include "selfdrive/frogpilot/screenrecorder/screenrecorder.h" // DISABLED: using dashcamd instead
|
||||
|
||||
const int btn_size = 192;
|
||||
const int img_size = (btn_size / 4) * 3;
|
||||
@@ -91,8 +91,6 @@ private:
|
||||
Params paramsMemory{"/dev/shm/params"};
|
||||
UIScene &scene;
|
||||
|
||||
ScreenRecorder *recorder_btn;
|
||||
|
||||
QHBoxLayout *bottom_layout;
|
||||
|
||||
bool alwaysOnLateralActive;
|
||||
|
||||
@@ -30,11 +30,12 @@ ReadyWindow::ReadyWindow(QWidget *parent) : QWidget(parent) {
|
||||
|
||||
timer = new QTimer(this);
|
||||
timer->callOnTimeout(this, &ReadyWindow::refresh);
|
||||
uptime.start();
|
||||
}
|
||||
|
||||
void ReadyWindow::showEvent(QShowEvent *event) {
|
||||
refresh();
|
||||
timer->start(5 * 1000);
|
||||
timer->start(2 * 1000);
|
||||
}
|
||||
|
||||
void ReadyWindow::hideEvent(QHideEvent *event) {
|
||||
@@ -43,34 +44,87 @@ void ReadyWindow::hideEvent(QHideEvent *event) {
|
||||
|
||||
void ReadyWindow::paintEvent(QPaintEvent *event) {
|
||||
QPainter painter(this);
|
||||
QPixmap *img_shown = nullptr;
|
||||
painter.fillRect(rect(), Qt::black);
|
||||
|
||||
if (is_hot) {
|
||||
if (img_hot.isNull()) {
|
||||
img_hot.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/hot.png");
|
||||
}
|
||||
img_shown = &img_hot;
|
||||
int x = (width() - img_hot.width()) / 2;
|
||||
int y = (height() - img_hot.height()) / 2;
|
||||
painter.drawPixmap(x, y, img_hot);
|
||||
} else {
|
||||
if (img_ready.isNull()) {
|
||||
img_ready.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready.png");
|
||||
// Boot logo — same rotation as spinner (bg.jpg is pre-rotated 90° CW for framebuffer)
|
||||
if (img_bg.isNull()) {
|
||||
QPixmap raw("/usr/comma/bg.jpg");
|
||||
if (!raw.isNull()) {
|
||||
QTransform rot;
|
||||
rot.rotate(-90);
|
||||
img_bg = raw.transformed(rot);
|
||||
}
|
||||
}
|
||||
if (!img_bg.isNull()) {
|
||||
QPixmap scaled = img_bg.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation);
|
||||
int x = (width() - scaled.width()) / 2;
|
||||
int y = (height() - scaled.height()) / 2;
|
||||
painter.drawPixmap(x, y, scaled);
|
||||
}
|
||||
img_shown = &img_ready;
|
||||
}
|
||||
|
||||
int x = (width() - img_shown->width()) / 2;
|
||||
int y = (height() - img_shown->height()) / 2;
|
||||
painter.drawPixmap(x, y, *img_shown);
|
||||
if (error_msg.isEmpty()) {
|
||||
// "READY!" 8-bit text sprite, 2x size, 15% below center
|
||||
static QPixmap ready_text("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png");
|
||||
if (!ready_text.isNull()) {
|
||||
QPixmap scaled = ready_text.scaled(ready_text.width() * 3 / 2, ready_text.height() * 3 / 2, Qt::KeepAspectRatio, Qt::FastTransformation);
|
||||
int tx = (width() - scaled.width()) / 2;
|
||||
int ty = height() / 2 + height() * 15 / 100;
|
||||
painter.drawPixmap(tx, ty, scaled);
|
||||
}
|
||||
} else {
|
||||
// Error state: red text at 25% below center
|
||||
QFont font("Inter", 50, QFont::Bold);
|
||||
painter.setFont(font);
|
||||
painter.setPen(QColor(0xFF, 0x44, 0x44));
|
||||
int ty = height() / 2 + height() * 25 / 100;
|
||||
QRect text_rect(0, ty, width(), 100);
|
||||
painter.drawText(text_rect, Qt::AlignHCenter | Qt::AlignTop, error_msg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReadyWindow::refresh() {
|
||||
bool changed = false;
|
||||
|
||||
// Temperature check
|
||||
std::string bytes = params.get("Offroad_TemperatureTooHigh");
|
||||
bool was_hot = is_hot;
|
||||
if (!bytes.empty()) {
|
||||
auto doc = QJsonDocument::fromJson(bytes.data());
|
||||
is_hot = true;
|
||||
cur_temp = doc["extra"].toString();
|
||||
update();
|
||||
} else if (is_hot) {
|
||||
} else {
|
||||
is_hot = false;
|
||||
update();
|
||||
}
|
||||
if (is_hot != was_hot) changed = true;
|
||||
|
||||
// Error state checks (only when not hot — hot has its own display)
|
||||
if (!is_hot) {
|
||||
QString prev_error = error_msg;
|
||||
|
||||
// Panda check — same logic as sidebar, with 10s grace period on startup
|
||||
if (uptime.elapsed() > 10000 &&
|
||||
uiState()->scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) {
|
||||
error_msg = "PANDA NOT CONNECTED";
|
||||
}
|
||||
// Car unrecognized check
|
||||
else if (!params.get("Offroad_CarUnrecognized").empty()) {
|
||||
error_msg = "CAR NOT RECOGNIZED";
|
||||
}
|
||||
else {
|
||||
error_msg = "";
|
||||
}
|
||||
|
||||
if (error_msg != prev_error) changed = true;
|
||||
}
|
||||
|
||||
if (changed) update();
|
||||
}
|
||||
@@ -8,8 +8,9 @@
|
||||
#include <QSocketNotifier>
|
||||
#include <QVariantAnimation>
|
||||
#include <QWidget>
|
||||
#include <QElapsedTimer>
|
||||
#include <QTimer>
|
||||
|
||||
|
||||
#include "common/util.h"
|
||||
#include "selfdrive/ui/ui.h"
|
||||
|
||||
@@ -26,6 +27,8 @@ private:
|
||||
QTimer* timer;
|
||||
bool is_hot = false;
|
||||
QString cur_temp;
|
||||
QPixmap img_ready;
|
||||
QString error_msg; // non-empty = show red error instead of READY!
|
||||
QElapsedTimer uptime;
|
||||
QPixmap img_bg;
|
||||
QPixmap img_hot;
|
||||
};
|
||||
Binary file not shown.
@@ -182,11 +182,12 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
|
||||
return ignore;
|
||||
}
|
||||
|
||||
// CLEARPILOT: Status window — live system stats, refreshed every second
|
||||
// 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);
|
||||
@@ -201,6 +202,65 @@ static QString shellCmd(const QString &cmd) {
|
||||
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);
|
||||
@@ -232,6 +292,7 @@ StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) {
|
||||
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");
|
||||
@@ -242,89 +303,66 @@ StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) {
|
||||
|
||||
setStyleSheet("StatusWindow { background-color: black; }");
|
||||
|
||||
// Refresh every second
|
||||
connect(&watcher, &QFutureWatcher<StatusData>::finished, this, &StatusWindow::applyResults);
|
||||
|
||||
QTimer *timer = new QTimer(this);
|
||||
connect(timer, &QTimer::timeout, this, &StatusWindow::refresh);
|
||||
connect(timer, &QTimer::timeout, this, &StatusWindow::kickRefresh);
|
||||
timer->start(1000);
|
||||
refresh();
|
||||
}
|
||||
|
||||
void StatusWindow::refresh() {
|
||||
// Time
|
||||
time_label->setText(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss"));
|
||||
void StatusWindow::kickRefresh() {
|
||||
if (!isVisible() || collecting) return;
|
||||
collecting = true;
|
||||
watcher.setFuture(QtConcurrent::run(collectStatus));
|
||||
}
|
||||
|
||||
// Storage
|
||||
QString df = shellCmd("df -h /data | tail -1 | awk '{print $3 \" / \" $2 \" (\" $5 \" used)\"}'");
|
||||
storage_label->setText(df);
|
||||
void StatusWindow::applyResults() {
|
||||
collecting = false;
|
||||
StatusData d = watcher.result();
|
||||
|
||||
// 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));
|
||||
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;");
|
||||
}
|
||||
|
||||
// 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]));
|
||||
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;");
|
||||
}
|
||||
|
||||
// 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;");
|
||||
}
|
||||
ip_label->setText(d.ip.isEmpty() ? "No connection" : d.ip);
|
||||
wifi_label->setText(d.wifi.isEmpty() ? "Not connected" : d.wifi);
|
||||
|
||||
// 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 + ")");
|
||||
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;");
|
||||
}
|
||||
|
||||
// GPS
|
||||
QString gps_raw = shellCmd("cat /data/params/d/LastGPSPosition 2>/dev/null");
|
||||
if (gps_raw.isEmpty()) {
|
||||
if (d.gps.isEmpty()) {
|
||||
gps_label->setText("No fix");
|
||||
gps_label->setStyleSheet("color: #ff4444; font-size: 38px;");
|
||||
} else {
|
||||
gps_label->setText(gps_raw);
|
||||
gps_label->setText(d.gps);
|
||||
gps_label->setStyleSheet("color: white; font-size: 38px;");
|
||||
}
|
||||
|
||||
// Telemetry
|
||||
QString telem = shellCmd("cat /data/params/d/TelemetryEnabled 2>/dev/null");
|
||||
if (telem == "1") {
|
||||
if (d.telemetry == "1") {
|
||||
telemetry_label->setText("Enabled");
|
||||
telemetry_label->setStyleSheet("color: #17c44d; font-size: 38px;");
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#pragma once
|
||||
|
||||
#include <QFutureWatcher>
|
||||
#include <QStackedLayout>
|
||||
#include <QLabel>
|
||||
#include <QSocketNotifier>
|
||||
@@ -16,6 +17,12 @@ class StatusWindow : public QFrame {
|
||||
public:
|
||||
explicit StatusWindow(QWidget *parent = 0);
|
||||
|
||||
struct StatusData {
|
||||
QString time, storage, ram, load, temp, fan, ip, wifi;
|
||||
QString vpn_status, vpn_ip, gps, telemetry;
|
||||
float temp_c = 0;
|
||||
};
|
||||
|
||||
protected:
|
||||
void mousePressEvent(QMouseEvent *e) override;
|
||||
|
||||
@@ -23,9 +30,14 @@ signals:
|
||||
void closeStatus();
|
||||
|
||||
private slots:
|
||||
void refresh();
|
||||
void kickRefresh();
|
||||
void applyResults();
|
||||
|
||||
private:
|
||||
|
||||
QFutureWatcher<StatusData> watcher;
|
||||
bool collecting = false;
|
||||
|
||||
QLabel *storage_label;
|
||||
QLabel *ram_label;
|
||||
QLabel *load_label;
|
||||
@@ -37,6 +49,7 @@ private:
|
||||
QLabel *gps_label;
|
||||
QLabel *time_label;
|
||||
QLabel *telemetry_label;
|
||||
QLabel *panda_label;
|
||||
};
|
||||
|
||||
class MainWindow : public QWidget {
|
||||
|
||||
Reference in New Issue
Block a user