The in-kernel rtw89_8852bu on this Ubuntu kernel base predates mainline's
USB2->3 auto-switch for this chip, permanently capping the AP dongle at
USB2/480M. morrownr/rtw89 (dkms) has that switch. ap/rtw89.conf now
blacklists the in-kernel rtw89 modules and tunes the replacement
(disable_ps_mode + switch_usb_mode); ap/install-rtw89-driver.sh builds
and installs it, pinned to a specific upstream commit, kept separate
from deploy.sh since a dkms rebuild is too slow to run on every deploy.
Verified live: negotiates USB3/5000M on a dedicated USB3 controller
(480M on a USB2-only one, as expected), AP recovered via hostapd's
Restart=always + van-ap-watchdog with no manual intervention. Hit and
documented one real gotcha along the way: in-kernel rtw89_core refused
to unload while its own dependents (rtw89_8852b, rtw89_8852b_common)
were still loaded, which blocked the new module with a duplicate-symbol
error until all in-kernel modules were removed first.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJfEELeh3ercpRBp8yYrYS
- status_text: decode the 24-bit "status" field into human-readable flag
names (e.g. "Cell Temp Low, Low Voltage") instead of raw hex, using the
bit table pulled from com.lithionics.bms's array/advanced resource (dumped
with aapt -- the app's own StatusCodeTable class references stale/wrong
resource IDs and can't be trusted for this). Applied to "status" only;
last_fault_code is a lifetime latch that accumulates many bits over time
and isn't meaningfully summarized the same way.
- Fixed the info sensors (total_consumed, firmware_version, serial_number,
etc.) showing "Unavailable" in HA a minute after connecting: they were
getting the same expire_after as the periodic telemetry sensors, but the
$info line the come from is only sent once per BLE connection, so HA's
expire timer always fired. These are retained-topic diagnostics meant to
hold their last value indefinitely; expire_after now only applies to the
periodic SENSORS group.
- STUCK_DISCOVERY_THRESHOLD 3 -> 1: restart bluetooth.service after the
first failed scan instead of waiting for three, cutting reconnect time
roughly in third. Still rate-limited via BLUETOOTH_RESTART_COOLDOWN_S.
Also removed the per-line "RAW ..." debug print (was flooding the journal).
All three changes verified live on host wan via journalctl/mosquitto_sub.
Publishes the RV's 12V LiFePO4 house battery to HA via MQTT discovery
(pack voltage, 4 cell voltages, current, SOC, BMS/battery temp, status).
Connects over the battery's BLE HM-10 UART module (service ffe0/char
ffe1, no pairing) using bleak; protocol reverse-engineered from the
com.lithionics.bms Android app's own BLE/parsing code.
Deliberately named li3/ and van-li3-battery, not battery/van-battery —
that name is reserved for the host's own AC/UPS power-supply monitor
(different hardware, unrelated concern).
Self-heals a bluetoothd discovery-state wedge (Discovering stuck "yes",
connects failing with le-connection-abort-by-local) that shows up after
repeated failed connects to this device on the Pi's onboard adapter: the
daemon retries scan/connect internally (MQTT session and HA entities
stay up across retries) and restarts bluetooth.service itself after 3
consecutive scan failures, rate-limited to once per 5 min.
Tails journalctl -kf for the "nvme nvmeN: I/O tag ... timeout, reset
controller" signature that crashed and corrupted the root fs on 2026-08-02
(and recurred 2026-08-04, that time self-healing). Watches a short grace
window to tell a clean self-heal from an escalation (repeated timeout or a
following ext4 error) before paging via the existing Pushover credentials,
with a live SMART/superblock snapshot in the alert body.
EC25-AF GNSS wiring (udev hotplug into gpsd) plus van-gps-owntracks, a
port of the wayback-era gps_to_owntracks.py: apt-only deps (python3-gps
instead of pip-only gpsdclient, paho 2.x callback API), broker secrets
moved out of the code into /etc/van-gps/config.json (0600, seeded from
a sanitized example), gpsd host now localhost. client_id is vanq-wan —
a legacy client still holds vanq on the broker and shared IDs get
kicked in a connect/disconnect loop. Exit-on-disconnect + systemd
Restart=always is the reconnect logic.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Include the gsm device in the WAN/Uplinks table, shown as its routed
netdev with IP and metric (nmcli actions still target cdc-wdm0). New
Signal column fed by mmcli -K in the single status spawn (percent, tech,
operator). Restart = mmcli disable/enable — the EC25 MBIM plugin doesn't
support --reset — with a USB unbind/bind fallback (vendor 2c7c) when
ModemManager can't reach the modem. README: modem setup steps (Koodo
profile creation, MBIM netdev gotcha, Cockpit row).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The fixed 2 GiB ha_van allocation starved the 4 GB Pi. HA Container now
runs on the host network (http://10.42.0.1:8123): ha/homeassistant.container
installs to /etc/containers/systemd/, config in /srv/homeassistant, host
D-Bus mounted for onboard Bluetooth (needs apparmor=unconfined — Ubuntu's
dbus-daemon mediates per AppArmor label and denies AddMatch to BlueZ —
plus NET_ADMIN/NET_RAW for habluetooth adapter recovery).
Drop the VM-era plumbing: the 10.42.0.50 dhcp-host pin becomes a
host-record for 10.42.0.1, and the legacy-URL DNAT + hairpin masquerade
go away entirely. ha_van.xml retired to git history.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The USB hub (5GHz + 2.4GHz AP dongles, Starlink + LAN RTL8153s) moves over
from wayback; MAC-derived wlx*/enx* names travel with it, so hostapd/
networkd/cockpit configs are unchanged. Pi diffs only: failover WAN list
(wlan0 wifi 100, eth0 150, starlink USB 200, Koodo 300), cpu_thermal
sensor, bcm2835 watchdog 10s, no HA DNAT/lease, and deploy.sh drops
battery/lid/heartbeat/ZT-dns. Netplan reference in ap/50-van-wan.yaml.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
vanrouter.js hardcoded the 5GHz iface; generalize to an APS list
(iface + hostapd unit + band label). The Access Points card now renders
a status line, client table, and Restart button per band, and the WAN
table excludes both AP ifaces.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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>
The VM lived on libvirt's default NAT net (192.168.122.50) behind a
/etc/libvirt/hooks/network hook that inserted FORWARD accepts above
libvirt's REJECT. libvirtd restarts re-inserted its chains on top
without firing the hook, silently cutting clients off from HA.
Now the VM's NIC (virtio, was e1000) bridges into br0: pinned lease
10.42.0.50 / name 'homeassistant' via van-ap dnsmasq, mDNS discovery
works, and no firewall hook can rot. Legacy http://10.42.0.1:8123
kept working via DNAT + same-subnet hairpin masquerade. Hook, libvirt
DHCP reservation, and iptables rules removed; domain XML in ha/.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Starlink's USB ethernet shares a hub with the rtw89 Wi-Fi adapter; when Starlink
flaps, the hub re-enumerates and tears the radio down, so hostapd loses its
interface and exits. Two stock defaults left the AP dark: Restart=on-failure
misses clean exits, and the 5-in-10s start limit makes systemd give up during a
re-enumeration storm.
- hostapd-restart.conf drop-in: Restart=always, RestartSec=5, StartLimitIntervalSec=0
so hostapd retries forever until the interface returns (verified: SIGKILL -> back
to state=ENABLED in ~7s).
- van-ap-watchdog daemon: backstop for the case systemd can't see (hostapd running
but radio wedged). Polls hostapd_cli status; restarts hostapd if the iface is
present but not ENABLED, waits it out if the iface is mid-re-enumeration.
- hostapd.conf: add ctrl_interface so the watchdog can read the real AP state.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add a wired LAN port on the same 10.42.0.0/24 segment as VanLink WiFi by
bridging both into br0. The gateway IP, dnsmasq, and NAT move off the wlan name
onto br0; hostapd (not networkd) enslaves the wlan via bridge=br0, since a
station-mode wlan can't be bridged until it's in AP mode. NM leaves the wired
port unmanaged so networkd owns it. Failover/Starlink (enxd8ec5eeb3512)
untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two TODO Tier-1 items:
- Hardware watchdog: power/10-vanlink-watchdog.conf drop-in sets
RuntimeWatchdogSec=20s + RebootWatchdogSec=5min; deploy.sh applies it via
daemon-reexec. PID1 pets intel_oc_wdt; a >20s systemd hang hard-resets the box.
- Heartbeat / dead-man's switch: vendored hbc.yaml + hbc.service report to
hbd.wrede.pvt (UDP 50003) every 15s as user andreas, shipping cpu/mem/disk/
network/zfs metrics. The hbc binary is installed once via the heartbeat
project's installer (README §4); deploy.sh starts the service when present.
Auto-power-on-when-mains-returns: documented as a BIOS-only option (not
OS-controllable on this ZenBook) in README §3 + TODO, per decision.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
While on battery (AC0/online=0), sends escalating Pushover alerts at 25/20/15%
and at 10% sends a final alert then `systemctl poweroff` (after shutdown_grace
so the alert flushes first). Edge-triggered per discharge episode; re-arms when
mains returns; plug-out already-low collapses to a single alert (no burst).
Pushover token+user live only in /etc/van-battery/pushover.json (0600), seeded
from pushover.json.example on first deploy and never committed. Missing/
placeholder creds disable sending but NOT the shutdown — running flat must
always power down safely. Publishes /run/van-battery/state.json. Stdlib only.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Single sysfs sample loop drives three jobs off one daemon (modeled on
van-failover):
- live state at /run/van-thermal/state.json; new Cockpit "Temperatures"
card reads it in the existing 5s loop (no per-refresh `sensors` shell-out)
- journal alerts on ok<->warn<->crit crossings, with clear_margin hysteresis
- throttled, self-rotating CSV history at /var/log/van-thermal.csv
Sensors resolved by hwmon name+label (coretemp/Package id 0, nvme/Composite),
never by hwmonN index (not stable across boots). Defaults: CPU warn 80 /
crit 95, NVMe warn 65 / crit 70. deploy.sh installs + enables the service.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
wayback runs as an always-on router living lid-closed in the van, but
stock logind HandleLidSwitch=suspend would sleep it on lid close (even
on AC). Add a logind drop-in that ignores the lid in all states and
mask every sleep target so idle/GUI/stray `systemctl suspend` can't
suspend it either.
- power/10-vanlink-nolid.conf -> /etc/systemd/logind.conf.d/
- deploy.sh: install drop-in, mask sleep/suspend/hibernate/hybrid-sleep,
restart systemd-logind
- README: power/ layout row, never-sleep operation notes + reversal recipe
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous fix (RequiredForOnline=no on the ZT link) could not work and the
~2min boot stall returned. systemd-networkd-wait-online (systemd 255) only
declares the network online when at least one managed, online-eligible link
goes routable. networkd here manages only the AP (wlxc…, local-only) and the ZT
overlay — and with the ZT link also marked RequiredForOnline=no (on top of the
AP link that already was), wait-online had zero candidate links and could never
satisfy "online", burning its full 120s timeout every boot. Journal confirmed:
"Timeout occurred while waiting for network connectivity" at exactly +120s,
gating network-online.target → zerotier-one (the recovery path).
Mask systemd-networkd-wait-online entirely — neither networkd link is a real
uplink, and real uplink readiness is already covered by NetworkManager-wait-online.
Wire the mask into deploy.sh, delete the now-dead RequiredForOnline=no drop-in,
and correct README §3/§5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
systemd-networkd-wait-online waits for every managed networkd link to go
routable. The ZT link ztuga7c2kh only appears once zerotier-one starts, but
zerotier-one is ordered After=network-online.target — so wait-online blocked
on the not-yet-existing link until its 120s timeout, adding ~2min to every
boot and delaying ZeroTier (the recovery path).
Add a RequiredForOnline=no drop-in so the overlay link is excluded from the
gate; real uplink readiness is already covered by NetworkManager-wait-online
(networkd owns only the AP + ZT links here, neither a true uplink). Wire it
into deploy.sh and document the loop in README §3/§5.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>