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