diff --git a/hw/dixie/default.nix b/hw/dixie/default.nix index 0b6649c..db7c711 100644 --- a/hw/dixie/default.nix +++ b/hw/dixie/default.nix @@ -128,6 +128,105 @@ let TimeoutStartSec = "600"; }; }; + + # USB AX9L (mt7921u) is primary; internal CNVi (iwlwifi/wlo1) is fallback. + # Connection profiles stay on-box (PSK); this only selects which radio is up. + usbWifiMac = "74:19:f8:17:5e:53"; + usbWifiConn = "ATTjkThnDS"; + internalWifiConn = "ATTjkThnDS-iwlwifi"; + internalWifiIf = "wlo1"; + wifiUsbFailover = pkgs.writeShellScript "wifi-usb-failover" '' + set -eu + nmcli=${pkgs.networkmanager}/bin/nmcli + logger=${pkgs.util-linux}/bin/logger + systemd_run=${pkgs.systemd}/bin/systemd-run + tr=${pkgs.coreutils}/bin/tr + + USB_MAC="${usbWifiMac}" + USB_CONN="${usbWifiConn}" + FALLBACK_CONN="${internalWifiConn}" + INTERNAL_IF="${internalWifiIf}" + + log() { "$logger" -t wifi-usb-failover -- "$*"; } + + iface_mac() { + "$tr" 'A-F' 'a-f' < "/sys/class/net/$1/address" 2>/dev/null || true + } + + usb_iface() { + for n in /sys/class/net/*; do + n=''${n##*/} + [ "$n" = "lo" ] && continue + [ "$(iface_mac "$n")" = "$USB_MAC" ] && { echo "$n"; return 0; } + done + return 1 + } + + is_usb_iface() { + [ "$(iface_mac "$1")" = "$USB_MAC" ] + } + + demote_internal() { + "$nmcli" device set "$INTERNAL_IF" autoconnect no >/dev/null 2>&1 || true + "$nmcli" connection down "$FALLBACK_CONN" >/dev/null 2>&1 || true + "$nmcli" device disconnect "$INTERNAL_IF" >/dev/null 2>&1 || true + } + + promote_internal() { + if usb_iface >/dev/null 2>&1; then + log "USB still present; not failing over" + return 0 + fi + log "USB absent; bringing up $FALLBACK_CONN on $INTERNAL_IF" + "$nmcli" device set "$INTERNAL_IF" autoconnect yes >/dev/null 2>&1 || true + "$nmcli" connection up "$FALLBACK_CONN" ifname "$INTERNAL_IF" >/dev/null 2>&1 || true + } + + settle() { + i=0 + while [ "$i" -lt 20 ]; do + if usb_iface >/dev/null 2>&1; then + u=$(usb_iface) + log "settle: USB $u present; demoting internal" + demote_internal + if ${pkgs.iproute2}/bin/ip -4 -o addr show dev "$u" | ${pkgs.gnugrep}/bin/grep -q ' inet '; then + return 0 + fi + "$nmcli" connection up "$USB_CONN" ifname "$u" >/dev/null 2>&1 || true + return 0 + fi + i=$((i + 1)) + sleep 1 + done + log "settle: no USB after 20s; failing over" + promote_internal + } + + IFACE=''${1-} + ACTION=''${2-} + + case "$IFACE" in + boot|settle) settle; exit 0 ;; + after-usb-down) sleep 3; promote_internal; exit 0 ;; + esac + + case "$ACTION" in + up) + if is_usb_iface "$IFACE"; then + log "USB $IFACE up; demoting $INTERNAL_IF" + demote_internal + fi + ;; + down) + if is_usb_iface "$IFACE"; then + log "USB $IFACE down; scheduling failover check" + "$systemd_run" --quiet --collect --unit=wifi-usb-failover-down \ + --property=Type=oneshot "$0" after-usb-down >/dev/null 2>&1 || true + fi + ;; + esac + exit 0 + ''; in { imports = [ @@ -203,11 +302,24 @@ in upstream = "http://127.0.0.1:11434"; }; - # Wi-Fi only host. NetworkManager comes from the base config; the AX9L (mt7921u) becomes - # the primary once plugged in and the internal CNVi radio stays as fallback. Connection - # profiles are copied from the live-USB session at install time (see the install notes), - # not stored in this repo. + # Wi-Fi only host. NetworkManager comes from the base config; the AX9L (mt7921u) is + # primary and the internal CNVi radio is fallback. Connection profiles are copied + # from the live-USB session at install time (see the install notes), not stored here. networking.networkmanager.wifi.backend = "wpa_supplicant"; + networking.networkmanager.dispatcherScripts = [ + { source = wifiUsbFailover; type = "basic"; } + ]; + systemd.services.wifi-usb-failover-boot = { + description = "Prefer USB Wi-Fi; fail over to internal CNVi if the dongle is absent"; + after = [ "NetworkManager.service" ]; + wants = [ "NetworkManager.service" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${wifiUsbFailover} boot"; + }; + }; # Reachable only over the tailnet: 11434 (router), 11433 (vision), 11435 (tether) # on tailscale0, and SSH from the LAN (base config opens 22) for bring-up.