van-leds: drive the R5C power/LAN/WAN/WiFi LEDs (ENABLE_LEDS, LED_* vars, post-check probe)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KqhjP7nud5gdBg25zCiRk9
This commit is contained in:
2026-09-23 22:04:11 -04:00
co-authored by Claude Sonnet 5
parent e2036c295d
commit 0f643be719
9 changed files with 382 additions and 2 deletions
+118
View File
@@ -99,6 +99,7 @@ if awk '
need["van-li3-battery"]="ENABLE_LI3"
need["van-nvme-watch"]="ENABLE_NVME_WATCH"
need["nvme-watch-config"]="ENABLE_NVME_WATCH"
need["van-leds"]="ENABLE_LEDS"
need["homeassistant"]="ENABLE_HA"
need["esphome"]="ENABLE_HA"
need["frigate"]="ENABLE_FRIGATE"
@@ -815,4 +816,121 @@ d=$(./deploy.sh --diff hosts/van.conf 2>&1 || true)
! echo "$d" | grep -q 'credentials.json' && ok "21d: --diff never lists credentials.json" || bad "21d: --diff mentions credentials.json"
rm -rf "$t21"
# 22. van-leds: R5C front LEDs (power / LAN / WAN / WiFi)
if python3 -c 'import ast,sys; ast.parse(open(sys.argv[1]).read())' leds/van-leds; then ok "22a: van-leds parses"; else bad "22a: van-leds does not parse"; fi
[ -x leds/van-leds ] && ok "22a: van-leds is executable" || bad "22a: van-leds not executable"
t22=$(mktemp -d)
o=$(python3 - "$t22" 2>&1 <<'PY'
import importlib.machinery, importlib.util, io, json, os, sys, contextlib
ldr = importlib.machinery.SourceFileLoader("vl", "leds/van-leds")
spec = importlib.util.spec_from_loader("vl", ldr)
m = importlib.util.module_from_spec(spec)
ldr.exec_module(m)
tmp = sys.argv[1]
bad = 0
def check(name, cond):
global bad
print(("ok - " if cond else "FAIL - ") + name)
bad |= (not cond)
def st(active, wans):
return {"updated": "x", "active_device": active, "wans": wans}
H = st("eth1", [{"name": "starlink", "device": "eth1", "healthy": True, "active": True}])
U = st("eth1", [{"name": "starlink", "device": "eth1", "healthy": False, "active": True}])
N = st(None, [{"name": "starlink", "device": "eth1", "healthy": False}])
d = m.desired_wan
check("22b: healthy active -> netdev on it", d(H, True) == ("netdev", "eth1"))
check("22b: healthy active, carrier 0 -> still netdev", d(H, False) == ("netdev", "eth1"))
check("22b: unhealthy active -> heartbeat", d(U, True) == ("heartbeat", None))
check("22b: no active + carrier -> heartbeat", d(N, True) == ("heartbeat", None))
check("22b: no active + no carrier -> none", d(N, False) == ("none", None))
for label, bs in (("missing", None), ("invalid", "garbage"), ("no wans", {"x": 1})):
check(f"22b: {label} state + carrier -> netdev on the uplink", d(bs, True, "up0") == ("netdev", "up0"))
check(f"22b: {label} state + no carrier -> none", d(bs, False, "up0") == ("none", None))
# fake sysfs
leds, net = os.path.join(tmp, "leds"), os.path.join(tmp, "net")
for n in ("red:power", "green:lan", "green:wan", "green:wlan"):
os.makedirs(os.path.join(leds, n))
for f, v in (("trigger", "none"), ("brightness", "0"), ("device_name", ""), ("link", "0"), ("tx", "0"), ("rx", "0")):
open(os.path.join(leds, n, f), "w").write(v + "\n")
for i, c in (("lan0", "1"), ("wl0", "1"), ("up0", "1"), ("eth1", "1")):
os.makedirs(os.path.join(net, i))
open(os.path.join(net, i, "carrier"), "w").write(c + "\n")
sf = os.path.join(tmp, "state.json")
json.dump(H, open(sf, "w"))
cfg = dict(m.DEFAULTS, led_power="red:power", led_lan="green:lan", led_wan="green:wan", led_wlan="green:wlan",
lan_iface="lan0", wlan_iface="wl0", uplink_iface="up0", sysfs_leds=leds, sysfs_net=net, failover_state=sf)
def rd(led, f): return open(os.path.join(leds, led, f)).read().strip()
def poll(c=cfg):
with contextlib.redirect_stdout(io.StringIO()):
return m.poll_once(c)
w = poll()
check("22c: power default-on", rd("red:power", "trigger") == "default-on")
check("22c: lan netdev on lan0, link/tx/rx 1", (rd("green:lan", "trigger"), rd("green:lan", "device_name"), rd("green:lan", "link"), rd("green:lan", "tx"), rd("green:lan", "rx")) == ("netdev", "lan0", "1", "1", "1"))
check("22c: wlan netdev on wl0", (rd("green:wlan", "trigger"), rd("green:wlan", "device_name")) == ("netdev", "wl0"))
check("22c: wan netdev on the active device", (rd("green:wan", "trigger"), rd("green:wan", "device_name")) == ("netdev", "eth1"))
check("22c: first run writes", len(w) > 0)
w = poll()
check("22c: second run makes no writes", w == [])
json.dump(U, open(sf, "w")); w = poll()
check("22c: unhealthy state -> wan heartbeat", rd("green:wan", "trigger") == "heartbeat" and len(w) == 1)
json.dump(N, open(sf, "w")); open(os.path.join(net, "up0", "carrier"), "w").write("0\n"); poll()
check("22c: nothing -> wan none, brightness 0", rd("green:wan", "trigger") == "none" and rd("green:wan", "brightness") == "0")
open(sf, "w").write("{broken"); open(os.path.join(net, "up0", "carrier"), "w").write("1\n"); poll()
check("22c: invalid state falls back to wired uplink", (rd("green:wan", "trigger"), rd("green:wan", "device_name")) == ("netdev", "up0"))
open(os.path.join(leds, "green:lan", "trigger"), "w").write("none\n"); w = poll()
check("22c: a reset trigger is re-asserted", rd("green:lan", "trigger") == "netdev" and any(x.endswith("trigger") for x in w))
c2 = dict(cfg, led_lan="green:nope", led_wlan="")
try:
w = poll(c2); ok_ = True
except Exception as e:
ok_ = False
check("22c: missing / empty LED names are skipped without raising", ok_)
buf = io.StringIO()
with contextlib.redirect_stdout(buf):
m.usable_leds(c2, announce=True)
check("22c: skipped LEDs are logged once at start", buf.getvalue().count("skipped") == 2)
check("22c: apply_led writes only on difference", m.apply_led(os.path.join(leds, "red:power"), "default-on") == [])
print("STATUS", int(bad))
PY
)
echo "$o" | grep -v '^STATUS'
echo "$o" | grep -q '^STATUS 0$' || bad "22b/c: van-leds behavioural tests"
# 22d. config rendering
expect van leds/config.json '"lan_iface": "enP1p17s0"'
expect van leds/config.json '"wlan_iface": "wlp1s0"'
expect van leds/config.json '"uplink_iface": "enP2p33s0"'
expect van leds/config.json '"led_power": "red:power"'
expect van leds/config.json '"led_wan": "green:wan"'
expect van leds/config.json '"led_wlan": "green:wlan"'
expect wan leds/config.json '"led_power": ""'
expect wan leds/config.json '"led_lan": ""'
expect wan leds/config.json '"led_wan": ""'
expect wan leds/config.json '"led_wlan": ""'
r van leds/config.json | python3 -c 'import json,sys; json.load(sys.stdin)' && ok "22d: rendered leds/config.json is valid JSON" || bad "22d: leds/config.json invalid JSON"
# 22e. post_check LED probe against a fake sysfs dir
mkl() { # mkl <dir> <led> <trigger> [device_name]
mkdir -p "$1/$2"; echo "none [$3] heartbeat" > "$1/$2/trigger"; echo "${4:-}" > "$1/$2/device_name"; }
L="$t22/sysleds"; rm -rf "$L"
mkl "$L" red:power default-on; mkl "$L" green:lan netdev enP1p17s0; mkl "$L" green:wlan netdev wlp1s0; mkl "$L" green:wan heartbeat
pl() { VANLINK_SYSLEDS="$1" POSTCHECK_WAIT=0 ./deploy.sh --post-check "${2:-hosts/van.conf}" 2>&1 || true; }
o=$(pl "$L")
{ echo "$o" | grep -qF 'ok LED red:power (power): default-on' && echo "$o" | grep -qF 'ok LED green:lan (lan): netdev enP1p17s0' \
&& echo "$o" | grep -qF 'ok LED green:wlan (wlan): netdev wlp1s0' && echo "$o" | grep -qF 'ok LED green:wan (wan): heartbeat' \
&& ! echo "$o" | grep -qE -- '-> post-deploy: LED'; } && ok "22e: correct LEDs -> ok lines, no warn" || { bad "22e: correct LEDs"; echo "$o" | grep LED | sed 's/^/ /'; }
mkl "$L" green:wan netdev enP2p33s0
o=$(pl "$L"); echo "$o" | grep -qF 'ok LED green:wan (wan): netdev enP2p33s0' && ok "22e: wan LED netdev named" || bad "22e: wan LED netdev not reported"
rm -rf "$L/green:wlan"; o=$(pl "$L")
echo "$o" | grep -qF -- '-> post-deploy: LED green:wlan not present — is the nanopi-r5c-leds overlay enabled in /boot/armbianEnv.txt?' && ok "22e: missing LED -> warn" || bad "22e: missing LED no warn"
mkl "$L" green:wlan netdev wlp1s0; mkl "$L" green:lan netdev wrongdev
o=$(pl "$L"); echo "$o" | grep -qE -- '-> post-deploy: LED green:lan .*wrongdev' && ok "22e: wrong lan device -> warn" || bad "22e: wrong lan device no warn"
mkl "$L" green:lan netdev enP1p17s0; mkl "$L" red:power heartbeat
o=$(pl "$L"); echo "$o" | grep -qE -- '-> post-deploy: LED red:power .*expected default-on' && ok "22e: wrong power trigger -> warn" || bad "22e: wrong power trigger no warn"
o=$(pl "$L" hosts/wan.conf); { ! echo "$o" | grep -q 'LED'; } && ok "22e: ENABLE_LEDS=0 host -> no LED output" || bad "22e: wan.conf produced LED output"
# 22f. flags; nothing LED-related outside the ENABLE_LEDS guard
(. hosts/van.conf; [ "$ENABLE_LEDS" = 1 ]) && ok "22f: van.conf ENABLE_LEDS=1" || bad "22f: van.conf ENABLE_LEDS"
(. hosts/wan.conf; [ "$ENABLE_LEDS" = 0 ] && [ -z "$LED_POWER$LED_LAN$LED_WAN$LED_WLAN" ]) && ok "22f: wan.conf ENABLE_LEDS=0, no LED names" || bad "22f: wan.conf LEDs"
grep -qF 'need["van-leds"]="ENABLE_LEDS"' tests/check-templates.sh && ok "22f: check 6 gates van-leds" || bad "22f: check 6 lacks van-leds"
rm -rf "$t22"
exit "$fail"