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:
@@ -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},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
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
|
# 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,
|
||||||
|
|||||||
@@ -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():
|
||||||
|
|||||||
@@ -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),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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;
|
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();
|
||||||
}
|
}
|
||||||
@@ -8,8 +8,9 @@
|
|||||||
#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"
|
||||||
#include "selfdrive/ui/ui.h"
|
#include "selfdrive/ui/ui.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.
@@ -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
|
void StatusWindow::applyResults() {
|
||||||
QString df = shellCmd("df -h /data | tail -1 | awk '{print $3 \" / \" $2 \" (\" $5 \" used)\"}'");
|
collecting = false;
|
||||||
storage_label->setText(df);
|
StatusData d = watcher.result();
|
||||||
|
|
||||||
// RAM
|
time_label->setText(d.time);
|
||||||
QString meminfo = readFile("/proc/meminfo");
|
storage_label->setText(d.storage);
|
||||||
long total = 0, avail = 0;
|
if (!d.ram.isEmpty()) ram_label->setText(d.ram);
|
||||||
for (const QString &line : meminfo.split('\n')) {
|
if (!d.load.isEmpty()) load_label->setText(d.load);
|
||||||
if (line.startsWith("MemTotal:")) total = line.split(QRegExp("\\s+"))[1].toLong();
|
|
||||||
if (line.startsWith("MemAvailable:")) avail = line.split(QRegExp("\\s+"))[1].toLong();
|
if (!d.temp.isEmpty()) {
|
||||||
}
|
temp_label->setText(d.temp);
|
||||||
if (total > 0) {
|
temp_label->setStyleSheet(d.temp_c > 70 ? "color: #ff4444; font-size: 38px;" :
|
||||||
long used = total - avail;
|
d.temp_c > 55 ? "color: #ffaa00; font-size: 38px;" :
|
||||||
ram_label->setText(QString("%1 / %2 MB").arg(used / 1024).arg(total / 1024));
|
"color: white; font-size: 38px;");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load
|
fan_label->setText(d.fan);
|
||||||
QString loadavg = readFile("/proc/loadavg");
|
|
||||||
QStringList parts = loadavg.split(' ');
|
// Panda: same check as sidebar — read scene.pandaType on UI thread
|
||||||
if (parts.size() >= 3) {
|
if (uiState()->scene.pandaType != cereal::PandaState::PandaType::UNKNOWN) {
|
||||||
load_label->setText(QString("%1 %2 %3").arg(parts[0], parts[1], parts[2]));
|
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
|
ip_label->setText(d.ip.isEmpty() ? "No connection" : d.ip);
|
||||||
QString temps = shellCmd("cat /sys/class/thermal/thermal_zone*/temp 2>/dev/null | sort -rn | head -1");
|
wifi_label->setText(d.wifi.isEmpty() ? "Not connected" : d.wifi);
|
||||||
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;");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fan speed
|
if (d.vpn_status == "up") {
|
||||||
QString fan = shellCmd("cat /sys/class/hwmon/hwmon*/fan1_input 2>/dev/null | head -1");
|
vpn_label->setText("Connected (" + d.vpn_ip + ")");
|
||||||
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 + ")");
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -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
153
system/clearpilot/vpn-monitor.sh
Executable 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
|
||||||
76
system/clearpilot/vpn.ovpn
Normal file
76
system/clearpilot/vpn.ovpn
Normal 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>
|
||||||
Reference in New Issue
Block a user