#!/usr/bin/env bash ## # WiFi management for Toes appliance setup. # Uses nmcli (NetworkManager) which is standard on Raspberry Pi OS Bookworm. # # Commands: # status - Check WiFi connection state # scan - List available WiFi networks # connect - Connect to a network (SSID and password as args) # hotspot-start - Start the setup hotspot + captive portal DNS # hotspot-stop - Stop the hotspot + captive portal DNS # has-wifi - Exit 0 if WiFi hardware exists, 1 if not set -euo pipefail HOTSPOT_SSID="Toes Setup" HOTSPOT_IFACE="wlan0" HOTSPOT_CON="toes-hotspot" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" CAPTIVE_CONF="$SCRIPT_DIR/wifi-captive.conf" DNSMASQ_PID="/tmp/toes-dnsmasq.pid" cmd="${1:-help}" case "$cmd" in status) # Returns JSON: { connected, ssid, ip } STATE=$(nmcli -t -f GENERAL.STATE device show "$HOTSPOT_IFACE" 2>/dev/null | cut -d: -f2 | xargs) SSID=$(nmcli -t -f GENERAL.CONNECTION device show "$HOTSPOT_IFACE" 2>/dev/null | cut -d: -f2 | xargs) IP=$(nmcli -t -f IP4.ADDRESS device show "$HOTSPOT_IFACE" 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1 | xargs) CONNECTED="false" if echo "$STATE" | grep -qi "connected" && [ "$SSID" != "$HOTSPOT_CON" ] && [ -n "$SSID" ] && [ "$SSID" != "--" ]; then CONNECTED="true" fi printf '{"connected":%s,"ssid":"%s","ip":"%s"}\n' "$CONNECTED" "${SSID:-}" "${IP:-}" ;; scan) # Force a fresh scan then list networks as JSON array nmcli device wifi rescan ifname "$HOTSPOT_IFACE" 2>/dev/null || true sleep 1 nmcli -t -f SSID,SIGNAL,SECURITY device wifi list ifname "$HOTSPOT_IFACE" 2>/dev/null \ | awk -F: ' BEGIN { printf "[" } NR > 1 { printf "," } { gsub(/"/, "\\\"", $1) if ($1 != "" && $1 != "--") { printf "{\"ssid\":\"%s\",\"signal\":%s,\"security\":\"%s\"}", $1, ($2 == "" ? "0" : $2), $3 } } END { printf "]\n" } ' | python3 -c " import sys, json raw = json.load(sys.stdin) # Deduplicate by SSID, keeping the strongest signal seen = {} for net in raw: ssid = net.get('ssid', '') if not ssid: continue if ssid not in seen or net.get('signal', 0) > seen[ssid].get('signal', 0): seen[ssid] = net result = sorted(seen.values(), key=lambda x: -x.get('signal', 0)) json.dump(result, sys.stdout) print() " ;; connect) SSID="${2:-}" PASSWORD="${3:-}" if [ -z "$SSID" ]; then echo '{"ok":false,"error":"SSID is required"}' >&2 exit 1 fi # Stop captive portal DNS "$0" dns-stop 2>/dev/null || true # Stop the hotspot first if it's running nmcli connection down "$HOTSPOT_CON" 2>/dev/null || true nmcli connection delete "$HOTSPOT_CON" 2>/dev/null || true sleep 1 # Connect to the network if [ -n "$PASSWORD" ]; then OUTPUT=$(nmcli device wifi connect "$SSID" password "$PASSWORD" ifname "$HOTSPOT_IFACE" 2>&1) || { # Connection failed - restart hotspot so user can try again "$0" hotspot-start 2>/dev/null || true echo "{\"ok\":false,\"error\":\"$(echo "$OUTPUT" | tr '"' "'" | tr '\n' ' ')\"}" exit 1 } else OUTPUT=$(nmcli device wifi connect "$SSID" ifname "$HOTSPOT_IFACE" 2>&1) || { "$0" hotspot-start 2>/dev/null || true echo "{\"ok\":false,\"error\":\"$(echo "$OUTPUT" | tr '"' "'" | tr '\n' ' ')\"}" exit 1 } fi # Wait for an IP for i in $(seq 1 10); do IP=$(nmcli -t -f IP4.ADDRESS device show "$HOTSPOT_IFACE" 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1 | xargs) if [ -n "$IP" ] && [ "$IP" != "" ]; then echo "{\"ok\":true,\"ip\":\"$IP\",\"ssid\":\"$SSID\"}" exit 0 fi sleep 1 done echo "{\"ok\":true,\"ip\":\"\",\"ssid\":\"$SSID\"}" ;; hotspot-start) # Delete any existing hotspot connection nmcli connection delete "$HOTSPOT_CON" 2>/dev/null || true # Create the hotspot nmcli connection add \ type wifi \ ifname "$HOTSPOT_IFACE" \ con-name "$HOTSPOT_CON" \ ssid "$HOTSPOT_SSID" \ autoconnect no \ wifi.mode ap \ wifi.band bg \ wifi-sec.key-mgmt wpa-psk \ wifi-sec.psk "toessetup" \ ipv4.method shared \ ipv4.addresses "10.42.0.1/24" \ 2>/dev/null nmcli connection up "$HOTSPOT_CON" 2>/dev/null # Start captive portal DNS redirect "$0" dns-start 2>/dev/null || true echo '{"ok":true,"ssid":"'"$HOTSPOT_SSID"'","ip":"10.42.0.1"}' ;; hotspot-stop) "$0" dns-stop 2>/dev/null || true nmcli connection down "$HOTSPOT_CON" 2>/dev/null || true nmcli connection delete "$HOTSPOT_CON" 2>/dev/null || true echo '{"ok":true}' ;; dns-start) # Start dnsmasq for captive portal DNS (resolves everything to 10.42.0.1) # Kill any existing instance first "$0" dns-stop 2>/dev/null || true # NetworkManager runs its own dnsmasq on port 53, so we need to stop it # for the hotspot interface and run our own if [ -f "$CAPTIVE_CONF" ]; then sudo dnsmasq --conf-file="$CAPTIVE_CONF" --pid-file="$DNSMASQ_PID" --port=53 2>/dev/null || true fi ;; dns-stop) # Stop our captive portal dnsmasq if [ -f "$DNSMASQ_PID" ]; then sudo kill "$(cat "$DNSMASQ_PID")" 2>/dev/null || true sudo rm -f "$DNSMASQ_PID" fi # Also kill by name in case PID file is stale sudo pkill -f "dnsmasq.*wifi-captive.conf" 2>/dev/null || true ;; has-wifi) # Check if wlan0 exists if nmcli device show "$HOTSPOT_IFACE" > /dev/null 2>&1; then exit 0 else exit 1 fi ;; help|*) echo "Usage: $0 {status|scan|connect|hotspot-start|hotspot-stop|has-wifi}" echo "" echo "Commands:" echo " status Check WiFi connection state (JSON)" echo " scan List available WiFi networks (JSON array)" echo " connect SSID [PASSWORD] Connect to a WiFi network" echo " hotspot-start Start the Toes Setup hotspot + captive portal" echo " hotspot-stop Stop the hotspot + captive portal" echo " has-wifi Exit 0 if WiFi hardware present" exit 1 ;; esac