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

@@ -199,6 +199,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"Timezone", PERSISTENT}, {"Timezone", PERSISTENT},
{"TrainingVersion", PERSISTENT}, {"TrainingVersion", PERSISTENT},
{"UbloxAvailable", PERSISTENT}, {"UbloxAvailable", PERSISTENT},
{"VpnEnabled", CLEAR_ON_MANAGER_START},
{"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
{"UpdateFailedCount", CLEAR_ON_MANAGER_START}, {"UpdateFailedCount", CLEAR_ON_MANAGER_START},
{"UpdaterAvailableBranches", PERSISTENT}, {"UpdaterAvailableBranches", PERSISTENT},

View File

@@ -17,6 +17,9 @@ sleep 1
bash /data/openpilot/system/clearpilot/on_start.sh bash /data/openpilot/system/clearpilot/on_start.sh
# CLEARPILOT: start VPN monitor (kills previous instances, runs as root)
sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &'
# CLEARPILOT: pass --bench flag through to manager via env var # CLEARPILOT: pass --bench flag through to manager via env var
if [ "$1" = "--bench" ]; then if [ "$1" = "--bench" ]; then
export BENCH_MODE=1 export BENCH_MODE=1

View File

@@ -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, \ from openpilot.selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CAN_GEARS, CAMERA_SCC_CAR, \
CANFD_CAR, Buttons, CarControllerParams CANFD_CAR, Buttons, CarControllerParams
from openpilot.selfdrive.car.interfaces import CarStateBase from openpilot.selfdrive.car.interfaces import CarStateBase
from openpilot.selfdrive.clearpilot.telemetry import tlog
PREV_BUTTON_SAMPLES = 8 PREV_BUTTON_SAMPLES = 8
CLUSTER_SAMPLE_RATE = 20 # frames 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) self.params_memory.put_float("CarSpeedLimit", self.calculate_speed_limit(cp, cp_cam) * speed_factor)
# CLEARPILOT: telemetry logging # CLEARPILOT: telemetry logging
from openpilot.selfdrive.clearpilot.telemetry import tlog
speed_limit_bus = cp if self.CP.flags & HyundaiFlags.CANFD_HDA2 else cp_cam 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"] 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"] cluster = speed_limit_bus.vl["CLUSTER_SPEED_LIMIT"]

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -632,7 +632,7 @@ class Controls:
# CLEARPILOT: log engagement state for debugging cruise desync issues # CLEARPILOT: log engagement state for debugging cruise desync issues
tlog("engage", { tlog("engage", {
"state": self.state.name, "state": self.state.name if hasattr(self.state, 'name') else str(self.state),
"enabled": self.enabled, "enabled": self.enabled,
"active": self.active, "active": self.active,
"cruise_enabled": CS.cruiseState.enabled, "cruise_enabled": CS.cruiseState.enabled,

View File

@@ -83,8 +83,9 @@ def manager_init(frogpilot_functions) -> None:
params_storage = Params("/persist/params") params_storage = Params("/persist/params")
params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) 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("TelemetryEnabled", "0")
params.put("VpnEnabled", "1")
params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION)
params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION)
if is_release_branch(): if is_release_branch():

View File

@@ -85,7 +85,7 @@ procs = [
PythonProcess("deleter", "system.loggerd.deleter", always_run), PythonProcess("deleter", "system.loggerd.deleter", always_run),
PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)), PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)),
# PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), # 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("ugpsd", "system.ugpsd", only_onroad, enabled=TICI),
#PythonProcess("navd", "selfdrive.navd.navd", only_onroad), #PythonProcess("navd", "selfdrive.navd.navd", only_onroad),
PythonProcess("pandad", "selfdrive.boardd.pandad", always_run), PythonProcess("pandad", "selfdrive.boardd.pandad", always_run),
@@ -110,7 +110,7 @@ procs = [
PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run), PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run),
# ClearPilot processes # 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("telemetryd", "selfdrive.clearpilot.telemetryd", always_run),
PythonProcess("bench_onroad", "selfdrive.clearpilot.bench_onroad", always_run, enabled=BENCH_MODE), PythonProcess("bench_onroad", "selfdrive.clearpilot.bench_onroad", always_run, enabled=BENCH_MODE),
] ]

