ap: parallel 2.4GHz AP (hostapd-2g) on the Linksys WUSB6300v2 dongle

Second radio (wlxd8ec5e2faa8c, RTL8822BU/rtw88), same VanLink SSID+PSK,
ch6 HT20, bridged into br0 next to the 5GHz AP. Runs as its own hostapd
instance + own watchdog so a USB wedge on one radio never darkens the
other; van-ap-watchdog now takes conf path + unit name as args (defaults
unchanged). rtw88.conf disables deep power-save (rtw89 gotcha analog)
and the driver's USB2->3 self-upgrade (USB3 noise in the 2.4GHz band —
note: dongle currently sits in a USB3 port, so it still enumerates
SuperSpeed; move to a USB2 port if 2.4GHz range disappoints).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Andreas Wrede
2026-07-06 10:03:16 -04:00
co-authored by Claude Fable 5
parent a7819edbc2
commit e652b0f7bd
9 changed files with 106 additions and 17 deletions
+10 -5
View File
@@ -18,6 +18,10 @@ exited, so Restart=always never fired. The link check catches exactly that. If t
unhealthy for `fail_threshold` checks in a row, it clears any failed state and restarts
hostapd. If the interface is simply gone (mid re-enumeration) it waits — there is
nothing to restart onto, and Restart=always reclaims it when it reappears. Stdlib only.
Usage: van-ap-watchdog [hostapd.conf path] [systemd unit]
Defaults watch the 5GHz AP (/etc/hostapd/hostapd.conf, unit hostapd); the 2.4GHz
instance (van-ap-watchdog-2g.service) passes hostapd-2g.conf + hostapd-2g.
"""
import re
@@ -26,7 +30,8 @@ import sys
import time
from pathlib import Path
HOSTAPD_CONF = "/etc/hostapd/hostapd.conf"
HOSTAPD_CONF = sys.argv[1] if len(sys.argv) > 1 else "/etc/hostapd/hostapd.conf"
HOSTAPD_UNIT = sys.argv[2] if len(sys.argv) > 2 else "hostapd"
CTRL_DIR = "/var/run/hostapd"
INTERVAL = 15 # seconds between health checks (backstop cadence, not first response)
FAIL_THRESHOLD = 2 # consecutive bad checks before restarting (debounces re-enum blips)
@@ -98,15 +103,15 @@ def ap_enabled(ifname):
def recover(ifname):
log(f"AP {ifname} not beaconing — clearing failed state and restarting hostapd", "warn")
log(f"AP {ifname} not beaconing — clearing failed state and restarting {HOSTAPD_UNIT}", "warn")
# reset-failed first in case some other path parked the unit in a failed state;
# harmless when it isn't.
subprocess.run(["systemctl", "reset-failed", "hostapd"],
subprocess.run(["systemctl", "reset-failed", HOSTAPD_UNIT],
capture_output=True, text=True)
r = subprocess.run(["systemctl", "restart", "hostapd"],
r = subprocess.run(["systemctl", "restart", HOSTAPD_UNIT],
capture_output=True, text=True)
if r.returncode != 0:
log(f"hostapd restart returned {r.returncode}: {r.stderr.strip()}", "warn")
log(f"{HOSTAPD_UNIT} restart returned {r.returncode}: {r.stderr.strip()}", "warn")
def main():