starlink: dish route + status card on the Van Router page
The Starlink RTL8153 gets a declarative NM profile in 50-van-wan.yaml (replaces the auto 'Wired connection 1') with a 192.168.100.1/32 link route, so the dish's management address stays reachable from the router and — via the existing !br0 masquerade — from the van LAN, regardless of which WAN holds the default route. New Cockpit "Starlink" card queries the dish's gRPC API (:9200, get_status via grpcurl — not packaged in apt, deploy.sh warns when missing) inside the existing single batched spawn: online/obstructed/outage pill, alerts, uptime, sw version, PoP latency, down/up throughput, obstruction %. Sentinels distinguish adapter-absent / no-grpcurl / dish-unreachable. Gotcha captured while cutting over: a WAN profile without ipv4.route-metric makes NM re-assert its DHCP default (metric ~101) against van-failover's enforce_route pruning every cycle; the profile metric must match (failover's set_profile_metric does this on health transitions). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
2da97d26f4
commit
751facd511
@@ -35,6 +35,11 @@
|
||||
<div id="failover"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>Starlink</h3>
|
||||
<div id="starlink"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h3>WAN / Uplinks</h3>
|
||||
<table>
|
||||
|
||||
@@ -12,6 +12,9 @@ const APS = [
|
||||
{ iface: "wlxd8ec5e2faa8c", unit: "hostapd-2g", band: "2.4GHz" }, // RTL8822BU
|
||||
];
|
||||
const PREFER_FILE = "/run/van-failover/prefer"; // van-failover reads this to pick the preferred WAN
|
||||
// Starlink dish: gRPC status API on the fixed management IP (reached via the /32
|
||||
// link route the netplan profile installs on the RTL8153 uplink).
|
||||
const STARLINK = { iface: "enxd8ec5eeb3512", dish: "192.168.100.1:9200" };
|
||||
|
||||
function run(args, opts) {
|
||||
return cockpit.spawn(args, Object.assign({ err: "message" }, opts || {}));
|
||||
@@ -43,6 +46,13 @@ const STATUS_SCRIPT = (() => {
|
||||
add("neigh", "ip -j neigh show dev br0");
|
||||
add("leases", "cat /var/lib/misc/dnsmasq.leases");
|
||||
add("thermal", "cat /run/van-thermal/state.json");
|
||||
// Sentinel words (ABSENT/NOGRPCURL/UNREACHABLE) let the renderer tell the
|
||||
// three failure modes apart; anything starting with '{' is dish status JSON.
|
||||
add("starlink",
|
||||
`if ! ip link show ${STARLINK.iface} >/dev/null 2>&1; then echo ABSENT; ` +
|
||||
`elif ! command -v grpcurl >/dev/null 2>&1; then echo NOGRPCURL; ` +
|
||||
`else timeout 4 grpcurl -plaintext -max-time 3 -d '{"get_status":{}}' ` +
|
||||
`${STARLINK.dish} SpaceX.API.Device.Device/Handle || echo UNREACHABLE; fi`);
|
||||
add("battery", "cat /run/van-battery/state.json");
|
||||
add("failover", "cat /run/van-failover/state.json");
|
||||
add("devices", "nmcli -t -f DEVICE,TYPE,STATE,CONNECTION device status");
|
||||
@@ -169,6 +179,64 @@ function renderThermal(th) {
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ---------- Starlink (dish gRPC get_status via grpcurl) ---------- */
|
||||
|
||||
function fmtMbps(bps) {
|
||||
return bps == null ? "—" : (bps / 1e6).toFixed(1) + " Mbps";
|
||||
}
|
||||
|
||||
function fmtUptime(s) {
|
||||
s = parseInt(s, 10);
|
||||
if (isNaN(s)) return "—";
|
||||
const d = Math.floor(s / 86400), h = Math.floor(s % 86400 / 3600), m = Math.floor(s % 3600 / 60);
|
||||
return (d ? `${d}d ` : "") + (d || h ? `${h}h ` : "") + `${m}m`;
|
||||
}
|
||||
|
||||
function renderStarlink(raw) {
|
||||
const el = document.getElementById("starlink");
|
||||
const t = (raw || "").trim();
|
||||
if (!t || t === "ABSENT") {
|
||||
el.innerHTML = `<p class="muted">Starlink adapter (<code>${esc(STARLINK.iface)}</code>) not plugged in.</p>`;
|
||||
return;
|
||||
}
|
||||
if (t === "NOGRPCURL") {
|
||||
el.innerHTML = `<p class="muted">grpcurl not installed — the dish status API is gRPC. ` +
|
||||
`arm64 binary: <code>github.com/fullstorydev/grpcurl/releases</code></p>`;
|
||||
return;
|
||||
}
|
||||
if (t === "UNREACHABLE" || t[0] !== "{") {
|
||||
el.innerHTML = `<p><span class="pill bad">dish unreachable</span> ` +
|
||||
`<span class="muted">adapter present but 192.168.100.1 not answering ` +
|
||||
`(dish booting / unpowered / route missing?)</span></p>`;
|
||||
return;
|
||||
}
|
||||
|
||||
const st = (parseJSON(t, {}) || {}).dishGetStatus || {};
|
||||
const obs = st.obstructionStats || {};
|
||||
const alerts = Object.keys(st.alerts || {}).filter(k => st.alerts[k]);
|
||||
|
||||
let state;
|
||||
if (st.outage)
|
||||
state = `<span class="pill bad">${esc(st.outage.cause || "OUTAGE")}</span>`;
|
||||
else if (obs.currentlyObstructed)
|
||||
state = `<span class="pill warn">obstructed</span>`;
|
||||
else
|
||||
state = `<span class="pill ok">online</span>`;
|
||||
|
||||
let html = `<p>${state}` +
|
||||
(alerts.length ? ` <span class="pill warn">alerts: ${esc(alerts.join(", "))}</span>` : "") +
|
||||
` <span class="muted">uptime ${esc(fmtUptime((st.deviceState || {}).uptimeS))}` +
|
||||
` · sw ${esc((st.deviceInfo || {}).softwareVersion || "—")}</span></p>`;
|
||||
html += `<table><thead><tr><th>Latency (PoP)</th><th>Down</th><th>Up</th><th>Obstructed</th></tr></thead><tbody>`;
|
||||
html += `<tr><td>${st.popPingLatencyMs == null || st.popPingLatencyMs < 0 ? "—" : esc(st.popPingLatencyMs.toFixed(0)) + " ms"}</td>` +
|
||||
`<td>${esc(fmtMbps(st.downlinkThroughputBps))}</td>` +
|
||||
`<td>${esc(fmtMbps(st.uplinkThroughputBps))}</td>` +
|
||||
`<td>${obs.fractionObstructed == null ? "—" : esc((obs.fractionObstructed * 100).toFixed(1)) + " %"}</td></tr>`;
|
||||
html += `</tbody></table>`;
|
||||
html += `<p class="muted">Dish web UI: <code>http://192.168.100.1</code> (from the van LAN)</p>`;
|
||||
el.innerHTML = html;
|
||||
}
|
||||
|
||||
/* ---------- Battery / power source (van-battery daemon state) ---------- */
|
||||
|
||||
function renderBattery(b) {
|
||||
@@ -320,6 +388,7 @@ async function refresh() {
|
||||
const secs = parseSections(await sh(STATUS_SCRIPT));
|
||||
renderAPs(APS.map((a, i) => parseAP(a, i, secs)), parseClientDir(secs));
|
||||
renderThermal(parseJSON(secs.thermal, null));
|
||||
renderStarlink(secs.starlink);
|
||||
renderBattery(parseJSON(secs.battery, null));
|
||||
renderFailover(parseJSON(secs.failover, null));
|
||||
renderWAN(parseWAN(secs));
|
||||
|
||||
Reference in New Issue
Block a user