deploy: hardware-instance templating, wlan0 boot watchdog, deploy-time warnings in Cockpit
- deploy.conf templates interface names/USB IDs (@TOKEN@ substitution) across ap/* configs so a dongle swap only needs deploy.conf edited, not the repo configs themselves; drops ap/rtw88.conf (old 2.4GHz dongle retired for the DWA-171, which needs no such power-save override). - failover/van-wlan-watchdog: recovers wlan0 from NM's post-boot no-secrets wedge (a boot-time supplicant race, not a real credential failure). - deploy.sh: warn() collects dependency/config warnings (missing python3-gps, python3-paho-mqtt, mobile-broadband-provider-info, grpcurl, gpsd; netplan drift; unedited example configs) into /var/lib/vanlink/deploy-warnings.json, rendered as an amber Cockpit card so they're visible without reading deploy output. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
8d5415f326
commit
5d88e1b30c
@@ -1,5 +1,5 @@
|
|||||||
[Match]
|
[Match]
|
||||||
Name=wlxc83a35a4ee55
|
Name=@WIFI_5G_IFACE@
|
||||||
|
|
||||||
[Link]
|
[Link]
|
||||||
RequiredForOnline=no
|
RequiredForOnline=no
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Match]
|
[Match]
|
||||||
Name=wlxd8ec5e2faa8c
|
Name=@WIFI_2G_IFACE@
|
||||||
|
|
||||||
[Link]
|
[Link]
|
||||||
RequiredForOnline=no
|
RequiredForOnline=no
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
# gets DHCP/DNS from the same dnsmasq as Wi-Fi clients. NM must leave this device
|
# gets DHCP/DNS from the same dnsmasq as Wi-Fi clients. NM must leave this device
|
||||||
# alone (see van-ap-unmanaged.conf) for networkd to own it here.
|
# alone (see van-ap-unmanaged.conf) for networkd to own it here.
|
||||||
[Match]
|
[Match]
|
||||||
Name=enx00e04c331140
|
Name=@LAN_USB_IFACE@
|
||||||
|
|
||||||
[Link]
|
[Link]
|
||||||
RequiredForOnline=no
|
RequiredForOnline=no
|
||||||
|
|||||||
+5
-4
@@ -10,10 +10,11 @@ network:
|
|||||||
version: 2
|
version: 2
|
||||||
ethernets:
|
ethernets:
|
||||||
# Starlink dish uplink (RTL8153 USB NIC, MAC-named — travels with the
|
# Starlink dish uplink (RTL8153 USB NIC, MAC-named — travels with the
|
||||||
# adapter). The /32 link route keeps the dish's management address reachable
|
# adapter, see STARLINK_IFACE in deploy.conf). The /32 link route keeps the
|
||||||
# no matter which WAN holds the default route: the dish answers on
|
# dish's management address reachable no matter which WAN holds the
|
||||||
# 192.168.100.1 (gRPC :9200) even while the uplink sits behind CGNAT.
|
# default route: the dish answers on 192.168.100.1 (gRPC :9200) even while
|
||||||
enxd8ec5eeb3512:
|
# the uplink sits behind CGNAT.
|
||||||
|
@STARLINK_IFACE@:
|
||||||
renderer: NetworkManager
|
renderer: NetworkManager
|
||||||
optional: true
|
optional: true
|
||||||
dhcp4: true
|
dhcp4: true
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
interface=wlxd8ec5e2faa8c
|
interface=@WIFI_2G_IFACE@
|
||||||
# Second, independent hostapd instance (hostapd-2g.service) — 2.4GHz band of the same
|
# Second, independent hostapd instance (hostapd-2g.service) — 2.4GHz band of the same
|
||||||
# VanLink network, bridged into the same br0 segment as the 5GHz AP and the wired port.
|
# VanLink network, bridged into the same br0 segment as the 5GHz AP and the wired port.
|
||||||
# Separate process on purpose: a USB wedge on one radio must never take down the other.
|
# Separate process on purpose: a USB wedge on one radio must never take down the other.
|
||||||
@@ -17,7 +17,7 @@ wmm_enabled=1
|
|||||||
# HT20 only — 40MHz in 2.4GHz overlaps most of the band and coexistence would force
|
# HT20 only — 40MHz in 2.4GHz overlaps most of the band and coexistence would force
|
||||||
# it back to 20MHz near any neighbor anyway.
|
# it back to 20MHz near any neighbor anyway.
|
||||||
ieee80211n=1
|
ieee80211n=1
|
||||||
ht_capab=[LDPC][SHORT-GI-20]
|
ht_capab=[SHORT-GI-20]
|
||||||
auth_algs=1
|
auth_algs=1
|
||||||
wpa=2
|
wpa=2
|
||||||
wpa_passphrase=1foot11foot11
|
wpa_passphrase=1foot11foot11
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
[Unit]
|
[Unit]
|
||||||
Description=VanLink 2.4GHz AP (hostapd on wlxd8ec5e2faa8c, RTL8822BU)
|
Description=VanLink 2.4GHz AP (hostapd on @WIFI_2G_IFACE@ — see deploy.conf for current hardware)
|
||||||
# Same USB re-enumeration hazards as the 5GHz AP (see hostapd-restart.conf for the
|
# Same USB re-enumeration hazards as the 5GHz AP (see hostapd-restart.conf for the
|
||||||
# full story): never stop retrying, and back off a few seconds so the USB device can
|
# full story): never stop retrying, and back off a few seconds so the USB device can
|
||||||
# re-enumerate before the next attempt.
|
# re-enumerate before the next attempt.
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
interface=wlxc83a35a4ee55
|
interface=@WIFI_5G_IFACE@
|
||||||
# Put the AP into br0 so Wi-Fi and the wired LAN port share one 10.42.0.0/24 segment.
|
# Put the AP into br0 so Wi-Fi and the wired LAN port share one 10.42.0.0/24 segment.
|
||||||
# hostapd adds the wlan to the bridge after setting AP mode; the bridge itself + its
|
# hostapd adds the wlan to the bridge after setting AP mode; the bridge itself + its
|
||||||
# wired member + the gateway IP are defined under /etc/systemd/network (2x-van-br0/lan).
|
# wired member + the gateway IP are defined under /etc/systemd/network (2x-van-br0/lan).
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
# 2.4GHz AP dongle (Linksys WUSB6300 v2, RTL8822BU, rtw88_8822bu).
|
|
||||||
# Deep power-save has no business on an always-on AP radio — same reasoning as the
|
|
||||||
# rtw89 disable_ps_mode gotcha on the 5GHz dongle (rtw89.conf).
|
|
||||||
options rtw88_core disable_lps_deep=Y
|
|
||||||
# Don't let the driver switch a USB2-enumerated dongle up to USB3: USB3 signalling
|
|
||||||
# radiates broadband noise right across the 2.4GHz band, and HT20 rates fit in USB2
|
|
||||||
# easily. NOTE: this only blocks the driver's own USB2->3 switch — a dongle sitting in
|
|
||||||
# a USB3 port still enumerates at SuperSpeed (it currently does); move it to a USB2
|
|
||||||
# port if 2.4GHz range/interference ever looks poor.
|
|
||||||
options rtw88_usb switch_usb_mode=N
|
|
||||||
@@ -2,4 +2,4 @@
|
|||||||
# The AP wlans (5GHz + 2.4GHz) and the wired LAN ports (USB dongle + onboard
|
# The AP wlans (5GHz + 2.4GHz) and the wired LAN ports (USB dongle + onboard
|
||||||
# eth0) are all owned by systemd-networkd/hostapd (bridged into br0), so
|
# eth0) are all owned by systemd-networkd/hostapd (bridged into br0), so
|
||||||
# NetworkManager must not touch them.
|
# NetworkManager must not touch them.
|
||||||
unmanaged-devices=interface-name:wlxc83a35a4ee55;interface-name:wlxd8ec5e2faa8c;interface-name:enx00e04c331140;interface-name:eth0
|
unmanaged-devices=interface-name:@WIFI_5G_IFACE@;interface-name:@WIFI_2G_IFACE@;interface-name:@LAN_USB_IFACE@;interface-name:eth0
|
||||||
|
|||||||
@@ -15,6 +15,11 @@
|
|||||||
<span id="updated" class="muted">loading…</span>
|
<span id="updated" class="muted">loading…</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card" id="deploywarn-card" style="display:none">
|
||||||
|
<h3>Deploy Warnings</h3>
|
||||||
|
<div id="deploywarn"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>Access Points</h3>
|
<h3>Access Points</h3>
|
||||||
<div id="ap"></div>
|
<div id="ap"></div>
|
||||||
@@ -45,6 +50,14 @@
|
|||||||
<div id="starlink"></div>
|
<div id="starlink"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-head">
|
||||||
|
<h3>Wi-Fi Networks</h3>
|
||||||
|
<button id="wifi-scan" class="btn">Scan</button>
|
||||||
|
</div>
|
||||||
|
<div id="wifi"><p class="muted">Click Scan to search for networks.</p></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="card">
|
<div class="card">
|
||||||
<h3>WAN / Uplinks</h3>
|
<h3>WAN / Uplinks</h3>
|
||||||
<table>
|
<table>
|
||||||
|
|||||||
@@ -23,3 +23,7 @@ th { color: #6a6e73; font-weight: 600; }
|
|||||||
.active-wan { font-weight: 700; color: #0066cc; }
|
.active-wan { font-weight: 700; color: #0066cc; }
|
||||||
.btn { margin-left: 6px; padding: 3px 10px; cursor: pointer; }
|
.btn { margin-left: 6px; padding: 3px 10px; cursor: pointer; }
|
||||||
.btn[disabled] { cursor: default; opacity: 0.5; }
|
.btn[disabled] { cursor: default; opacity: 0.5; }
|
||||||
|
#deploywarn-card { border-color: #e08a00; }
|
||||||
|
.warn-list { margin: 0; padding-left: 20px; }
|
||||||
|
.warn-list li { background: #f5d9a8; color: #5f4414; border-radius: 4px;
|
||||||
|
padding: 4px 8px; margin: 4px 0; list-style: none; margin-left: -20px; }
|
||||||
|
|||||||
+141
-12
@@ -7,18 +7,20 @@
|
|||||||
// { superuser: "require" } which triggers Cockpit's admin (polkit) escalation.
|
// { superuser: "require" } which triggers Cockpit's admin (polkit) escalation.
|
||||||
|
|
||||||
// AP radios (MAC-derived iface names, stable) — each runs its own hostapd unit.
|
// AP radios (MAC-derived iface names, stable) — each runs its own hostapd unit.
|
||||||
|
// Interface names/vendor ID below are templated from deploy.conf at deploy
|
||||||
|
// time (see deploy.sh's render()) — edit deploy.conf, not the values here.
|
||||||
const APS = [
|
const APS = [
|
||||||
{ iface: "wlxc83a35a4ee55", unit: "hostapd", band: "5GHz" }, // RTL8852BU
|
{ iface: "@WIFI_5G_IFACE@", unit: "hostapd", band: "5GHz" },
|
||||||
{ iface: "wlxd8ec5e2faa8c", unit: "hostapd-2g", band: "2.4GHz" }, // RTL8822BU
|
{ iface: "@WIFI_2G_IFACE@", unit: "hostapd-2g", band: "2.4GHz" },
|
||||||
];
|
];
|
||||||
const PREFER_FILE = "/run/van-failover/prefer"; // van-failover reads this to pick the preferred WAN
|
const PREFER_FILE = "/run/van-failover/prefer"; // van-failover reads this to pick the preferred WAN
|
||||||
// Wired LAN bridge ports: clients behind them are found via the bridge FDB.
|
// Wired LAN bridge ports: clients behind them are found via the bridge FDB.
|
||||||
const LAN_PORTS = { "eth0": "LAN (eth0)", "enx00e04c331140": "LAN (USB)" };
|
const LAN_PORTS = { "eth0": "LAN (eth0)", "@LAN_USB_IFACE@": "LAN (USB)" };
|
||||||
// Starlink dish: gRPC status API on the fixed management IP (reached via the /32
|
// Starlink dish: gRPC status API on the fixed management IP (reached via the /32
|
||||||
// link route the netplan profile installs on the RTL8153 uplink).
|
// link route the netplan profile installs on the RTL8153 uplink).
|
||||||
const STARLINK = { iface: "enxd8ec5eeb3512", dish: "192.168.100.1:9200" };
|
const STARLINK = { iface: "@STARLINK_IFACE@", dish: "192.168.100.1:9200" };
|
||||||
// Cellular modem (Quectel EC25-AF): used to find the USB device for a hard restart.
|
// Cellular modem (Quectel EC25-AF): used to find the USB device for a hard restart.
|
||||||
const MODEM_USB_VENDOR = "2c7c";
|
const MODEM_USB_VENDOR = "@MODEM_USB_VENDOR@";
|
||||||
|
|
||||||
function run(args, opts) {
|
function run(args, opts) {
|
||||||
return cockpit.spawn(args, Object.assign({ err: "message" }, opts || {}));
|
return cockpit.spawn(args, Object.assign({ err: "message" }, opts || {}));
|
||||||
@@ -47,6 +49,7 @@ const STATUS_SCRIPT = (() => {
|
|||||||
add(`info${i}`, `iw dev ${a.iface} info`);
|
add(`info${i}`, `iw dev ${a.iface} info`);
|
||||||
add(`stations${i}`, `iw dev ${a.iface} station dump`);
|
add(`stations${i}`, `iw dev ${a.iface} station dump`);
|
||||||
});
|
});
|
||||||
|
add("deploywarn", "cat /var/lib/vanlink/deploy-warnings.json");
|
||||||
add("neigh", "ip -j neigh show dev br0");
|
add("neigh", "ip -j neigh show dev br0");
|
||||||
add("fdb", "bridge -j fdb show br br0");
|
add("fdb", "bridge -j fdb show br br0");
|
||||||
add("leases", "cat /var/lib/misc/dnsmasq.leases");
|
add("leases", "cat /var/lib/misc/dnsmasq.leases");
|
||||||
@@ -83,6 +86,21 @@ function parseJSON(text, fallback) {
|
|||||||
try { return JSON.parse(text); } catch (e) { return fallback; }
|
try { return JSON.parse(text); } catch (e) { return fallback; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Deploy warnings (deploy.sh's collected warn() calls) ---------- */
|
||||||
|
|
||||||
|
function renderDeployWarnings(dw) {
|
||||||
|
const card = document.getElementById("deploywarn-card");
|
||||||
|
const warnings = (dw && dw.warnings) || [];
|
||||||
|
if (!warnings.length) { card.style.display = "none"; return; }
|
||||||
|
card.style.display = "";
|
||||||
|
let html = `<ul class="warn-list">`;
|
||||||
|
warnings.forEach(w => { html += `<li>${esc(w)}</li>`; });
|
||||||
|
html += `</ul>`;
|
||||||
|
html += `<p class="muted">From last deploy (${esc(dw.deployed || "—")}) — ` +
|
||||||
|
`re-run <code>sudo ./deploy.sh</code> after fixing to clear.</p>`;
|
||||||
|
document.getElementById("deploywarn").innerHTML = html;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Access Point ---------- */
|
/* ---------- Access Point ---------- */
|
||||||
|
|
||||||
function parseAP(a, i, secs) {
|
function parseAP(a, i, secs) {
|
||||||
@@ -321,7 +339,7 @@ function renderFailover(fo) {
|
|||||||
` <span class="muted">· updated ${esc(fo.updated || "")}</span></p>`;
|
` <span class="muted">· updated ${esc(fo.updated || "")}</span></p>`;
|
||||||
html += `<table><thead><tr><th>WAN</th><th>Priority</th><th>Device</th><th>Status</th><th>Metric</th></tr></thead><tbody>`;
|
html += `<table><thead><tr><th>WAN</th><th>Priority</th><th>Device</th><th>Status</th><th>Metric</th></tr></thead><tbody>`;
|
||||||
fo.wans.forEach(w => {
|
fo.wans.forEach(w => {
|
||||||
const status = !w.present ? `<span class="pill">absent</span>`
|
const status = !w.present ? `<span class="pill warn">absent</span>`
|
||||||
: w.up ? `<span class="pill ok">up</span>`
|
: w.up ? `<span class="pill ok">up</span>`
|
||||||
: `<span class="pill bad">down</span>`;
|
: `<span class="pill bad">down</span>`;
|
||||||
const prio = w.preferred ? `${esc(w.priority)} <span class="muted">(preferred)</span>` : esc(w.priority);
|
const prio = w.preferred ? `${esc(w.priority)} <span class="muted">(preferred)</span>` : esc(w.priority);
|
||||||
@@ -333,6 +351,106 @@ function renderFailover(fo) {
|
|||||||
el.innerHTML = html;
|
el.innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---------- Wi-Fi network selector (wlan0 WAN) ---------- */
|
||||||
|
|
||||||
|
// The onboard radio used for the WiFi WAN — unlike the AP dongles it's never
|
||||||
|
// templated, this host only ever has the one.
|
||||||
|
const WIFI_IFACE = "wlan0";
|
||||||
|
|
||||||
|
// SSIDs the van's own AP radios are currently beaconing (kept live from refresh()'s
|
||||||
|
// `iw dev ... info` parse, not hardcoded) — wlan0 "connecting" to its own AP would be
|
||||||
|
// a nonsensical loop, so those rows get their Connect button disabled below.
|
||||||
|
let ownAPSSIDs = new Set();
|
||||||
|
|
||||||
|
// nmcli -t escapes ':' inside field values as '\:' — split on unescaped ':' only.
|
||||||
|
// SSIDs are environment-controlled strings (the van parks near arbitrary APs), so
|
||||||
|
// worth handling properly rather than a naive split(":").
|
||||||
|
function splitNmcli(line) {
|
||||||
|
const parts = [];
|
||||||
|
let cur = "";
|
||||||
|
for (let i = 0; i < line.length; i++) {
|
||||||
|
if (line[i] === "\\" && line[i + 1] === ":") { cur += ":"; i++; }
|
||||||
|
else if (line[i] === ":") { parts.push(cur); cur = ""; }
|
||||||
|
else cur += line[i];
|
||||||
|
}
|
||||||
|
parts.push(cur);
|
||||||
|
return parts;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderWifiList(rows) {
|
||||||
|
const el = document.getElementById("wifi");
|
||||||
|
if (!rows.length) {
|
||||||
|
el.innerHTML = `<p class="muted">No networks found. Try Scan.</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let html = `<table><thead><tr><th>SSID</th><th>Signal</th><th>Security</th><th></th></tr></thead><tbody>`;
|
||||||
|
rows.forEach(r => {
|
||||||
|
const ownAP = ownAPSSIDs.has(r.ssid);
|
||||||
|
const label = r.inUse ? "Connected" : ownAP ? "Own AP" : "Connect";
|
||||||
|
html += `<tr><td>${r.inUse ? "★ " : ""}${esc(r.ssid)}</td>` +
|
||||||
|
`<td>${esc(r.signal)}%</td><td>${esc(r.security || "open")}</td>` +
|
||||||
|
`<td class="acts"><button class="btn wifi-connect"${ownAP ? ` title="This is the van's own VanLink AP — wlan0 can't usefully connect to it"` : ""}>${label}</button></td></tr>`;
|
||||||
|
});
|
||||||
|
html += `</tbody></table>`;
|
||||||
|
el.innerHTML = html;
|
||||||
|
el.querySelectorAll(".wifi-connect").forEach((btn, i) => {
|
||||||
|
const r = rows[i];
|
||||||
|
btn.disabled = r.inUse || ownAPSSIDs.has(r.ssid);
|
||||||
|
btn.onclick = () => connectWifi(r);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function scanWifi() {
|
||||||
|
const el = document.getElementById("wifi");
|
||||||
|
el.innerHTML = `<p class="muted">Scanning…</p>`;
|
||||||
|
try {
|
||||||
|
// Actually triggering a rescan (not just listing NM's cache) needs the
|
||||||
|
// org.freedesktop.NetworkManager.wifi.scan polkit action, which a plain
|
||||||
|
// Cockpit session doesn't have — without escalation this silently returns
|
||||||
|
// only the cached/connected AP instead of erroring.
|
||||||
|
const out = await run(["nmcli", "-t", "-f", "IN-USE,SSID,SIGNAL,SECURITY",
|
||||||
|
"device", "wifi", "list", "ifname", WIFI_IFACE, "--rescan", "yes"],
|
||||||
|
{ superuser: "require" });
|
||||||
|
const seen = new Set();
|
||||||
|
const rows = out.trim().split("\n").filter(Boolean).map(splitNmcli).map(f => ({
|
||||||
|
inUse: f[0].indexOf("*") !== -1, ssid: f[1], signal: f[2], security: f[3],
|
||||||
|
})).filter(r => {
|
||||||
|
if (!r.ssid || seen.has(r.ssid)) return false;
|
||||||
|
seen.add(r.ssid);
|
||||||
|
return true;
|
||||||
|
}).sort((a, b) => (b.inUse - a.inUse) || (parseInt(b.signal, 10) - parseInt(a.signal, 10)));
|
||||||
|
renderWifiList(rows);
|
||||||
|
} catch (e) {
|
||||||
|
el.innerHTML = `<p class="muted">Scan failed: ${esc(e.message)}</p>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function connectWifi(r) {
|
||||||
|
if (ownAPSSIDs.has(r.ssid)) return; // belt-and-suspenders; button is disabled too
|
||||||
|
// Try without a password first — reuses a saved profile's stored secret (or
|
||||||
|
// just works for an open network); only prompt if NM actually needs one.
|
||||||
|
try {
|
||||||
|
await run(["nmcli", "device", "wifi", "connect", r.ssid, "ifname", WIFI_IFACE],
|
||||||
|
{ superuser: "require" });
|
||||||
|
scanWifi();
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
if (!/secret|password|psk/i.test(e.message || "")) {
|
||||||
|
window.alert("Connect failed: " + e.message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const pwd = window.prompt(`Password for "${r.ssid}":`);
|
||||||
|
if (!pwd) return;
|
||||||
|
try {
|
||||||
|
await run(["nmcli", "device", "wifi", "connect", r.ssid, "password", pwd, "ifname", WIFI_IFACE],
|
||||||
|
{ superuser: "require" });
|
||||||
|
} catch (e) { window.alert("Connect failed: " + e.message); }
|
||||||
|
scanWifi();
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("wifi-scan").onclick = scanWifi;
|
||||||
|
|
||||||
/* ---------- WAN / uplinks ---------- */
|
/* ---------- WAN / uplinks ---------- */
|
||||||
|
|
||||||
// mmcli -K key-values -> { netdev, signal, tech, operator } (null if no modem).
|
// mmcli -K key-values -> { netdev, signal, tech, operator } (null if no modem).
|
||||||
@@ -402,16 +520,24 @@ function parseWAN(secs) {
|
|||||||
return devs;
|
return devs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NM device states that count as "ready to serve traffic" vs. transient vs.
|
||||||
|
// broken/unusable — drives the state-column pill color.
|
||||||
|
const WAN_STATE_PILL = { connected: "ok", connecting: "warn" };
|
||||||
|
|
||||||
function renderWAN(devs) {
|
function renderWAN(devs) {
|
||||||
const tb = document.getElementById("wan");
|
const tb = document.getElementById("wan");
|
||||||
tb.innerHTML = "";
|
tb.innerHTML = "";
|
||||||
devs.forEach(d => {
|
devs.forEach(d => {
|
||||||
const isUp = d.state === "connected";
|
const isConnected = d.state === "connected";
|
||||||
|
// No carrier: nmcli can't connect a device with nothing on the other
|
||||||
|
// end of the wire, so the button would just fail — disable it rather
|
||||||
|
// than let it produce a silent "Toggle failed" alert.
|
||||||
|
const noCarrier = d.state === "unavailable";
|
||||||
const tr = document.createElement("tr");
|
const tr = document.createElement("tr");
|
||||||
tr.innerHTML =
|
tr.innerHTML =
|
||||||
`<td class="${d.active ? "active-wan" : ""}">${d.active ? "★ " : ""}${esc(d.device)}</td>` +
|
`<td class="${d.active ? "active-wan" : ""}">${d.active ? "★ " : ""}${esc(d.device)}</td>` +
|
||||||
`<td>${esc(d.type)}</td>` +
|
`<td>${esc(d.type)}</td>` +
|
||||||
`<td>${esc(d.state)}</td>` +
|
`<td><span class="pill ${WAN_STATE_PILL[d.state] || "bad"}">${esc(d.state)}</span></td>` +
|
||||||
`<td>${esc(d.ip || "—")}</td>` +
|
`<td>${esc(d.ip || "—")}</td>` +
|
||||||
`<td>${d.signal || "—"}</td>` +
|
`<td>${d.signal || "—"}</td>` +
|
||||||
`<td>${d.metric != null ? esc(d.metric) : "—"}</td>` +
|
`<td>${d.metric != null ? esc(d.metric) : "—"}</td>` +
|
||||||
@@ -428,8 +554,9 @@ function renderWAN(devs) {
|
|||||||
}
|
}
|
||||||
const tog = document.createElement("button");
|
const tog = document.createElement("button");
|
||||||
tog.className = "btn";
|
tog.className = "btn";
|
||||||
tog.textContent = isUp ? "Down" : "Up";
|
tog.textContent = isConnected ? "Disconnect" : "Connect";
|
||||||
tog.onclick = () => toggleWAN(d, isUp);
|
tog.disabled = !isConnected && noCarrier;
|
||||||
|
tog.onclick = () => toggleWAN(d, isConnected);
|
||||||
acts.appendChild(tog);
|
acts.appendChild(tog);
|
||||||
|
|
||||||
if (d.type === "gsm") {
|
if (d.type === "gsm") {
|
||||||
@@ -456,9 +583,9 @@ async function preferWAN(chosen) {
|
|||||||
setTimeout(refresh, 4500); // daemon enforces on its next probe loop
|
setTimeout(refresh, 4500); // daemon enforces on its next probe loop
|
||||||
}
|
}
|
||||||
|
|
||||||
async function toggleWAN(d, isUp) {
|
async function toggleWAN(d, isConnected) {
|
||||||
try {
|
try {
|
||||||
const verb = isUp ? "disconnect" : "connect";
|
const verb = isConnected ? "disconnect" : "connect";
|
||||||
await run(["nmcli", "device", verb, d.nmdev], { superuser: "require" });
|
await run(["nmcli", "device", verb, d.nmdev], { superuser: "require" });
|
||||||
} catch (e) { window.alert("Toggle failed: " + e.message); }
|
} catch (e) { window.alert("Toggle failed: " + e.message); }
|
||||||
refresh();
|
refresh();
|
||||||
@@ -492,7 +619,9 @@ async function restartAP(unit) {
|
|||||||
async function refresh() {
|
async function refresh() {
|
||||||
try {
|
try {
|
||||||
const secs = parseSections(await sh(STATUS_SCRIPT));
|
const secs = parseSections(await sh(STATUS_SCRIPT));
|
||||||
|
renderDeployWarnings(parseJSON(secs.deploywarn, null));
|
||||||
const aps = APS.map((a, i) => parseAP(a, i, secs));
|
const aps = APS.map((a, i) => parseAP(a, i, secs));
|
||||||
|
ownAPSSIDs = new Set(aps.map(a => a.ssid).filter(Boolean));
|
||||||
renderAPs(aps);
|
renderAPs(aps);
|
||||||
renderClients(collectClients(aps, secs, parseClientDir(secs)));
|
renderClients(collectClients(aps, secs, parseClientDir(secs)));
|
||||||
renderThermal(parseJSON(secs.thermal, null));
|
renderThermal(parseJSON(secs.thermal, null));
|
||||||
|
|||||||
+26
@@ -0,0 +1,26 @@
|
|||||||
|
# deploy.conf — hardware-instance identifiers for this Pi's vanlink deployment.
|
||||||
|
#
|
||||||
|
# These are the values that change when a USB Wi-Fi dongle, LAN adapter, or
|
||||||
|
# modem gets physically swapped (MAC-derived interface names are stable per
|
||||||
|
# physical device, but change when the device changes). Edit here and run
|
||||||
|
# `sudo ./deploy.sh` — it substitutes @TOKEN@ placeholders in the repo's
|
||||||
|
# config templates with these values before installing them.
|
||||||
|
#
|
||||||
|
# Find a new device's interface name after plugging it in: `iw dev` (wifi) or
|
||||||
|
# `ip -br link` (wired). USB vendor ID: `lsusb`.
|
||||||
|
# Preview a rendered file without deploying: `sudo ./deploy.sh render <file>`
|
||||||
|
|
||||||
|
# 5GHz AP radio (currently: Realtek RTL8852BU, driver rtw89_8852bu)
|
||||||
|
WIFI_5G_IFACE=wlxc83a35a4ee55
|
||||||
|
|
||||||
|
# 2.4GHz AP radio (currently: D-Link DWA-171, RTL8821CU, driver rtw88_8821cu)
|
||||||
|
WIFI_2G_IFACE=wlx3c3332002066
|
||||||
|
|
||||||
|
# Wired LAN port, USB-attached gigabit adapter (RTL8153-family)
|
||||||
|
LAN_USB_IFACE=enx00e04c331140
|
||||||
|
|
||||||
|
# Starlink dish uplink, USB-attached gigabit adapter (RTL8153-family)
|
||||||
|
STARLINK_IFACE=enxd8ec5eeb3512
|
||||||
|
|
||||||
|
# Cellular modem USB vendor ID (Quectel EC25-AF)
|
||||||
|
MODEM_USB_VENDOR=2c7c
|
||||||
@@ -1,49 +1,98 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
# Deploy vanlink configs/scripts (Pi 4 "wan" port) to their system locations.
|
# Deploy vanlink configs/scripts (Pi 4 "wan" port) to their system locations.
|
||||||
# Usage: cd ~/vanlink && sudo ./deploy.sh
|
# Usage: cd ~/vanlink && sudo ./deploy.sh
|
||||||
|
# sudo ./deploy.sh render <file> # preview a templated file on stdout
|
||||||
# Idempotent. Netplan (wlan0 + Starlink NIC = NM-managed WANs; eth0 is a LAN
|
# Idempotent. Netplan (wlan0 + Starlink NIC = NM-managed WANs; eth0 is a LAN
|
||||||
# port on br0) is NOT deployed here — reference copy in ap/50-van-wan.yaml,
|
# port on br0) is NOT deployed here — reference copy in ap/50-van-wan.yaml,
|
||||||
# applied once manually (apply flaps uplinks). We do check it for drift below.
|
# applied once manually (apply flaps uplinks). We do check it for drift below.
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
cd "$(dirname "$(readlink -f "$0")")"
|
cd "$(dirname "$(readlink -f "$0")")"
|
||||||
|
|
||||||
|
# deploy.conf holds hardware-instance identifiers (interface names, USB vendor
|
||||||
|
# IDs) that change when a dongle/adapter gets physically swapped. Config files
|
||||||
|
# below carry @TOKEN@ placeholders substituted from these variables via
|
||||||
|
# render()/install_rendered() — edit deploy.conf, not the individual configs.
|
||||||
|
set -a
|
||||||
|
source ./deploy.conf
|
||||||
|
set +a
|
||||||
|
|
||||||
|
render() { # render <file> -> stdout, with @TOKEN@ placeholders substituted
|
||||||
|
sed -e "s|@WIFI_5G_IFACE@|$WIFI_5G_IFACE|g" \
|
||||||
|
-e "s|@WIFI_2G_IFACE@|$WIFI_2G_IFACE|g" \
|
||||||
|
-e "s|@LAN_USB_IFACE@|$LAN_USB_IFACE|g" \
|
||||||
|
-e "s|@STARLINK_IFACE@|$STARLINK_IFACE|g" \
|
||||||
|
-e "s|@MODEM_USB_VENDOR@|$MODEM_USB_VENDOR|g" \
|
||||||
|
"$1"
|
||||||
|
}
|
||||||
|
install_rendered() { # install_rendered <src> <dst> [mode]
|
||||||
|
local tmp
|
||||||
|
tmp=$(mktemp)
|
||||||
|
render "$1" > "$tmp"
|
||||||
|
install -D -m"${3:-0644}" "$tmp" "$2"
|
||||||
|
rm -f "$tmp"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${1:-}" = "render" ]; then
|
||||||
|
[ -n "${2:-}" ] || { echo "Usage: $0 render <file>"; exit 1; }
|
||||||
|
render "$2"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
[ "$(id -u)" = 0 ] || { echo "Run with sudo (writes to /etc, /usr)."; exit 1; }
|
[ "$(id -u)" = 0 ] || { echo "Run with sudo (writes to /etc, /usr)."; exit 1; }
|
||||||
|
|
||||||
|
# Collected below and persisted to WARNINGS_FILE so the Cockpit vanrouter page
|
||||||
|
# can surface deploy-time issues (missing deps, unedited example configs, drift)
|
||||||
|
# without someone having to remember to scroll back through deploy output.
|
||||||
|
WARNINGS=()
|
||||||
|
WARNINGS_FILE=/var/lib/vanlink/deploy-warnings.json
|
||||||
|
warn() { # warn <message...> — prints " -> <message>" (as before) and records it
|
||||||
|
echo " -> $*"
|
||||||
|
WARNINGS+=("$*")
|
||||||
|
}
|
||||||
|
write_warnings() {
|
||||||
|
install -d -m0755 "$(dirname "$WARNINGS_FILE")"
|
||||||
|
{
|
||||||
|
printf '{\n "deployed": "%s",\n "warnings": [' "$(date -Iseconds)"
|
||||||
|
local first=1 w e
|
||||||
|
for w in "${WARNINGS[@]}"; do
|
||||||
|
[ "$first" = 1 ] || printf ','
|
||||||
|
first=0
|
||||||
|
e=${w//\\/\\\\}; e=${e//\"/\\\"}
|
||||||
|
printf '\n "%s"' "$e"
|
||||||
|
done
|
||||||
|
printf '\n ]\n}\n'
|
||||||
|
} > "$WARNINGS_FILE"
|
||||||
|
}
|
||||||
|
|
||||||
echo "== netplan drift check =="
|
echo "== netplan drift check =="
|
||||||
# Netplan is never installed by this script (applying it flaps the uplinks —
|
# Netplan is never installed by this script (applying it flaps the uplinks —
|
||||||
# see note below), so it's easy to edit ap/50-van-wan.yaml and forget the
|
# see note below), so it's easy to edit ap/50-van-wan.yaml and forget the
|
||||||
# manual `netplan apply` step. Warn loudly rather than silently drifting.
|
# manual `netplan apply` step. Warn loudly rather than silently drifting.
|
||||||
if [ ! -f /etc/netplan/50-van-wan.yaml ]; then
|
if [ ! -f /etc/netplan/50-van-wan.yaml ]; then
|
||||||
echo " -> WARNING: /etc/netplan/50-van-wan.yaml is missing. Repo config"
|
warn "netplan: /etc/netplan/50-van-wan.yaml is missing — repo config was never deployed. Run: sudo ./deploy.sh render ap/50-van-wan.yaml | sudo tee /etc/netplan/50-van-wan.yaml && sudo netplan generate && sudo netplan apply"
|
||||||
echo " was never deployed to this system. Run manually:"
|
elif ! diff -q <(render ap/50-van-wan.yaml) /etc/netplan/50-van-wan.yaml >/dev/null 2>&1; then
|
||||||
echo " sudo cp ap/50-van-wan.yaml /etc/netplan/50-van-wan.yaml"
|
warn "netplan: /etc/netplan/50-van-wan.yaml differs from ap/50-van-wan.yaml (rendered). Deploy manually: sudo ./deploy.sh render ap/50-van-wan.yaml | sudo tee /etc/netplan/50-van-wan.yaml && sudo netplan generate && sudo netplan apply"
|
||||||
echo " sudo netplan generate && sudo netplan apply"
|
diff -u /etc/netplan/50-van-wan.yaml <(render ap/50-van-wan.yaml) || true
|
||||||
elif ! diff -q ap/50-van-wan.yaml /etc/netplan/50-van-wan.yaml >/dev/null 2>&1; then
|
|
||||||
echo " -> WARNING: /etc/netplan/50-van-wan.yaml differs from ap/50-van-wan.yaml."
|
|
||||||
diff -u /etc/netplan/50-van-wan.yaml ap/50-van-wan.yaml || true
|
|
||||||
echo " Deploy the change manually:"
|
|
||||||
echo " sudo cp ap/50-van-wan.yaml /etc/netplan/50-van-wan.yaml"
|
|
||||||
echo " sudo netplan generate && sudo netplan apply"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== access point =="
|
echo "== access point =="
|
||||||
install -D -m0644 ap/hostapd.conf /etc/hostapd/hostapd.conf
|
install_rendered ap/hostapd.conf /etc/hostapd/hostapd.conf
|
||||||
install -D -m0644 ap/hostapd-restart.conf /etc/systemd/system/hostapd.service.d/restart.conf
|
install -D -m0644 ap/hostapd-restart.conf /etc/systemd/system/hostapd.service.d/restart.conf
|
||||||
install -D -m0644 ap/default-hostapd /etc/default/hostapd
|
install -D -m0644 ap/default-hostapd /etc/default/hostapd
|
||||||
install -D -m0644 ap/hostapd-2g.conf /etc/hostapd/hostapd-2g.conf
|
install_rendered ap/hostapd-2g.conf /etc/hostapd/hostapd-2g.conf
|
||||||
install -D -m0644 ap/hostapd-2g.service /etc/systemd/system/hostapd-2g.service
|
install_rendered ap/hostapd-2g.service /etc/systemd/system/hostapd-2g.service
|
||||||
install -D -m0644 ap/11-van-ap-2g.network /etc/systemd/network/11-van-ap-2g.network
|
install_rendered ap/11-van-ap-2g.network /etc/systemd/network/11-van-ap-2g.network
|
||||||
install -D -m0644 ap/van-ap-watchdog-2g.service /etc/systemd/system/van-ap-watchdog-2g.service
|
install -D -m0644 ap/van-ap-watchdog-2g.service /etc/systemd/system/van-ap-watchdog-2g.service
|
||||||
install -D -m0644 ap/rtw88.conf /etc/modprobe.d/rtw88.conf
|
|
||||||
install -D -m0755 ap/van-ap-watchdog /usr/local/sbin/van-ap-watchdog
|
install -D -m0755 ap/van-ap-watchdog /usr/local/sbin/van-ap-watchdog
|
||||||
install -D -m0644 ap/van-ap-watchdog.service /etc/systemd/system/van-ap-watchdog.service
|
install -D -m0644 ap/van-ap-watchdog.service /etc/systemd/system/van-ap-watchdog.service
|
||||||
install -D -m0644 ap/van-ap-dnsmasq.conf /etc/van-ap/dnsmasq.conf
|
install -D -m0644 ap/van-ap-dnsmasq.conf /etc/van-ap/dnsmasq.conf
|
||||||
install -D -m0644 ap/van-ap-dnsmasq.service /etc/systemd/system/van-ap-dnsmasq.service
|
install -D -m0644 ap/van-ap-dnsmasq.service /etc/systemd/system/van-ap-dnsmasq.service
|
||||||
install -D -m0644 ap/10-van-ap.network /etc/systemd/network/10-van-ap.network
|
install_rendered ap/10-van-ap.network /etc/systemd/network/10-van-ap.network
|
||||||
install -D -m0644 ap/20-van-br0.netdev /etc/systemd/network/20-van-br0.netdev
|
install -D -m0644 ap/20-van-br0.netdev /etc/systemd/network/20-van-br0.netdev
|
||||||
install -D -m0644 ap/21-van-br0.network /etc/systemd/network/21-van-br0.network
|
install -D -m0644 ap/21-van-br0.network /etc/systemd/network/21-van-br0.network
|
||||||
install -D -m0644 ap/22-van-lan.network /etc/systemd/network/22-van-lan.network
|
install_rendered ap/22-van-lan.network /etc/systemd/network/22-van-lan.network
|
||||||
install -D -m0644 ap/23-van-lan-eth0.network /etc/systemd/network/23-van-lan-eth0.network
|
install -D -m0644 ap/23-van-lan-eth0.network /etc/systemd/network/23-van-lan-eth0.network
|
||||||
install -D -m0644 ap/van-ap-unmanaged.conf /etc/NetworkManager/conf.d/van-ap-unmanaged.conf
|
install_rendered ap/van-ap-unmanaged.conf /etc/NetworkManager/conf.d/van-ap-unmanaged.conf
|
||||||
# Mask dracut's initramfs-generated catch-all (/run/systemd/network/
|
# Mask dracut's initramfs-generated catch-all (/run/systemd/network/
|
||||||
# zzzz-dracut-default.network, regenerated every boot): it matches every
|
# zzzz-dracut-default.network, regenerated every boot): it matches every
|
||||||
# unconfigured link, so networkd co-managed the NM-owned WANs — a second
|
# unconfigured link, so networkd co-managed the NM-owned WANs — a second
|
||||||
@@ -61,15 +110,34 @@ install -D -m0644 failover/config.json /etc/van-failover/config.json
|
|||||||
install -D -m0644 failover/van-failover.service /etc/systemd/system/van-failover.service
|
install -D -m0644 failover/van-failover.service /etc/systemd/system/van-failover.service
|
||||||
install -D -m0755 failover/50-disable-eee /etc/NetworkManager/dispatcher.d/50-disable-eee
|
install -D -m0755 failover/50-disable-eee /etc/NetworkManager/dispatcher.d/50-disable-eee
|
||||||
install -D -m0644 failover/99-van-arp.conf /etc/sysctl.d/99-van-arp.conf
|
install -D -m0644 failover/99-van-arp.conf /etc/sysctl.d/99-van-arp.conf
|
||||||
|
# Backstop for wlan0's post-boot NM no-secrets wedge (see the script's docstring).
|
||||||
|
install -D -m0755 failover/van-wlan-watchdog /usr/local/sbin/van-wlan-watchdog
|
||||||
|
install -D -m0644 failover/van-wlan-watchdog.service /etc/systemd/system/van-wlan-watchdog.service
|
||||||
|
|
||||||
|
echo "== cellular modem (GSM/LTE) =="
|
||||||
|
# NM's gsm.auto-config APN lookup needs this apt-only carrier database; without
|
||||||
|
# it even the right APN can't be auto-detected, and MVNOs (e.g. Koodo, which
|
||||||
|
# isn't listed under its own name — only under host network "Telus Mobility")
|
||||||
|
# often aren't in it anyway, so the modem's NM connection profile may still
|
||||||
|
# need an explicit gsm.apn set by hand regardless.
|
||||||
|
dpkg -s mobile-broadband-provider-info >/dev/null 2>&1 \
|
||||||
|
|| warn "mobile-broadband-provider-info missing (apt install mobile-broadband-provider-info) — GSM APN auto-config will fail"
|
||||||
|
|
||||||
echo "== cockpit plugin =="
|
echo "== cockpit plugin =="
|
||||||
install -d /usr/share/cockpit/vanrouter
|
install -d /usr/share/cockpit/vanrouter
|
||||||
install -m0644 cockpit/vanrouter/* /usr/share/cockpit/vanrouter/
|
for f in cockpit/vanrouter/*; do
|
||||||
|
b=$(basename "$f")
|
||||||
|
if [ "$b" = "vanrouter.js" ]; then
|
||||||
|
install_rendered "$f" "/usr/share/cockpit/vanrouter/$b"
|
||||||
|
else
|
||||||
|
install -m0644 "$f" "/usr/share/cockpit/vanrouter/$b"
|
||||||
|
fi
|
||||||
|
done
|
||||||
# Bridge fd headroom (Python bridge frees spawn pipes only at GC; 1024 is too tight)
|
# Bridge fd headroom (Python bridge frees spawn pipes only at GC; 1024 is too tight)
|
||||||
install -D -m0644 cockpit/cockpit-session-nofile.conf /etc/systemd/system/cockpit-session@.service.d/nofile.conf
|
install -D -m0644 cockpit/cockpit-session-nofile.conf /etc/systemd/system/cockpit-session@.service.d/nofile.conf
|
||||||
# The Starlink card queries the dish's gRPC API; grpcurl isn't packaged in apt.
|
# The Starlink card queries the dish's gRPC API; grpcurl isn't packaged in apt.
|
||||||
command -v grpcurl >/dev/null 2>&1 \
|
command -v grpcurl >/dev/null 2>&1 \
|
||||||
|| echo " -> grpcurl missing (Starlink card will say so): install linux_arm64 binary from github.com/fullstorydev/grpcurl/releases"
|
|| warn "grpcurl missing (Starlink card will say so): install linux_arm64 binary from github.com/fullstorydev/grpcurl/releases"
|
||||||
|
|
||||||
echo "== gps (cellular modem GNSS -> gpsd) =="
|
echo "== gps (cellular modem GNSS -> gpsd) =="
|
||||||
if dpkg -s gpsd >/dev/null 2>&1; then
|
if dpkg -s gpsd >/dev/null 2>&1; then
|
||||||
@@ -77,7 +145,7 @@ if dpkg -s gpsd >/dev/null 2>&1; then
|
|||||||
install -D -m0644 gps/gpsd.default /etc/default/gpsd
|
install -D -m0644 gps/gpsd.default /etc/default/gpsd
|
||||||
udevadm control --reload
|
udevadm control --reload
|
||||||
else
|
else
|
||||||
echo " -> gpsd not installed (apt install gpsd gpsd-clients) — skipping GPS setup"
|
warn "gpsd not installed (apt install gpsd gpsd-clients) — skipping GPS setup"
|
||||||
fi
|
fi
|
||||||
# OwnTracks publisher: gpsd fix -> MQTT (broker + creds live only on the
|
# OwnTracks publisher: gpsd fix -> MQTT (broker + creds live only on the
|
||||||
# system, 0600 — same pattern as pushover.json).
|
# system, 0600 — same pattern as pushover.json).
|
||||||
@@ -85,10 +153,12 @@ install -D -m0755 gps/van-gps-owntracks /usr/local/sbin/van-gps-owntrack
|
|||||||
install -D -m0644 gps/van-gps-owntracks.service /etc/systemd/system/van-gps-owntracks.service
|
install -D -m0644 gps/van-gps-owntracks.service /etc/systemd/system/van-gps-owntracks.service
|
||||||
if [ ! -f /etc/van-gps/config.json ]; then
|
if [ ! -f /etc/van-gps/config.json ]; then
|
||||||
install -D -m0600 gps/config.json.example /etc/van-gps/config.json
|
install -D -m0600 gps/config.json.example /etc/van-gps/config.json
|
||||||
echo " -> seeded /etc/van-gps/config.json (EDIT IT: add MQTT username + password)"
|
warn "seeded /etc/van-gps/config.json (EDIT IT: add MQTT username + password)"
|
||||||
fi
|
fi
|
||||||
|
python3 -c 'import gps' 2>/dev/null \
|
||||||
|
|| warn "python3-gps missing (apt install python3-gps) — van-gps-owntracks won't start"
|
||||||
python3 -c 'import paho.mqtt' 2>/dev/null \
|
python3 -c 'import paho.mqtt' 2>/dev/null \
|
||||||
|| echo " -> python3-paho-mqtt missing (apt install python3-paho-mqtt) — van-gps-owntracks won't start"
|
|| warn "python3-paho-mqtt missing (apt install python3-paho-mqtt) — van-gps-owntracks won't start"
|
||||||
|
|
||||||
echo "== thermal monitor =="
|
echo "== thermal monitor =="
|
||||||
install -D -m0755 power/van-thermal /usr/local/sbin/van-thermal
|
install -D -m0755 power/van-thermal /usr/local/sbin/van-thermal
|
||||||
@@ -98,7 +168,7 @@ install -D -m0644 power/van-thermal.service /etc/systemd/system/van-thermal.serv
|
|||||||
# under /etc/van-battery/ for parity with wayback's van-thermal default.
|
# under /etc/van-battery/ for parity with wayback's van-thermal default.
|
||||||
if [ ! -f /etc/van-battery/pushover.json ]; then
|
if [ ! -f /etc/van-battery/pushover.json ]; then
|
||||||
install -D -m0600 power/pushover.json.example /etc/van-battery/pushover.json
|
install -D -m0600 power/pushover.json.example /etc/van-battery/pushover.json
|
||||||
echo " -> seeded /etc/van-battery/pushover.json (EDIT IT: add Pushover token + user)"
|
warn "seeded /etc/van-battery/pushover.json (EDIT IT: add Pushover token + user)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "== home assistant =="
|
echo "== home assistant =="
|
||||||
@@ -121,10 +191,10 @@ systemctl daemon-reexec
|
|||||||
# its wait-online would just stall network-online.target. NM-wait-online covers WANs.
|
# its wait-online would just stall network-online.target. NM-wait-online covers WANs.
|
||||||
systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
|
systemctl mask systemd-networkd-wait-online.service >/dev/null 2>&1 || true
|
||||||
systemctl unmask hostapd >/dev/null 2>&1 || true
|
systemctl unmask hostapd >/dev/null 2>&1 || true
|
||||||
systemctl enable regdomain.service hostapd hostapd-2g van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-ap-watchdog van-ap-watchdog-2g van-gps-owntracks >/dev/null 2>&1 || true
|
systemctl enable regdomain.service hostapd hostapd-2g van-ap-dnsmasq nftables systemd-networkd van-failover van-thermal van-ap-watchdog van-ap-watchdog-2g van-wlan-watchdog van-gps-owntracks >/dev/null 2>&1 || true
|
||||||
# bluetooth: host BlueZ serves the onboard hci0 to the HA container over D-Bus
|
# bluetooth: host BlueZ serves the onboard hci0 to the HA container over D-Bus
|
||||||
systemctl enable --now bluetooth >/dev/null 2>&1 || true
|
systemctl enable --now bluetooth >/dev/null 2>&1 || true
|
||||||
systemctl start homeassistant || echo " -> homeassistant failed to start (podman/quadlet — check journalctl -u homeassistant)"
|
systemctl start homeassistant || warn "homeassistant failed to start (podman/quadlet — check journalctl -u homeassistant)"
|
||||||
systemctl restart van-thermal
|
systemctl restart van-thermal
|
||||||
systemctl restart van-gps-owntracks
|
systemctl restart van-gps-owntracks
|
||||||
# Pick up unmanaged-devices changes so NM releases/keeps the right interfaces.
|
# Pick up unmanaged-devices changes so NM releases/keeps the right interfaces.
|
||||||
@@ -132,7 +202,7 @@ nmcli general reload 2>/dev/null || systemctl reload NetworkManager 2>/dev/null
|
|||||||
# restart in dependency order: bridge + members first, then hostapd enslaves the
|
# restart in dependency order: bridge + members first, then hostapd enslaves the
|
||||||
# radios, then dnsmasq binds br0, then NAT/failover
|
# radios, then dnsmasq binds br0, then NAT/failover
|
||||||
systemctl restart systemd-networkd
|
systemctl restart systemd-networkd
|
||||||
systemctl restart van-ap-dnsmasq nftables van-failover
|
systemctl restart van-ap-dnsmasq nftables van-failover van-wlan-watchdog
|
||||||
# The AP radios live on the USB hub and may be absent; the start then fails but
|
# The AP radios live on the USB hub and may be absent; the start then fails but
|
||||||
# Restart=always keeps retrying and claims them the moment they enumerate.
|
# Restart=always keeps retrying and claims them the moment they enumerate.
|
||||||
systemctl restart hostapd hostapd-2g \
|
systemctl restart hostapd hostapd-2g \
|
||||||
@@ -141,11 +211,13 @@ systemctl restart hostapd hostapd-2g \
|
|||||||
systemctl restart van-ap-watchdog van-ap-watchdog-2g
|
systemctl restart van-ap-watchdog van-ap-watchdog-2g
|
||||||
networkctl reload 2>/dev/null || true
|
networkctl reload 2>/dev/null || true
|
||||||
|
|
||||||
|
write_warnings
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "Deployed. NOTE: until the USB hub (AP radios + LAN/Starlink adapters) is"
|
echo "Deployed. NOTE: until the USB hub (AP radios + LAN/Starlink adapters) is"
|
||||||
echo "plugged in, hostapd/hostapd-2g just retry every 5s — that is by design."
|
echo "plugged in, hostapd/hostapd-2g just retry every 5s — that is by design."
|
||||||
echo "Verify (with hub present):"
|
echo "Verify (with hub present):"
|
||||||
echo " iw dev wlxc83a35a4ee55 info | grep -E 'ssid|channel|width'"
|
echo " iw dev $WIFI_5G_IFACE info | grep -E 'ssid|channel|width'"
|
||||||
echo " iw dev wlxd8ec5e2faa8c info | grep -E 'ssid|channel|width'"
|
echo " iw dev $WIFI_2G_IFACE info | grep -E 'ssid|channel|width'"
|
||||||
echo " cat /run/van-failover/state.json"
|
echo " cat /run/van-failover/state.json"
|
||||||
echo " cat /run/van-thermal/state.json"
|
echo " cat /run/van-thermal/state.json"
|
||||||
|
|||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""van-wlan-watchdog — recover wlan0 from NetworkManager's post-boot no-secrets wedge.
|
||||||
|
|
||||||
|
Seen on 2026-07-30: wlan0 (onboard radio, used as the WiFi WAN) sometimes fails its
|
||||||
|
very first post-boot association attempt with a spurious supplicant "psk mismatch"
|
||||||
|
(a boot-time brcmfmac firmware/regulatory race, not a real credential problem — a
|
||||||
|
second attempt with the *same* stored secret succeeds immediately). NetworkManager
|
||||||
|
treats any handshake failure it reads as bad secrets as terminal: it does not retry
|
||||||
|
autoconnect after a no-secrets failure, so the device just sits in `disconnected`
|
||||||
|
until something explicitly re-triggers it. This is that trigger.
|
||||||
|
|
||||||
|
Deliberately does NOT hardcode a connection/SSID name: which WiFi network wlan0 uses
|
||||||
|
changes with wherever the van is parked (home, a campsite, a neighbour's AP — see
|
||||||
|
config profiles managed from Cockpit's Networking tab or the Wi-Fi selector on the Van
|
||||||
|
Router page). `nmcli device connect wlan0` lets NM pick amongst whatever profiles are
|
||||||
|
saved and in range on its own, exactly like its normal autoconnect would.
|
||||||
|
|
||||||
|
Bounded to a few minutes after boot, then exits — this is a backstop for the boot race
|
||||||
|
above, not a permanent watcher. It must NOT fight a deliberate later disconnect (e.g.
|
||||||
|
the Cockpit "Disconnect" button), which is why it doesn't loop forever.
|
||||||
|
|
||||||
|
Usage: van-wlan-watchdog [device]
|
||||||
|
Defaults to wlan0 (the AP radios are separate USB dongles, watched by
|
||||||
|
van-ap-watchdog instead).
|
||||||
|
"""
|
||||||
|
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
DEVICE = sys.argv[1] if len(sys.argv) > 1 else "wlan0"
|
||||||
|
INTERVAL = 15 # seconds between checks
|
||||||
|
BUDGET_S = 8 * INTERVAL # ~2 minutes of retries after boot, then give up quietly
|
||||||
|
|
||||||
|
|
||||||
|
def log(msg, level="info"):
|
||||||
|
# systemd journal severity prefixes (sd-daemon), same convention as van-ap-watchdog.
|
||||||
|
pri = {"info": "<6>", "warn": "<4>", "crit": "<2>"}.get(level, "<6>")
|
||||||
|
print(pri + msg, flush=True)
|
||||||
|
|
||||||
|
|
||||||
|
def device_state(dev):
|
||||||
|
"""NM's state word for dev (e.g. 'connected', 'disconnected', 'unavailable'), or
|
||||||
|
None if nmcli failed or the device isn't known to NM yet."""
|
||||||
|
try:
|
||||||
|
out = subprocess.run(
|
||||||
|
["nmcli", "-t", "-f", "GENERAL.STATE", "device", "show", dev],
|
||||||
|
capture_output=True, text=True, timeout=10,
|
||||||
|
).stdout.strip()
|
||||||
|
except (OSError, subprocess.SubprocessError) as e:
|
||||||
|
log(f"nmcli device show {dev} failed: {e}", "warn")
|
||||||
|
return None
|
||||||
|
# terse output looks like "GENERAL.STATE:30 (disconnected)"
|
||||||
|
if "(" in out and out.endswith(")"):
|
||||||
|
return out.rsplit("(", 1)[1][:-1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def connect(dev):
|
||||||
|
log(f"{dev} is disconnected — nudging NM to reconnect (autoconnect doesn't retry "
|
||||||
|
f"after a no-secrets failure, only manual/dispatcher re-activation does)", "warn")
|
||||||
|
r = subprocess.run(["nmcli", "device", "connect", dev],
|
||||||
|
capture_output=True, text=True, timeout=45)
|
||||||
|
if r.returncode == 0:
|
||||||
|
log(f"{dev} reconnected")
|
||||||
|
else:
|
||||||
|
log(f"nmcli device connect {dev} returned {r.returncode}: {r.stderr.strip()}", "warn")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
log(f"van-wlan-watchdog up: watching {DEVICE} for up to {BUDGET_S}s after boot")
|
||||||
|
elapsed = 0
|
||||||
|
while elapsed < BUDGET_S:
|
||||||
|
state = device_state(DEVICE)
|
||||||
|
if state == "disconnected":
|
||||||
|
connect(DEVICE)
|
||||||
|
elif state == "connected":
|
||||||
|
log(f"{DEVICE} connected — exiting")
|
||||||
|
return
|
||||||
|
# "unavailable" (no known/in-range AP), "unmanaged", or unreadable: nothing to
|
||||||
|
# nudge — retrying nmcli device connect would just fail again.
|
||||||
|
time.sleep(INTERVAL)
|
||||||
|
elapsed += INTERVAL
|
||||||
|
log(f"{DEVICE} still not connected after {BUDGET_S}s — giving up "
|
||||||
|
f"(leaving it for manual/Cockpit reconnect from here on)", "warn")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
sys.exit(0)
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=wlan0 boot-reconnect backstop (NM no-secrets wedge after a boot-time supplicant glitch)
|
||||||
|
After=NetworkManager.service
|
||||||
|
Wants=NetworkManager.service
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
ExecStart=/usr/local/sbin/van-wlan-watchdog
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
Reference in New Issue
Block a user