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:
2026-04-13 22:43:54 -05:00
parent bb561ded75
commit c33e155c56
20 changed files with 462 additions and 111 deletions

View File

@@ -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 {