From f91dbe4d5f7ccb123b3f1108a56bfaa5242609f0 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 15:56:46 +0000 Subject: [PATCH 1/2] WiFi: adapter chooser, optional wifis, wrong-password diagnosis - Menu asks which adapter when several exist (shows USB/PCI); the web UI helper prefers the adapter already configured. - Stale-entry cleanup also marks remaining installer wifis optional so a dead WiFi entry can't hold up boot. - After a failed connect, read wpa_supplicant's log to say wrong password / network not found / rejected. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4 --- menus/wifi.sh | 87 ++++++++++++++++++- .../files/usr/local/bin/kiosk-wifi-helper | 48 +++++++++- 2 files changed, 130 insertions(+), 5 deletions(-) diff --git a/menus/wifi.sh b/menus/wifi.sh index ac17415..f412d92 100644 --- a/menus/wifi.sh +++ b/menus/wifi.sh @@ -57,17 +57,91 @@ try: del w[n] if not w: del d["network"]["wifis"] + # Not "optional" = the boot waits (up to ~2 min) for it to get an address. + opt = [n for n, v in w.items() if isinstance(v, dict) and not v.get("optional")] + for n in opt: + w[n]["optional"] = True + if stale or opt: + if not stale: + shutil.copy2(p, p + ".kiosk-bak") mode = os.stat(p).st_mode & 0o777 with open(p, "w") as fh: yaml.safe_dump(d, fh, default_flow_style=False) os.chmod(p, mode) - print("Removed stale WiFi entries from %s: %s" % (p, ", ".join(map(str, stale)))) + print("Fixed WiFi entries in %s (removed: %s)" % (p, ", ".join(map(str, stale)) or "none")) except Exception: pass PYEOF done } +# Why a connect attempt failed, from wpa_supplicant's own log lines in +# the journal (netplan runs it as netplan-wpa-.service). Prints a +# short human-readable reason, or nothing if the log says nothing useful. +wifi_diagnose() { + local log + log=$(journalctl -b -u "netplan-wpa-$1" -n 150 --no-pager 2>/dev/null) || return 0 + if grep -q "reason=WRONG_KEY\|pre-shared key may be incorrect" <<< "$log"; then + echo "wrong password (the network rejected the key)" + elif grep -q "CTRL-EVENT-NETWORK-NOT-FOUND" <<< "$log"; then + echo "network not found - wrong name, or out of range of this adapter" + elif grep -q "CTRL-EVENT-ASSOC-REJECT\|CTRL-EVENT-AUTH-REJECT" <<< "$log"; then + echo "the access point refused the connection (MAC filter, or WPA3-only/unsupported security)" + fi +} + +# Every wireless interface, one per line. +wifi_list_ifaces() { + local d + for d in /sys/class/net/*; do + if [[ -d "$d/wireless" || -e "$d/phy80211" ]]; then + basename "$d" + fi + done +} + +# "USB" / "PCI" / "" - what an interface hangs off, to tell a dongle +# from a built-in card when there are two. +wifi_iface_bus() { + local sub + sub=$(readlink -f "/sys/class/net/$1/device/subsystem" 2>/dev/null) || return 0 + case "$sub" in + */usb) echo "USB" ;; + */pci) echo "PCI" ;; + esac +} + +# The interface to configure: the only one, or the user's pick when there +# are several (e.g. built-in card plus a USB dongle). +wifi_choose_iface() { + local -a ifaces + mapfile -t ifaces < <(wifi_list_ifaces) + if (( ${#ifaces[@]} == 0 )); then + wifi_detect_iface + return + fi + if (( ${#ifaces[@]} == 1 )); then + echo "${ifaces[0]}" + return 0 + fi + { + echo "Several WiFi adapters found:" + local i bus + for i in "${!ifaces[@]}"; do + bus=$(wifi_iface_bus "${ifaces[$i]}") + printf ' %d. %s%s\n' "$((i + 1))" "${ifaces[$i]}" "${bus:+ ($bus)}" + done + } >&2 + local choice + read -r -p "Which one? [1]: " choice &2 + choice=${choice:-1} + if [[ "$choice" =~ ^[0-9]+$ ]] && (( choice >= 1 && choice <= ${#ifaces[@]} )); then + echo "${ifaces[$((choice - 1))]}" + else + echo "${ifaces[0]}" + fi +} + wifi_menu() { local rc=0 wifi_configure || rc=$? @@ -165,7 +239,7 @@ wifi_configure() { fi local wifi_iface - if ! wifi_iface=$(wifi_detect_iface); then + if ! wifi_iface=$(wifi_choose_iface); then log_warning "No WiFi hardware detected" echo "Network interfaces found: $(ls /sys/class/net 2>/dev/null | tr '\n' ' ')" echo "If you have a USB WiFi adapter, make sure it's plugged in. A built-in" @@ -344,7 +418,14 @@ WATCHEOF [[ -n "${SSH_CONNECTION:-}" ]] && echo "Connection successful - watchdog will not revert" else log_warning "Config applied but $wifi_iface has no address yet" - echo "Wrong password or network name is the usual cause. Details:" + local why + why=$(wifi_diagnose "$wifi_iface") + if [[ -n "$why" ]]; then + log_error "Couldn't connect to $ssid: $why" + else + echo "Wrong password or network name is the usual cause." + fi + echo "Details:" echo " sudo journalctl -b -u 'netplan-wpa-*' -u systemd-networkd -n 30 --no-pager" fi else diff --git a/provision/files/usr/local/bin/kiosk-wifi-helper b/provision/files/usr/local/bin/kiosk-wifi-helper index 6c1ab92..9099c68 100755 --- a/provision/files/usr/local/bin/kiosk-wifi-helper +++ b/provision/files/usr/local/bin/kiosk-wifi-helper @@ -107,8 +107,40 @@ except Exception: pass' "$WIFI_NETPLAN_FILE" 2>/dev/null } +# Why a connect attempt failed, from wpa_supplicant's own log lines in +# the journal (netplan runs it as netplan-wpa-.service). Prints a +# short human-readable reason, or nothing if the log says nothing useful. +diagnose_failure() { + local log + log=$(journalctl -b -u "netplan-wpa-$1" -n 150 --no-pager 2>/dev/null) || return 0 + if grep -q "reason=WRONG_KEY\|pre-shared key may be incorrect" <<< "$log"; then + echo "wrong password (the network rejected the key)" + elif grep -q "CTRL-EVENT-NETWORK-NOT-FOUND" <<< "$log"; then + echo "network not found - wrong name, or out of range of this adapter" + elif grep -q "CTRL-EVENT-ASSOC-REJECT\|CTRL-EVENT-AUTH-REJECT" <<< "$log"; then + echo "the access point refused the connection (MAC filter, or WPA3-only/unsupported security)" + fi +} + +# Prefer the adapter our netplan file already configures (if it's still +# there) over "first one found", so a built-in card plus a USB dongle +# don't swap places between calls. +prefer_configured_iface() { + [[ -f "$WIFI_NETPLAN_FILE" ]] || return 1 + python3 -I -c ' +import sys, os, yaml +try: + d = yaml.safe_load(open(sys.argv[1])) + for n in (d.get("network", {}).get("wifis") or {}): + if os.path.exists(sys.argv[2] + "/net/" + str(n)): + print(n); sys.exit(0) +except Exception: + pass +sys.exit(1)' "$WIFI_NETPLAN_FILE" "$SYS_CLASS" 2>/dev/null +} + need_iface() { - IFACE=$(detect_iface) || reply_error "No WiFi adapter found. If it's a USB adapter, check it's plugged in." + IFACE=$(prefer_configured_iface || detect_iface) || reply_error "No WiFi adapter found. If it's a USB adapter, check it's plugged in." command -v iw >/dev/null || reply_error "WiFi support (iw, wpasupplicant) isn't installed - run Core Settings -> WiFi once from the admin menu" } @@ -191,11 +223,18 @@ try: del w[n] if not w: del d["network"]["wifis"] + # Not "optional" = the boot waits (up to ~2 min) for it to get an address. + opt = [n for n, v in w.items() if isinstance(v, dict) and not v.get("optional")] + for n in opt: + w[n]["optional"] = True + if stale or opt: + if not stale: + shutil.copy2(p, p + ".kiosk-bak") mode = os.stat(p).st_mode & 0o777 with open(p, "w") as fh: yaml.safe_dump(d, fh, default_flow_style=False) os.chmod(p, mode) - print("Removed stale WiFi entries from %s: %s" % (p, ", ".join(map(str, stale)))) + print("Fixed WiFi entries in %s (removed: %s)" % (p, ", ".join(map(str, stale)) or "none")) except Exception: pass PYEOF @@ -271,6 +310,11 @@ do_connect() { if [[ -z "$addr" ]]; then restore "$backup" + local why + why=$(diagnose_failure "$IFACE") + if [[ -n "$why" ]]; then + reply_error "Couldn't connect to \"$ssid\": $why." + fi if [[ -n "$backup" ]]; then reply_error "Couldn't connect to \"$ssid\" - wrong password, or out of range. The previous WiFi setting was put back." fi From c196a5f7e755327403ac84d79807e2e2e8fdea57 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 16 Oct 2026 15:58:43 +0000 Subject: [PATCH 2/2] Turn off WiFi power save and USB autosuspend for Realtek dongles Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01A97KUggSE3rBL8veDM5Mr4 --- lib/provision.sh | 3 +++ .../files/etc/udev/rules.d/70-kiosk-wifi-power.rules | 11 +++++++++++ 2 files changed, 14 insertions(+) create mode 100644 provision/files/etc/udev/rules.d/70-kiosk-wifi-power.rules diff --git a/lib/provision.sh b/lib/provision.sh index 93fd71e..307a613 100644 --- a/lib/provision.sh +++ b/lib/provision.sh @@ -242,6 +242,9 @@ provision_install_wifi_helper() { log_warning "Couldn't set up the kiosk WiFi screen's permission - Ctrl+Alt+Super+W won't be able to connect" fi rm -f "$tmp" + # Flaky cheap dongles (RTL8188CUS...) need WiFi power saving off. + provision_install_file "etc/udev/rules.d/70-kiosk-wifi-power.rules" /etc/udev/rules.d/70-kiosk-wifi-power.rules + sudo udevadm control --reload-rules 2>/dev/null || true } provision_configure_firewall() { diff --git a/provision/files/etc/udev/rules.d/70-kiosk-wifi-power.rules b/provision/files/etc/udev/rules.d/70-kiosk-wifi-power.rules new file mode 100644 index 0000000..4064080 --- /dev/null +++ b/provision/files/etc/udev/rules.d/70-kiosk-wifi-power.rules @@ -0,0 +1,11 @@ +# Keep WiFi adapters awake. Power saving (the adapter dozing between +# packets, and the USB port suspending an idle dongle) is what makes +# cheap dongles like the Realtek RTL8188CUS drop or stall a connection. +# Both are ordinary software settings, so nothing here can harm the +# hardware: the cost is a fraction of a watt. + +# Radio power save off on every WiFi interface as it appears. +ACTION=="add", SUBSYSTEM=="net", KERNEL=="wl*", RUN+="/usr/sbin/iw dev $name set power_save off" + +# No USB autosuspend for the common RTL8188/8192 CU/CUS/RU dongles. +ACTION=="add", SUBSYSTEM=="usb", ATTR{idVendor}=="0bda", ATTR{idProduct}=="8176|8177|8178|8179|817a|817b|817c|817d|817e|8191", TEST=="power/control", ATTR{power/control}="on"