diff --git a/common/params.cc b/common/params.cc index edc3597..e24b719 100755 --- a/common/params.cc +++ b/common/params.cc @@ -199,6 +199,7 @@ std::unordered_map keys = { {"Timezone", PERSISTENT}, {"TrainingVersion", PERSISTENT}, {"UbloxAvailable", PERSISTENT}, + {"VpnEnabled", CLEAR_ON_MANAGER_START}, {"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, {"UpdateFailedCount", CLEAR_ON_MANAGER_START}, {"UpdaterAvailableBranches", PERSISTENT}, diff --git a/launch_openpilot.sh b/launch_openpilot.sh index 9634937..8644263 100755 --- a/launch_openpilot.sh +++ b/launch_openpilot.sh @@ -17,6 +17,9 @@ sleep 1 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 if [ "$1" = "--bench" ]; then export BENCH_MODE=1 diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index e6130cb..f15adc3 100755 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -13,6 +13,7 @@ from openpilot.selfdrive.car.hyundai.hyundaicanfd import CanBus from openpilot.selfdrive.car.hyundai.values import HyundaiFlags, CAR, DBC, CAN_GEARS, CAMERA_SCC_CAR, \ CANFD_CAR, Buttons, CarControllerParams from openpilot.selfdrive.car.interfaces import CarStateBase +from openpilot.selfdrive.clearpilot.telemetry import tlog PREV_BUTTON_SAMPLES = 8 CLUSTER_SAMPLE_RATE = 20 # frames @@ -421,8 +422,6 @@ class CarState(CarStateBase): self.params_memory.put_float("CarSpeedLimit", self.calculate_speed_limit(cp, cp_cam) * speed_factor) # CLEARPILOT: telemetry logging - from openpilot.selfdrive.clearpilot.telemetry import tlog - speed_limit_bus = cp if self.CP.flags & HyundaiFlags.CANFD_HDA2 else cp_cam scc = cp_cam.vl["SCC_CONTROL"] if self.CP.flags & HyundaiFlags.CANFD_CAMERA_SCC else cp.vl["SCC_CONTROL"] cluster = speed_limit_bus.vl["CLUSTER_SPEED_LIMIT"] diff --git a/selfdrive/clearpilot/dashcamd b/selfdrive/clearpilot/dashcamd index 59ffc45..15aa9d6 100755 Binary files a/selfdrive/clearpilot/dashcamd and b/selfdrive/clearpilot/dashcamd differ diff --git a/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png b/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png new file mode 100644 index 0000000..462cc86 Binary files /dev/null and b/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png differ diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index b0809e9..b4e0662 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -632,7 +632,7 @@ class Controls: # CLEARPILOT: log engagement state for debugging cruise desync issues tlog("engage", { - "state": self.state.name, + "state": self.state.name if hasattr(self.state, 'name') else str(self.state), "enabled": self.enabled, "active": self.active, "cruise_enabled": CS.cruiseState.enabled, diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index 4764617..6be9c8a 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -83,8 +83,9 @@ def manager_init(frogpilot_functions) -> None: params_storage = Params("/persist/params") params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) - # CLEARPILOT: always start with telemetry disabled + # CLEARPILOT: always start with telemetry disabled, VPN enabled params.put("TelemetryEnabled", "0") + params.put("VpnEnabled", "1") params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) if is_release_branch(): diff --git a/selfdrive/manager/process_config.py b/selfdrive/manager/process_config.py index 128cf21..4971845 100755 --- a/selfdrive/manager/process_config.py +++ b/selfdrive/manager/process_config.py @@ -85,7 +85,7 @@ procs = [ PythonProcess("deleter", "system.loggerd.deleter", always_run), PythonProcess("dmonitoringd", "selfdrive.monitoring.dmonitoringd", driverview, enabled=(not PC or WEBCAM)), # PythonProcess("qcomgpsd", "system.qcomgpsd.qcomgpsd", qcomgps, enabled=TICI), - PythonProcess("gpsd", "system.clearpilot.gpsd", qcomgps, enabled=TICI), + # PythonProcess("gpsd", "system.clearpilot.gpsd", qcomgps, enabled=TICI), # DISABLED: testing perf # PythonProcess("ugpsd", "system.ugpsd", only_onroad, enabled=TICI), #PythonProcess("navd", "selfdrive.navd.navd", only_onroad), PythonProcess("pandad", "selfdrive.boardd.pandad", always_run), @@ -110,7 +110,7 @@ procs = [ PythonProcess("frogpilot_process", "selfdrive.frogpilot.frogpilot_process", always_run), # ClearPilot processes - NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], dashcam_should_run), + # NativeProcess("dashcamd", "selfdrive/clearpilot", ["./dashcamd"], dashcam_should_run), # DISABLED: testing perf PythonProcess("telemetryd", "selfdrive.clearpilot.telemetryd", always_run), PythonProcess("bench_onroad", "selfdrive.clearpilot.bench_onroad", always_run, enabled=BENCH_MODE), ] diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index 49c6dda..1423fe0 100755 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -39,7 +39,7 @@ qt_src = ["main.cc", "qt/sidebar.cc", "qt/onroad.cc", "qt/body.cc", "qt/window.cc", "qt/home.cc", "qt/offroad/settings.cc", "qt/offroad/software_settings.cc", "qt/offroad/onboarding.cc", "qt/offroad/driverview.cc", "qt/offroad/experimental_mode.cc", - "../frogpilot/screenrecorder/omx_encoder.cc", "../frogpilot/screenrecorder/screenrecorder.cc", + "../frogpilot/screenrecorder/omx_encoder.cc", # kept for dashcamd .o dependency "qt/ready.cc"] # build translation files diff --git a/selfdrive/ui/qt/home.cc b/selfdrive/ui/qt/home.cc index ad31b34..cf8e4eb 100755 --- a/selfdrive/ui/qt/home.cc +++ b/selfdrive/ui/qt/home.cc @@ -1,5 +1,6 @@ #include "selfdrive/ui/qt/home.h" +#include #include #include #include @@ -26,6 +27,7 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { main_layout->setSpacing(0); sidebar = new Sidebar(this); + sidebar->setVisible(false); main_layout->addWidget(sidebar); QObject::connect(sidebar, &Sidebar::openSettings, this, &HomeWindow::openSettings); QObject::connect(sidebar, &Sidebar::openOnroad, this, &HomeWindow::showOnroad); @@ -52,6 +54,7 @@ HomeWindow::HomeWindow(QWidget* parent) : QWidget(parent) { // CLEARPILOT ready = new ReadyWindow(this); slayout->addWidget(ready); + slayout->setCurrentWidget(ready); // show splash immediately, not ClearPilotPanel driver_view = new DriverViewWindow(this); connect(driver_view, &DriverViewWindow::done, [=] { @@ -139,6 +142,7 @@ void HomeWindow::mousePressEvent(QMouseEvent* e) { if (ready->isVisible() || onroad->isVisible()) { LOGW("CLP UI: tap -> showing ClearPilotPanel"); sidebar->setVisible(false); + home->resetToGeneral(); slayout->setCurrentWidget(home); } } @@ -166,18 +170,7 @@ static const char *clpSidebarBtnStyle = R"( } )"; -static const char *clpActionBtnStyle = R"( - QPushButton { - background-color: #393939; - color: white; - border-radius: 15px; - font-size: 50px; - font-weight: 500; - text-align: left; - padding-left: 30px; - } - QPushButton:pressed { background-color: #4a4a4a; } -)"; +// clpActionBtnStyle removed — no longer used // Shutdown timer: param value -> display label static QString shutdownLabel(int val) { @@ -220,10 +213,8 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) { general_panel->setContentsMargins(50, 25, 50, 25); // Status button - QPushButton *status_btn = new QPushButton("Status"); - status_btn->setFixedHeight(120); - status_btn->setStyleSheet(clpActionBtnStyle); - QObject::connect(status_btn, &QPushButton::clicked, [=]() { emit openStatus(); }); + auto *status_btn = new ButtonControl("System Status", "VIEW", ""); + connect(status_btn, &ButtonControl::clicked, [=]() { emit openStatus(); }); general_panel->addItem(status_btn); // Reset Calibration @@ -364,6 +355,18 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) { "Captures only changed values for efficiency.", "", this); debug_panel->addItem(telemetry_toggle); + auto *vpn_toggle = new ParamControl("VpnEnabled", "VPN", + "Connect to vpn.hanson.xyz for remote SSH access. " + "Disabling kills the active tunnel and stops reconnection attempts.", "", this); + QObject::connect(vpn_toggle, &ToggleControl::toggleFlipped, [](bool on) { + if (on) { + std::system("sudo bash -c 'nohup /data/openpilot/system/clearpilot/vpn-monitor.sh >> /tmp/vpn-monitor.log 2>&1 &'"); + } else { + std::system("sudo pkill -TERM -f vpn-monitor.sh; sudo killall openvpn"); + } + }); + debug_panel->addItem(vpn_toggle); + // ── Register panels with sidebar buttons ── QList> panels = { {"General", general_panel}, @@ -372,31 +375,34 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) { {"Debug", debug_panel}, }; + nav_group = new QButtonGroup(this); + nav_group->setExclusive(true); + for (auto &[name, panel] : panels) { QPushButton *btn = new QPushButton(name); btn->setCheckable(true); btn->setStyleSheet(clpSidebarBtnStyle); btn->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Expanding); sidebar_layout->addWidget(btn, 0, Qt::AlignRight); + nav_group->addButton(btn); // Network panel handles its own scrolling/margins if (name == "Network") { panel_widget->addWidget(panel); QObject::connect(btn, &QPushButton::clicked, [=, w = panel]() { - btn->setChecked(true); panel_widget->setCurrentWidget(w); }); } else { ScrollView *panel_frame = new ScrollView(panel, this); panel_widget->addWidget(panel_frame); QObject::connect(btn, &QPushButton::clicked, [=, w = panel_frame]() { - btn->setChecked(true); panel_widget->setCurrentWidget(w); }); } } // Select General by default + nav_group->buttons().first()->setChecked(true); panel_widget->setCurrentIndex(0); // Main layout: sidebar + panels @@ -425,3 +431,8 @@ ClearPilotPanel::ClearPilotPanel(QWidget* parent) : QFrame(parent) { #poweroff_btn:pressed { background-color: #FF2424; } )"); } + +void ClearPilotPanel::resetToGeneral() { + panel_widget->setCurrentIndex(0); + nav_group->buttons().first()->setChecked(true); +} diff --git a/selfdrive/ui/qt/home.h b/selfdrive/ui/qt/home.h index 2fb4e26..bec2527 100755 --- a/selfdrive/ui/qt/home.h +++ b/selfdrive/ui/qt/home.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -28,8 +29,12 @@ signals: void openStatus(); void closePanel(); +public: + void resetToGeneral(); + private: QStackedWidget *panel_widget; + QButtonGroup *nav_group; }; class HomeWindow : public QWidget { diff --git a/selfdrive/ui/qt/onroad.cc b/selfdrive/ui/qt/onroad.cc index 19a8dcb..f72098d 100755 --- a/selfdrive/ui/qt/onroad.cc +++ b/selfdrive/ui/qt/onroad.cc @@ -316,10 +316,7 @@ AnnotatedCameraWidget::AnnotatedCameraWidget(VisionStreamType type, QWidget* par QHBoxLayout *buttons_layout = new QHBoxLayout(); buttons_layout->setSpacing(0); - // Neokii screen recorder - recorder_btn = new ScreenRecorder(this); - recorder_btn->setVisible(false); - // buttons_layout->addWidget(recorder_btn); + // Neokii screen recorder — DISABLED: using dashcamd instead QVBoxLayout *top_right_layout = new QVBoxLayout(); top_right_layout->setSpacing(0); @@ -1074,8 +1071,7 @@ void AnnotatedCameraWidget::paintFrogPilotWidgets(QPainter &p) { drawSLCConfirmation(p); } - // CLEARPILOT: screen recorder runs invisibly, no UI button shown - recorder_btn->setVisible(false); + // CLEARPILOT: screen recorder disabled, using dashcamd instead } void AnnotatedCameraWidget::drawLeadInfo(QPainter &p) { diff --git a/selfdrive/ui/qt/onroad.h b/selfdrive/ui/qt/onroad.h index 3536b5d..8c9f1a0 100755 --- a/selfdrive/ui/qt/onroad.h +++ b/selfdrive/ui/qt/onroad.h @@ -12,7 +12,7 @@ #include "selfdrive/ui/ui.h" #include "selfdrive/ui/qt/widgets/cameraview.h" -#include "selfdrive/frogpilot/screenrecorder/screenrecorder.h" +// #include "selfdrive/frogpilot/screenrecorder/screenrecorder.h" // DISABLED: using dashcamd instead const int btn_size = 192; const int img_size = (btn_size / 4) * 3; @@ -91,8 +91,6 @@ private: Params paramsMemory{"/dev/shm/params"}; UIScene &scene; - ScreenRecorder *recorder_btn; - QHBoxLayout *bottom_layout; bool alwaysOnLateralActive; diff --git a/selfdrive/ui/qt/ready.cc b/selfdrive/ui/qt/ready.cc index aa82f52..b6864c8 100755 --- a/selfdrive/ui/qt/ready.cc +++ b/selfdrive/ui/qt/ready.cc @@ -30,11 +30,12 @@ ReadyWindow::ReadyWindow(QWidget *parent) : QWidget(parent) { timer = new QTimer(this); timer->callOnTimeout(this, &ReadyWindow::refresh); + uptime.start(); } void ReadyWindow::showEvent(QShowEvent *event) { refresh(); - timer->start(5 * 1000); + timer->start(2 * 1000); } void ReadyWindow::hideEvent(QHideEvent *event) { @@ -43,34 +44,87 @@ void ReadyWindow::hideEvent(QHideEvent *event) { void ReadyWindow::paintEvent(QPaintEvent *event) { QPainter painter(this); - QPixmap *img_shown = nullptr; + painter.fillRect(rect(), Qt::black); if (is_hot) { if (img_hot.isNull()) { img_hot.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/hot.png"); } - img_shown = &img_hot; + int x = (width() - img_hot.width()) / 2; + int y = (height() - img_hot.height()) / 2; + painter.drawPixmap(x, y, img_hot); } else { - if (img_ready.isNull()) { - img_ready.load("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready.png"); + // Boot logo — same rotation as spinner (bg.jpg is pre-rotated 90° CW for framebuffer) + if (img_bg.isNull()) { + QPixmap raw("/usr/comma/bg.jpg"); + if (!raw.isNull()) { + QTransform rot; + rot.rotate(-90); + img_bg = raw.transformed(rot); + } + } + if (!img_bg.isNull()) { + QPixmap scaled = img_bg.scaled(size(), Qt::KeepAspectRatio, Qt::SmoothTransformation); + int x = (width() - scaled.width()) / 2; + int y = (height() - scaled.height()) / 2; + painter.drawPixmap(x, y, scaled); } - img_shown = &img_ready; - } - int x = (width() - img_shown->width()) / 2; - int y = (height() - img_shown->height()) / 2; - painter.drawPixmap(x, y, *img_shown); + if (error_msg.isEmpty()) { + // "READY!" 8-bit text sprite, 2x size, 15% below center + static QPixmap ready_text("/data/openpilot/selfdrive/clearpilot/theme/clearpilot/images/ready_text.png"); + if (!ready_text.isNull()) { + QPixmap scaled = ready_text.scaled(ready_text.width() * 3 / 2, ready_text.height() * 3 / 2, Qt::KeepAspectRatio, Qt::FastTransformation); + int tx = (width() - scaled.width()) / 2; + int ty = height() / 2 + height() * 15 / 100; + painter.drawPixmap(tx, ty, scaled); + } + } else { + // Error state: red text at 25% below center + QFont font("Inter", 50, QFont::Bold); + painter.setFont(font); + painter.setPen(QColor(0xFF, 0x44, 0x44)); + int ty = height() / 2 + height() * 25 / 100; + QRect text_rect(0, ty, width(), 100); + painter.drawText(text_rect, Qt::AlignHCenter | Qt::AlignTop, error_msg); + } + } } void ReadyWindow::refresh() { + bool changed = false; + + // Temperature check std::string bytes = params.get("Offroad_TemperatureTooHigh"); + bool was_hot = is_hot; if (!bytes.empty()) { auto doc = QJsonDocument::fromJson(bytes.data()); is_hot = true; cur_temp = doc["extra"].toString(); - update(); - } else if (is_hot) { + } else { is_hot = false; - update(); } + if (is_hot != was_hot) changed = true; + + // Error state checks (only when not hot — hot has its own display) + if (!is_hot) { + QString prev_error = error_msg; + + // Panda check — same logic as sidebar, with 10s grace period on startup + if (uptime.elapsed() > 10000 && + uiState()->scene.pandaType == cereal::PandaState::PandaType::UNKNOWN) { + error_msg = "PANDA NOT CONNECTED"; + } + // Car unrecognized check + else if (!params.get("Offroad_CarUnrecognized").empty()) { + error_msg = "CAR NOT RECOGNIZED"; + } + else { + error_msg = ""; + } + + if (error_msg != prev_error) changed = true; + } + + if (changed) update(); } \ No newline at end of file diff --git a/selfdrive/ui/qt/ready.h b/selfdrive/ui/qt/ready.h index c2bb4d8..42eb822 100755 --- a/selfdrive/ui/qt/ready.h +++ b/selfdrive/ui/qt/ready.h @@ -8,8 +8,9 @@ #include #include #include +#include #include - + #include "common/util.h" #include "selfdrive/ui/ui.h" @@ -26,6 +27,8 @@ private: QTimer* timer; bool is_hot = false; QString cur_temp; - QPixmap img_ready; + QString error_msg; // non-empty = show red error instead of READY! + QElapsedTimer uptime; + QPixmap img_bg; QPixmap img_hot; }; \ No newline at end of file diff --git a/selfdrive/ui/qt/spinner b/selfdrive/ui/qt/spinner index a6d6ecb..afdb1c3 100755 Binary files a/selfdrive/ui/qt/spinner and b/selfdrive/ui/qt/spinner differ diff --git a/selfdrive/ui/qt/window.cc b/selfdrive/ui/qt/window.cc index c37c752..b001b0e 100755 --- a/selfdrive/ui/qt/window.cc +++ b/selfdrive/ui/qt/window.cc @@ -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 #include #include +#include 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::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 { diff --git a/selfdrive/ui/qt/window.h b/selfdrive/ui/qt/window.h index 0fbb8e4..746491a 100755 --- a/selfdrive/ui/qt/window.h +++ b/selfdrive/ui/qt/window.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -16,6 +17,12 @@ class StatusWindow : public QFrame { public: explicit StatusWindow(QWidget *parent = 0); + struct StatusData { + QString time, storage, ram, load, temp, fan, ip, wifi; + QString vpn_status, vpn_ip, gps, telemetry; + float temp_c = 0; + }; + protected: void mousePressEvent(QMouseEvent *e) override; @@ -23,9 +30,14 @@ signals: void closeStatus(); private slots: - void refresh(); + void kickRefresh(); + void applyResults(); private: + + QFutureWatcher watcher; + bool collecting = false; + QLabel *storage_label; QLabel *ram_label; QLabel *load_label; @@ -37,6 +49,7 @@ private: QLabel *gps_label; QLabel *time_label; QLabel *telemetry_label; + QLabel *panda_label; }; class MainWindow : public QWidget { diff --git a/system/clearpilot/vpn-monitor.sh b/system/clearpilot/vpn-monitor.sh new file mode 100755 index 0000000..0f9a6a5 --- /dev/null +++ b/system/clearpilot/vpn-monitor.sh @@ -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 diff --git a/system/clearpilot/vpn.ovpn b/system/clearpilot/vpn.ovpn new file mode 100644 index 0000000..8fd73ec --- /dev/null +++ b/system/clearpilot/vpn.ovpn @@ -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 " + + +-----BEGIN CERTIFICATE----- +MIIB2jCCAX+gAwIBAgIUFVGjbK1Qb5d3RkkoNPMsXeI/xVAwCgYIKoZIzj0EAwIw +HjEcMBoGA1UEAwwTT3BlblZQTi1JbnRlcm5hbC1DQTAgFw0yNjAyMDcwODQ3Mzda +GA8yMTI2MDExNDA4NDczN1owHjEcMBoGA1UEAwwTT3BlblZQTi1JbnRlcm5hbC1D +QTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABGb6RWOFnCJ9t7X5q6fqpv0y3Hg/ +dTU3ky+MAjfPRYfUWfiM7wVKubYOCc+pUHsJXWaghqu7nQoCeSzVDcPXlWGjgZgw +gZUwDAYDVR0TBAUwAwEB/zAdBgNVHQ4EFgQUNThAWabF1zsqNE19iCKuZMjHBIUw +WQYDVR0jBFIwUIAUNThAWabF1zsqNE19iCKuZMjHBIWhIqQgMB4xHDAaBgNVBAMM +E09wZW5WUE4tSW50ZXJuYWwtQ0GCFBVRo2ytUG+Xd0ZJKDTzLF3iP8VQMAsGA1Ud +DwQEAwIBBjAKBggqhkjOPQQDAgNJADBGAiEA2mPwEK8G4HXlRu6WZVSRdqyCPYYd +KffYalCXgw3pZ/sCIQC9qPNckHtubycu8kq4iM8Vl1vYMVEorn7DUFdXJCvtcg== +-----END CERTIFICATE----- + + + +-----BEGIN CERTIFICATE----- +MIIB2TCCAYCgAwIBAgIRALuRBSB68/ccWM8SASfEIV0wCgYIKoZIzj0EAwIwHjEc +MBoGA1UEAwwTT3BlblZQTi1JbnRlcm5hbC1DQTAgFw0yNjA0MTIwMDA1NDhaGA8y +MTI2MDMxOTAwMDU0OFowEDEOMAwGA1UEAwwFY29tbWEwWTATBgcqhkjOPQIBBggq +hkjOPQMBBwNCAAQ/jN83Z2Ikk+IWVPGxN0CNFCh74Yrb3W6VXAjGWa+ppVxSbdeq +YVBWjJl6qSg6n2ZMDivQ5NcKgsxMcY9ly/LEo4GqMIGnMAkGA1UdEwQCMAAwHQYD +VR0OBBYEFDIulLc8hAwTkGHq+z8K8eBBM0vVMFkGA1UdIwRSMFCAFDU4QFmmxdc7 +KjRNfYgirmTIxwSFoSKkIDAeMRwwGgYDVQQDDBNPcGVuVlBOLUludGVybmFsLUNB +ghQVUaNsrVBvl3dGSSg08yxd4j/FUDATBgNVHSUEDDAKBggrBgEFBQcDAjALBgNV +HQ8EBAMCB4AwCgYIKoZIzj0EAwIDRwAwRAIgR/ssLDNLmt1s0WXwGLszBUrlstUu +9nhP2PcmdnsOit4CIECFbQ7RHEZLQJWsL2DvKowCCzDtA6ZGDILTVfHwNyDn +-----END CERTIFICATE----- + + + +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgYolghDmo5ISWxjQy +sayXuFRSW5fkiIXJ1SGvSRLnmBmhRANCAAQ/jN83Z2Ikk+IWVPGxN0CNFCh74Yrb +3W6VXAjGWa+ppVxSbdeqYVBWjJl6qSg6n2ZMDivQ5NcKgsxMcY9ly/LE +-----END PRIVATE KEY----- + + + +# +# 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----- +