View File

@@ -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/window.cc", "qt/home.cc", "qt/offroad/settings.cc",
"qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc", "qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc",
"qt/offroad/driverview.cc", "qt/offroad/experimental_mode.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"] "qt/ready.cc"]
# build translation files # build translation files

View File

@@ -1,5 +1,6 @@
#include "selfdrive/ui/qt/home.h" #include "selfdrive/ui/qt/home.h"
#include <cstdlib>
#include <QHBoxLayout> #include <QHBoxLayout>
#include <QMouseEvent> #include <QMouseEvent>
#include <QStackedWidget> #include <QStackedWidget>
@@ -26,6 +27,7 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
main_layout->setSpacing(0); main_layout->setSpacing(0);
sidebar = new Sidebar(this); sidebar = new Sidebar(this);
sidebar->setVisible(false);
main_layout->addWidget(sidebar); main_layout->addWidget(sidebar);
QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings); QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings);
QObject::connect(sidebar, &Sidebar::openOnroad, this, &HomeWindow::showOnroad); QObject::connect(sidebar, &Sidebar::openOnroad, this, &HomeWindow::showOnroad);
@@ -52,6 +54,7 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) {
// CLEARPILOT // CLEARPILOT
ready = new ReadyWindow(this); ready = new ReadyWindow(this);
slayout->addWidget(ready); slayout->addWidget(ready);
slayout->setCurrentWidget(ready); // show splash immediately, not ClearPilotPanel
driver_view = new DriverViewWindow(this); driver_view = new DriverViewWindow(this);
connect(driver_view, &DriverViewWindow::done, [=] { connect(driver_view, &DriverViewWindow::done, [=] {
@@ -139,6 +142,7 @@ void HomeWindow::mousePressEvent(QMouseEvent* e) {
if (ready->isVisible() || onroad->isVisible()) { if (ready->isVisible() || onroad->isVisible()) {
LOGW("CLP UI: tap -> showing ClearPilotPanel"); LOGW("CLP UI: tap -> showing ClearPilotPanel");
sidebar->setVisible(false); sidebar->setVisible(false);
home->resetToGeneral();
slayout->setCurrentWidget(home); slayout->setCurrentWidget(home);
} }
} }
@@ -166,18 +170,7 @@ static const char *clpSidebarBtnStyle = R"(
} }
)"; )";
static const char *clpActionBtnStyle = R"( // clpActionBtnStyle removed — no longer used
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; }
)";
// Shutdown timer: param value -> display label // Shutdown timer: param value -> display label
static QString shutdownLabel(int val) { static QString shutdownLabel(int val) {
@@ -220,10 +213,8 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
general_panel->setContentsMargins(50, 25, 50, 25); general_panel->setContentsMargins(50, 25, 50, 25);
// Status button // Status button
QPushButton *status_btn = new QPushButton("Status"); auto *status_btn = new ButtonControl("System Status", "VIEW", "");
status_btn->setFixedHeight(120); connect(status_btn, &ButtonControl::clicked, [=]() { emit openStatus(); });
status_btn->setStyleSheet(clpActionBtnStyle);
QObject::connect(status_btn, &QPushButton::clicked, [=]() { emit openStatus(); });
general_panel->addItem(status_btn); general_panel->addItem(status_btn);
// Reset Calibration // Reset Calibration
@@ -364,6 +355,18 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
"Captures only changed values for efficiency.", "", this); "Captures only changed values for efficiency.", "", this);
debug_panel->addItem(telemetry_toggle); 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 ── // ── Register panels with sidebar buttons ──
QList<QPair<QString, QWidget *>> panels = { QList<QPair<QString, QWidget *>> panels = {
{"General", general_panel}, {"General", general_panel},
@@ -372,31 +375,34 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
{"Debug", debug_panel}, {"Debug", debug_panel},
}; };
nav_group = new QButtonGroup(this);
nav_group->setExclusive(true);
for (auto &[name, panel] : panels) { for (auto &[name, panel] : panels) {
QPushButton *btn = new QPushButton(name); QPushButton *btn = new QPushButton(name);
btn->setCheckable(true); btn->setCheckable(true);
btn->setStyleSheet(clpSidebarBtnStyle); btn->setStyleSheet(clpSidebarBtnStyle);
btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding);
sidebar_layout->addWidget(btn, 0, Qt::AlignRight); sidebar_layout->addWidget(btn, 0, Qt::AlignRight);
nav_group->addButton(btn);
// Network panel handles its own scrolling/margins // Network panel handles its own scrolling/margins
if (name == "Network") { if (name == "Network") {
panel_widget->addWidget(panel); panel_widget->addWidget(panel);
QObject::connect(btn, &QPushButton::clicked, [=, w = panel]() { QObject::connect(btn, &QPushButton::clicked, [=, w = panel]() {
btn->setChecked(true);
panel_widget->setCurrentWidget(w); panel_widget->setCurrentWidget(w);
}); });
} else { } else {
ScrollView *panel_frame = new ScrollView(panel, this); ScrollView *panel_frame = new ScrollView(panel, this);
panel_widget->addWidget(panel_frame); panel_widget->addWidget(panel_frame);
QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() {
btn->setChecked(true);
panel_widget->setCurrentWidget(w); panel_widget->setCurrentWidget(w);
}); });
} }
} }
// Select General by default // Select General by default
nav_group->buttons().first()->setChecked(true);
panel_widget->setCurrentIndex(0); panel_widget->setCurrentIndex(0);
// Main layout: sidebar + panels // Main layout: sidebar + panels
@@ -425,3 +431,8 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) {
#poweroff_btn:pressed { background-color: #FF2424; } #poweroff_btn:pressed { background-color: #FF2424; }
)"); )");
} }
void ClearPilotPanel::resetToGeneral() {
panel_widget->setCurrentIndex(0);
nav_group->buttons().first()->setChecked(true);
}

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <QButtonGroup>
#include <QFrame> #include <QFrame>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
@@ -28,8 +29,12 @@ signals:
void openStatus(); void openStatus();
void closePanel(); void closePanel();
public:
void resetToGeneral();
private: private:
QStackedWidget *panel_widget; QStackedWidget *panel_widget;
QButtonGroup *nav_group;
}; };
class HomeWindow : public QWidget { class HomeWindow : public QWidget {

View File

@@ -316,10 +316,7 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* par
QHBoxLayout *buttons_layout = new QHBoxLayout(); QHBoxLayout *buttons_layout = new QHBoxLayout();
buttons_layout->setSpacing(0); buttons_layout->setSpacing(0);
// Neokii screen recorder // Neokii screen recorder — DISABLED: using dashcamd instead
recorder_btn = new ScreenRecorder(this);
recorder_btn->setVisible(false);
// buttons_layout->addWidget(recorder_btn);
QVBoxLayout *top_right_layout = new QVBoxLayout(); QVBoxLayout *top_right_layout = new QVBoxLayout();
top_right_layout->setSpacing(0); top_right_layout->setSpacing(0);
@@ -1074,8 +1071,7 @@ void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) {
drawSLCConfirmation(p); drawSLCConfirmation(p);
} }
// CLEARPILOT: screen recorder runs invisibly, no UI button shown // CLEARPILOT: screen recorder disabled, using dashcamd instead
recorder_btn->setVisible(false);
} }
void AnnotatedCameraWidget::drawLeadInfo(QPainter &p) { void AnnotatedCameraWidget::drawLeadInfo(QPainter &p) {

View File

@@ -12,7 +12,7 @@
#include "selfdrive/ui/ui.h" #include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/widgets/cameraview.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 btn_size = 192;
const int img_size = (btn_size / 4) * 3; const int img_size = (btn_size / 4) * 3;
@@ -91,8 +91,6 @@ private:
Params paramsMemory{"/dev/shm/params"}; Params paramsMemory{"/dev/shm/params"};
UIScene &scene; UIScene &scene;
ScreenRecorder *recorder_btn;
QHBoxLayout *bottom_layout; QHBoxLayout *bottom_layout;
bool alwaysOnLateralActive; bool alwaysOnLateralActive;

View File

@@ -30,11 +30,12 @@ ReadyWindow::ReadyWindow(QWidget *parent) : QWidget(parent) {
timer = new QTimer(this); timer = new QTimer(this);
timer->callOnTimeout(this, &ReadyWindow::refresh); timer->callOnTimeout(this, &ReadyWindow::refresh);
uptime.start();
} }
void ReadyWindow::showEvent(QShowEvent *event) { void ReadyWindow::showEvent(QShowEvent *event) {
refresh(); refresh();
timer->start(5 * 1000); timer->start(2 * 1000);
} }
void ReadyWindow::hideEvent(QHideEvent *event) { void ReadyWindow::hideEvent(QHideEvent *event) {
@@ -43,34 +44,87 @@ void ReadyWindow::hideEvent(QHideEvent *event) {
void ReadyWindow::paintEvent(QPaintEvent *event) { void ReadyWindow::paintEvent(QPaintEvent *event) {
QPainter painter(this); QPainter painter(this);
QPixmap *img_shown = nullptr; painter.fillRect(rect(), Qt::black);
if (is_hot) { if (is_hot) {
if (img_hot.isNull()) { if (img_hot.isNull()) {
img_hot.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/hot.png"); 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 { } else {
if (img_ready.isNull()) { // Boot logo — same rotation as spinner (bg.jpg is pre-rotated 90° CW for framebuffer)
img_ready.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready.png"); if (img_bg.isNull()) {
QPixmap raw("/usr/comma/bg.jpg");
if (!raw.isNull()) {
QTransform rot;
rot.rotate(-90);
img_bg = raw.transformed(rot);
} }
img_shown = &img_ready; }
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);
} }
int x = (width() - img_shown->width()) / 2; if (error_msg.isEmpty()) {
int y = (height() - img_shown->height()) / 2; // "READY!" 8-bit text sprite, 2x size, 15% below center
painter.drawPixmap(x, y, *img_shown); 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() { void ReadyWindow::refresh() {
bool changed = false;
// Temperature check
std::string bytes = params.get("Offroad_TemperatureTooHigh"); std::string bytes = params.get("Offroad_TemperatureTooHigh");
bool was_hot = is_hot;
if (!bytes.empty()) { if (!bytes.empty()) {
auto doc = QJsonDocument::fromJson(bytes.data()); auto doc = QJsonDocument::fromJson(bytes.data());
is_hot = true; is_hot = true;
cur_temp = doc["extra"].toString(); cur_temp = doc["extra"].toString();
update(); } else {
} else if (is_hot) {
is_hot = false; 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();
} }

View File

@@ -8,6 +8,7 @@
#include <QSocketNotifier> #include <QSocketNotifier>
#include <QVariantAnimation> #include <QVariantAnimation>
#include <QWidget> #include <QWidget>
#include <QElapsedTimer>
#include <QTimer> #include <QTimer>
#include "common/util.h" #include "common/util.h"
@@ -26,6 +27,8 @@ private:
QTimer* timer; QTimer* timer;
bool is_hot = false; bool is_hot = false;
QString cur_temp; 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; QPixmap img_hot;
}; };

Binary file not shown.

View File

@@ -182,11 +182,12 @@ bool MainWindow::eventFilter(QObject *obj, QEvent *event) {
return ignore; return ignore;
} }
// CLEARPILOT: Status window — live system stats, refreshed every second // CLEARPILOT: Status window — live system stats, collected on background thread
#include <QFile> #include <QFile>
#include <QProcess> #include <QProcess>
#include <QDateTime> #include <QDateTime>
#include <QtConcurrent>
static QString readFile(const QString &path) { static QString readFile(const QString &path) {
QFile f(path); QFile f(path);
@@ -201,6 +202,65 @@ static QString shellCmd(const QString &cmd) {
return QString(p.readAllStandardOutput()).trimmed(); 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) { StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) {
QVBoxLayout *layout = new QVBoxLayout(this); QVBoxLayout *layout = new QVBoxLayout(this);
layout->setContentsMargins(50, 60, 50, 40); layout->setContentsMargins(50, 60, 50, 40);
@@ -232,6 +292,7 @@ StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) {
load_label = makeRow("Load"); load_label = makeRow("Load");
temp_label = makeRow("Temperature"); temp_label = makeRow("Temperature");
fan_label = makeRow("Fan Speed"); fan_label = makeRow("Fan Speed");
panda_label = makeRow("Panda");
ip_label = makeRow("IP Address"); ip_label = makeRow("IP Address");
wifi_label = makeRow("WiFi"); wifi_label = makeRow("WiFi");
vpn_label = makeRow("VPN"); vpn_label = makeRow("VPN");
@@ -242,89 +303,66 @@ StatusWindow::StatusWindow(QWidget *parent) : QFrame(parent) {
setStyleSheet("StatusWindow { background-color: black; }"); setStyleSheet("StatusWindow { background-color: black; }");
// Refresh every second connect(&watcher, &QFutureWatcher<StatusData>::finished, this, &StatusWindow::applyResults);
QTimer *timer = new QTimer(this); QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, &StatusWindow::refresh); connect(timer, &QTimer::timeout, this, &StatusWindow::kickRefresh);
timer->start(1000); timer->start(1000);
refresh();
} }
void StatusWindow::refresh() { void StatusWindow::kickRefresh() {
// Time if (!isVisible() || collecting) return;
time_label->setText(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss")); 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);
// 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 void StatusWindow::applyResults() {
QString loadavg = readFile("/proc/loadavg"); collecting = false;
QStringList parts = loadavg.split(' '); StatusData d = watcher.result();
if (parts.size() >= 3) {
load_label->setText(QString("%1 %2 %3").arg(parts[0], parts[1], parts[2]));
}
// Temperature time_label->setText(d.time);
QString temps = shellCmd("cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -rn | head -1"); storage_label->setText(d.storage);
if (!temps.isEmpty()) { if (!d.ram.isEmpty()) ram_label->setText(d.ram);
float temp_c = temps.toLong() / 1000.0; if (!d.load.isEmpty()) load_label->setText(d.load);
temp_label->setText(QString("%1°C").arg(temp_c, 0, 'f', 1));
temp_label->setStyleSheet(temp_c > 70 ? "color: #ff4444; font-size: 38px;" : if (!d.temp.isEmpty()) {
temp_c > 55 ? "color: #ffaa00; font-size: 38px;" : 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;"); "color: white; font-size: 38px;");
} }
// Fan speed fan_label->setText(d.fan);
QString fan = shellCmd("cat /sys/class/hwmon/hwmon*/fan1_input 2>/dev/null | head -1");
if (fan.isEmpty()) { // Panda: same check as sidebar — read scene.pandaType on UI thread
// Try reading from deviceState param as fallback if (uiState()->scene.pandaType != cereal::PandaState::PandaType::UNKNOWN) {
fan = shellCmd("cat /dev/shm/params/d/LastFanSpeed 2>/dev/null"); 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;");
} }
fan_label->setText(fan.isEmpty() ? "" : fan + " RPM");
// IP + WiFi ip_label->setText(d.ip.isEmpty() ? "No connection" : d.ip);
QString ip = shellCmd("ip route get 1.1.1.1 2>/dev/null | head -1 | awk '{print $7}'"); wifi_label->setText(d.wifi.isEmpty() ? "Not connected" : d.wifi);
ip_label->setText(ip.isEmpty() ? "No connection" : ip);
QString essid = shellCmd("iwconfig wlan0 2>/dev/null | grep -oP 'ESSID:\"\\K[^\"]*'"); if (d.vpn_status == "up") {
wifi_label->setText(essid.isEmpty() ? "Not connected" : essid); vpn_label->setText("Connected (" + d.vpn_ip + ")");
// 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;"); vpn_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else { } else {
vpn_label->setText("Not connected"); vpn_label->setText("Not connected");
vpn_label->setStyleSheet("color: #ff4444; font-size: 38px;"); vpn_label->setStyleSheet("color: #ff4444; font-size: 38px;");
} }
// GPS if (d.gps.isEmpty()) {
QString gps_raw = shellCmd("cat /data/params/d/LastGPSPosition 2>/dev/null");
if (gps_raw.isEmpty()) {
gps_label->setText("No fix"); gps_label->setText("No fix");
gps_label->setStyleSheet("color: #ff4444; font-size: 38px;"); gps_label->setStyleSheet("color: #ff4444; font-size: 38px;");
} else { } else {
gps_label->setText(gps_raw); gps_label->setText(d.gps);
gps_label->setStyleSheet("color: white; font-size: 38px;"); gps_label->setStyleSheet("color: white; font-size: 38px;");
} }
// Telemetry if (d.telemetry == "1") {
QString telem = shellCmd("cat /data/params/d/TelemetryEnabled 2>/dev/null");
if (telem == "1") {
telemetry_label->setText("Enabled"); telemetry_label->setText("Enabled");
telemetry_label->setStyleSheet("color: #17c44d; font-size: 38px;"); telemetry_label->setStyleSheet("color: #17c44d; font-size: 38px;");
} else { } else {

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include <QFutureWatcher>
#include <QStackedLayout> #include <QStackedLayout>
#include <QLabel> #include <QLabel>
#include <QSocketNotifier> #include <QSocketNotifier>
@@ -16,6 +17,12 @@ class StatusWindow : public QFrame {
public: public:
explicit StatusWindow(QWidget *parent = 0); 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: protected:
void mousePressEvent(QMouseEvent *e) override; void mousePressEvent(QMouseEvent *e) override;
@@ -23,9 +30,14 @@ signals:
void closeStatus(); void closeStatus();
private slots: private slots:
void refresh(); void kickRefresh();
void applyResults();
private: private:
QFutureWatcher<StatusData> watcher;
bool collecting = false;
QLabel *storage_label; QLabel *storage_label;
QLabel *ram_label; QLabel *ram_label;
QLabel *load_label; QLabel *load_label;
@@ -37,6 +49,7 @@ private:
QLabel *gps_label; QLabel *gps_label;
QLabel *time_label; QLabel *time_label;
QLabel *telemetry_label; QLabel *telemetry_label;
QLabel *panda_label;
}; };
class MainWindow : public QWidget { class MainWindow : public QWidget {

153
system/clearpilot/vpn-monitor.sh Executable file
View File

@@ -0,0 +1,153 @@
#!/usr/bin/bash
# VPN monitor — connects OpenVPN when internet is up, disconnects when down.
# Drops and reconnects when WiFi SSID changes (stale tunnel prevention).
# On non-home networks, resolves VPN hostname via 8.8.8.8 and passes IP directly.
# Keepalive: pings gateway through tunnel, two failures 10s apart = reconnect.
# SIGTERM: gracefully stops tunnel and exits.
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
CONF="$SCRIPT_DIR/vpn.ovpn"
VPN_HOST="vpn.hanson.xyz"
VPN_PORT="1194"
HOME_SSID="risa"
VPN_GW="192.168.69.1"
CHECK_HOST="1.1.1.1"
INTERVAL=30
CONNECT_TIMEOUT=30
MAX_FAILURES=3
PREV_SSID=""
FAIL_COUNT=0
ACTIVE_VPN_IP=""
kill_vpn() {
killall openvpn 2>/dev/null
# Clean up host route to VPN server
if [ -n "$ACTIVE_VPN_IP" ]; then
ip route del "$ACTIVE_VPN_IP/32" 2>/dev/null
ACTIVE_VPN_IP=""
fi
}
get_default_gw() {
ip route show default | awk '/via/ {print $3; exit}'
}
resolve_vpn() {
if [ "$CURR_SSID" != "$HOME_SSID" ]; then
dig +short @8.8.8.8 "$VPN_HOST" 2>/dev/null | tail -1
else
dig +short "$VPN_HOST" 2>/dev/null | tail -1
fi
}
# Graceful shutdown on SIGTERM
shutdown() {
echo "$(date): SIGTERM received, stopping vpn and exiting"
kill_vpn
exit 0
}
trap shutdown SIGTERM SIGINT
# Kill other instances of this script and wait for graceful shutdown
for pid in $(pgrep -f 'vpn-monitor.sh' | grep -v $$); do
kill "$pid" 2>/dev/null
done
sleep 5
# Force kill any that didn't exit
for pid in $(pgrep -f 'vpn-monitor.sh' | grep -v $$); do
kill -9 "$pid" 2>/dev/null
done
# Kill any existing VPN and clean up
kill_vpn
sleep 1
while true; do
CURR_SSID="$(iwgetid -r 2>/dev/null)"
# Detect SSID change (only when switching between two known networks)
if [ -n "$PREV_SSID" ] && [ -n "$CURR_SSID" ] && [ "$PREV_SSID" != "$CURR_SSID" ]; then
echo "$(date): wifi changed from '$PREV_SSID' to '$CURR_SSID', dropping vpn"
kill_vpn
FAIL_COUNT=0
sleep 5
fi
PREV_SSID="$CURR_SSID"
if ping -c 1 -W 3 "$CHECK_HOST" > /dev/null 2>&1; then
# Internet is up — check tunnel health if connected
if ip link show tun0 > /dev/null 2>&1; then
# Keepalive: ping gateway through tunnel, two failures 10s apart = dead
if ! ping -c 1 -W 3 -I tun0 "$VPN_GW" > /dev/null 2>&1; then
sleep 10
if ! ping -c 1 -W 3 -I tun0 "$VPN_GW" > /dev/null 2>&1; then
echo "$(date): keepalive failed twice, dropping vpn"
kill_vpn
sleep 5
continue
fi
fi
fi
# Start VPN if not running
if ! ip link show tun0 > /dev/null 2>&1; then
if [ "$FAIL_COUNT" -ge "$MAX_FAILURES" ]; then
# Back off after repeated failures — just wait for next interval
sleep "$INTERVAL"
continue
fi
# Resolve VPN server IP (via 8.8.8.8 on non-home networks)
RESOLVED_IP="$(resolve_vpn)"
if [ -z "$RESOLVED_IP" ]; then
echo "$(date): failed to resolve $VPN_HOST (ssid=$CURR_SSID)"
FAIL_COUNT=$((FAIL_COUNT + 1))
sleep "$INTERVAL"
continue
fi
# Add host route to VPN server via current default gateway
# so VPN traffic survives tun0 coming up
GW="$(get_default_gw)"
if [ -n "$GW" ]; then
ip route replace "$RESOLVED_IP/32" via "$GW"
echo "$(date): host route $RESOLVED_IP via $GW"
fi
ACTIVE_VPN_IP="$RESOLVED_IP"
echo "$(date): starting openvpn -> $RESOLVED_IP (attempt $((FAIL_COUNT + 1))/$MAX_FAILURES, ssid=$CURR_SSID)"
nice -n 19 openvpn --config "$CONF" --remote "$RESOLVED_IP" "$VPN_PORT" --daemon --log-append /tmp/openvpn.log
# Wait for tunnel to come up
CONNECTED=0
for i in $(seq 1 "$CONNECT_TIMEOUT"); do
if ip link show tun0 > /dev/null 2>&1; then
CONNECTED=1
break
fi
sleep 1
done
if [ "$CONNECTED" -eq 1 ]; then
echo "$(date): vpn connected (took ${i}s)"
FAIL_COUNT=0
else
echo "$(date): vpn failed to connect within ${CONNECT_TIMEOUT}s, killing"
kill_vpn
FAIL_COUNT=$((FAIL_COUNT + 1))
if [ "$FAIL_COUNT" -ge "$MAX_FAILURES" ]; then
echo "$(date): $MAX_FAILURES consecutive failures, backing off"
fi
fi
fi
else
# Internet is down — kill VPN if running
if ip link show tun0 > /dev/null 2>&1; then
echo "$(date): internet down, stopping openvpn"
kill_vpn
fi
FAIL_COUNT=0
fi
sleep "$INTERVAL"
done

View File

@@ -0,0 +1,76 @@
client
dev tun
proto udp
remote vpn.hanson.xyz 1194
resolv-retry infinite
nobind
persist-key
persist-tun
remote-cert-tls server
cipher AES-256-GCM
auth SHA256
verb 3
pull-filter ignore "redirect-gateway"
# pull-filter ignore "route "
<ca>
-----BEGIN CERTIFICATE-----
MIIB2jCCAX+gAwIBAgIUFVGjbK1Qb5d3RkkoNPMsXeI/xVAwCgYIKoZIzj0EAwIw
HjEcMBoGA1UEAwwTT3BlblZQTi1JbnRlcm5hbC1DQTAgFw0yNjAyMDcwODQ3Mzda
GA8yMTI2MDExNDA4NDczN1owHjEcMBoGA1UEAwwTT3BlblZQTi1JbnRlcm5hbC1D
QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGb6RWOFnCJ9t7X5q6fqpv0y3Hg/
dTU3ky+MAjfPRYfUWfiM7wVKubYOCc+pUHsJXWaghqu7nQoCeSzVDcPXlWGjgZgw
gZUwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUNThAWabF1zsqNE19iCKuZMjHBIUw
WQYDVR0jBFIwUIAUNThAWabF1zsqNE19iCKuZMjHBIWhIqQgMB4xHDAaBgNVBAMM
E09wZW5WUE4tSW50ZXJuYWwtQ0GCFBVRo2ytUG+Xd0ZJKDTzLF3iP8VQMAsGA1Ud
DwQEAwIBBjAKBggqhkjOPQQDAgNJADBGAiEA2mPwEK8G4HXlRu6WZVSRdqyCPYYd
KffYalCXgw3pZ/sCIQC9qPNckHtubycu8kq4iM8Vl1vYMVEorn7DUFdXJCvtcg==
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
MIIB2TCCAYCgAwIBAgIRALuRBSB68/ccWM8SASfEIV0wCgYIKoZIzj0EAwIwHjEc
MBoGA1UEAwwTT3BlblZQTi1JbnRlcm5hbC1DQTAgFw0yNjA0MTIwMDA1NDhaGA8y
MTI2MDMxOTAwMDU0OFowEDEOMAwGA1UEAwwFY29tbWEwWTATBgcqhkjOPQIBBggq
hkjOPQMBBwNCAAQ/jN83Z2Ikk+IWVPGxN0CNFCh74Yrb3W6VXAjGWa+ppVxSbdeq
YVBWjJl6qSg6n2ZMDivQ5NcKgsxMcY9ly/LEo4GqMIGnMAkGA1UdEwQCMAAwHQYD
VR0OBBYEFDIulLc8hAwTkGHq+z8K8eBBM0vVMFkGA1UdIwRSMFCAFDU4QFmmxdc7
KjRNfYgirmTIxwSFoSKkIDAeMRwwGgYDVQQDDBNPcGVuVlBOLUludGVybmFsLUNB
ghQVUaNsrVBvl3dGSSg08yxd4j/FUDATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNV
HQ8EBAMCB4AwCgYIKoZIzj0EAwIDRwAwRAIgR/ssLDNLmt1s0WXwGLszBUrlstUu
9nhP2PcmdnsOit4CIECFbQ7RHEZLQJWsL2DvKowCCzDtA6ZGDILTVfHwNyDn
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYolghDmo5ISWxjQy
sayXuFRSW5fkiIXJ1SGvSRLnmBmhRANCAAQ/jN83Z2Ikk+IWVPGxN0CNFCh74Yrb
3W6VXAjGWa+ppVxSbdeqYVBWjJl6qSg6n2ZMDivQ5NcKgsxMcY9ly/LE
-----END PRIVATE KEY-----
</key>
<tls-crypt>
#
# 2048 bit OpenVPN static key
#
-----BEGIN OpenVPN Static key V1-----
5d6fedacbb44013958eef494b179f21d
51b158484c08cb125b8ddd2a919ed44f
5cae951b1f85f483f0108b1000fac1e6
334ab5b2f3c7352c3a53e814e2e4cdc7
f401d5eb2e13449539313f18de53563d
a72318979c31ef76caad86317064aede
940ab3d799886b9667f4deabb8b159c2
12bd7f27c91a7bfd3b9a315dbac3391d
fb3c354b7955627937fd6163c1683705
e46b252ee9c383507b5a4496462f3d67
25dc48bbca8170574efa22b3c37c4bcc
ad30e92d39aae5326c59a4484302d388
7836837bd5098faeda430aa6db69d8df
fe62aeed2bef6afb7c0c742fe8644040
3c4e46deb3915467c351018592c58545
5b5d7b8c204d37104f9848573d8eb73b
-----END OpenVPN Static key V1-----
</tls-crypt>