Explorar el Código

Add periodic autohotspot check via systemd timer

The autohotspot script now supports two modes:
- Boot mode (default): 30s scan with retries at startup
- Check mode (--check): quick single-pass check every 60s

A systemd timer (autohotspot-check.timer) runs --check every 60s to:
- Fall back to hotspot if WiFi drops unexpectedly
- Reconnect to known WiFi when it becomes available in hotspot mode
- Recover from any disconnected state

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tuanchris hace 4 meses
padre
commit
05a95c9a95
Se han modificado 5 ficheros con 277 adiciones y 176 borrados
  1. 7 6
      modules/wifi/manager.py
  2. 237 167
      wifi/autohotspot
  3. 8 0
      wifi/autohotspot-check.service
  4. 11 0
      wifi/autohotspot-check.timer
  5. 14 3
      wifi/setup-wifi.sh

+ 7 - 6
modules/wifi/manager.py

@@ -360,22 +360,23 @@ def forget_network(ssid: str) -> dict:
 
 
 def _trigger_autohotspot():
-    """Re-run the autohotspot script to re-evaluate WiFi mode.
+    """Trigger an immediate autohotspot check.
 
-    Called when the active network is forgotten, so the system can
-    fall back to hotspot mode if no other known networks are available.
+    Called when the active network is forgotten so the user doesn't have
+    to wait for the next 60s timer tick. Falls back to direct nmcli
+    commands in Docker where the host script isn't available.
     """
     autohotspot_path = "/usr/local/bin/autohotspot"
     try:
         if os.path.exists(autohotspot_path):
-            logger.info("Re-running autohotspot after forgetting active network...")
+            logger.info("Triggering autohotspot --check after forgetting active network...")
             subprocess.Popen(
-                [autohotspot_path],
+                [autohotspot_path, "--check"],
                 stdout=subprocess.DEVNULL,
                 stderr=subprocess.DEVNULL,
             )
         else:
-            # Fallback: directly activate hotspot if autohotspot script not found
+            # Docker fallback: directly activate hotspot via D-Bus
             logger.info("Autohotspot script not found, activating hotspot directly...")
             run_nmcli("dev", "disconnect", "wlan0")
             run_nmcli("con", "up", HOTSPOT_CON_NAME)

+ 237 - 167
wifi/autohotspot

@@ -1,167 +1,237 @@
-#!/bin/bash
-#
-# Dune Weaver Autohotspot
-#
-# Runs on boot to decide between WiFi client mode and hotspot mode.
-# If a known WiFi network is found, connect to it.
-# If not, create a "Dune Weaver" hotspot so users can configure WiFi via the app.
-#
-# DNS redirect for captive portal is handled by NetworkManager's built-in
-# dnsmasq via config in /etc/NetworkManager/dnsmasq-shared.d/.
-#
-# Requires: NetworkManager (Pi Trixie)
-#
-
-set -euo pipefail
-
-HOTSPOT_CON_NAME="DuneWeaver-Hotspot"
-MODE_FILE="/tmp/dw-wifi-mode"
-SCAN_TIMEOUT=30
-SCAN_INTERVAL=5
-IFACE="wlan0"
-
-log() {
-    echo "[autohotspot] $*"
-    logger -t autohotspot "$*"
-}
-
-# Wait for NetworkManager to be ready
-wait_for_nm() {
-    local waited=0
-    while [ $waited -lt 10 ]; do
-        if nmcli general status &>/dev/null; then
-            return 0
-        fi
-        sleep 1
-        waited=$((waited + 1))
-    done
-    log "ERROR: NetworkManager not ready after 10s"
-    return 1
-}
-
-# Get list of saved WiFi connection names
-get_saved_wifi() {
-    nmcli -t -f NAME,TYPE con show | grep ':.*wireless' | cut -d: -f1 | grep -v "^${HOTSPOT_CON_NAME}$" || true
-}
-
-# Scan for available networks and check if any match saved connections
-check_for_known_wifi() {
-    # Trigger a fresh scan
-    nmcli dev wifi rescan ifname "$IFACE" 2>/dev/null || true
-    sleep 2
-
-    # Get available SSIDs
-    local available
-    available=$(nmcli -t -f SSID dev wifi list ifname "$IFACE" 2>/dev/null | sort -u | grep -v '^$' || true)
-
-    if [ -z "$available" ]; then
-        return 1
-    fi
-
-    # Get saved connections
-    local saved
-    saved=$(get_saved_wifi)
-
-    if [ -z "$saved" ]; then
-        return 1
-    fi
-
-    # Check if any saved connection matches an available network
-    while IFS= read -r saved_name; do
-        # Get the SSID associated with this saved connection
-        local saved_ssid
-        saved_ssid=$(nmcli -t -f connection.id,802-11-wireless.ssid con show "$saved_name" 2>/dev/null | grep '802-11-wireless.ssid' | cut -d: -f2 || true)
-
-        # If we couldn't get the SSID from connection details, use the connection name as SSID
-        if [ -z "$saved_ssid" ]; then
-            saved_ssid="$saved_name"
-        fi
-
-        if echo "$available" | grep -qxF "$saved_ssid"; then
-            echo "$saved_name"
-            return 0
-        fi
-    done <<< "$saved"
-
-    return 1
-}
-
-# Activate client mode with a known connection
-activate_client_mode() {
-    local con_name="$1"
-    log "Found known network, activating connection: $con_name"
-
-    # Deactivate hotspot if it's active
-    nmcli con down "$HOTSPOT_CON_NAME" 2>/dev/null || true
-
-    # Connect
-    if nmcli con up "$con_name" 2>/dev/null; then
-        local ip
-        ip=$(nmcli -t -f IP4.ADDRESS dev show "$IFACE" 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1 || echo "unknown")
-        log "Client mode active. Connected to '$con_name', IP: $ip"
-        echo "client" > "$MODE_FILE"
-        return 0
-    else
-        log "Failed to connect to '$con_name'"
-        return 1
-    fi
-}
-
-# Activate hotspot mode
-activate_hotspot_mode() {
-    log "No known WiFi found. Activating hotspot mode..."
-
-    # Make sure we're not connected to anything
-    nmcli dev disconnect "$IFACE" 2>/dev/null || true
-
-    # Activate the hotspot profile
-    # NM's shared mode starts built-in dnsmasq for DHCP+DNS.
-    # Our config in /etc/NetworkManager/dnsmasq-shared.d/ makes it
-    # redirect all DNS queries to 10.42.0.1 for captive portal detection.
-    if nmcli con up "$HOTSPOT_CON_NAME" 2>/dev/null; then
-        log "Hotspot active. SSID: $(nmcli -t -f 802-11-wireless.ssid con show "$HOTSPOT_CON_NAME" 2>/dev/null | cut -d: -f2)"
-        log "DNS redirect active via NM dnsmasq (captive portal mode)"
-        echo "hotspot" > "$MODE_FILE"
-        return 0
-    else
-        log "ERROR: Failed to activate hotspot"
-        return 1
-    fi
-}
-
-# Main logic
-main() {
-    log "Starting autohotspot check..."
-
-    # Wait for NetworkManager
-    if ! wait_for_nm; then
-        log "NetworkManager not available, exiting"
-        exit 1
-    fi
-
-    # Check if hotspot profile exists
-    if ! nmcli con show "$HOTSPOT_CON_NAME" &>/dev/null; then
-        log "Hotspot profile '$HOTSPOT_CON_NAME' not found. Run 'dw wifi setup' first."
-        echo "client" > "$MODE_FILE"
-        exit 0
-    fi
-
-    # Scan for known WiFi with timeout
-    local elapsed=0
-    while [ $elapsed -lt $SCAN_TIMEOUT ]; do
-        local match
-        if match=$(check_for_known_wifi); then
-            activate_client_mode "$match"
-            exit 0
-        fi
-
-        log "No known WiFi found yet, retrying... (${elapsed}s/${SCAN_TIMEOUT}s)"
-        sleep $SCAN_INTERVAL
-        elapsed=$((elapsed + SCAN_INTERVAL))
-    done
-
-    # No known WiFi found after timeout — go to hotspot mode
-    activate_hotspot_mode
-}
-
-main "$@"
+#!/bin/bash
+#
+# Dune Weaver Autohotspot
+#
+# Two modes:
+#   Boot mode (no args):  Scans for known WiFi with 30s retry timeout.
+#   Check mode (--check): Quick single-pass check, runs every 60s via timer.
+#
+# Logic:
+#   - If connected to known WiFi → do nothing
+#   - If in hotspot mode and known WiFi appears → switch to client, reboot
+#   - If disconnected → scan once → connect or activate hotspot
+#
+# DNS redirect for captive portal is handled by NetworkManager's built-in
+# dnsmasq via config in /etc/NetworkManager/dnsmasq-shared.d/.
+#
+# Requires: NetworkManager (Pi Trixie)
+#
+
+set -euo pipefail
+
+HOTSPOT_CON_NAME="DuneWeaver-Hotspot"
+MODE_FILE="/tmp/dw-wifi-mode"
+SCAN_TIMEOUT=30
+SCAN_INTERVAL=5
+IFACE="wlan0"
+
+log() {
+    echo "[autohotspot] $*"
+    logger -t autohotspot "$*"
+}
+
+# Wait for NetworkManager to be ready
+wait_for_nm() {
+    local waited=0
+    while [ $waited -lt 10 ]; do
+        if nmcli general status &>/dev/null; then
+            return 0
+        fi
+        sleep 1
+        waited=$((waited + 1))
+    done
+    log "ERROR: NetworkManager not ready after 10s"
+    return 1
+}
+
+# Get list of saved WiFi connection names
+get_saved_wifi() {
+    nmcli -t -f NAME,TYPE con show | grep ':.*wireless' | cut -d: -f1 | grep -v "^${HOTSPOT_CON_NAME}$" || true
+}
+
+# Get current wlan0 connection state: "client", "hotspot", or "disconnected"
+get_current_state() {
+    local active
+    active=$(nmcli -t -f NAME,TYPE,DEVICE con show --active 2>/dev/null || true)
+
+    while IFS= read -r line; do
+        local name device
+        name=$(echo "$line" | cut -d: -f1)
+        device=$(echo "$line" | rev | cut -d: -f1 | rev)
+        if [ "$device" = "$IFACE" ]; then
+            if [ "$name" = "$HOTSPOT_CON_NAME" ]; then
+                echo "hotspot"
+                return
+            fi
+            echo "client"
+            return
+        fi
+    done <<< "$active"
+
+    echo "disconnected"
+}
+
+# Scan for available networks and check if any match saved connections
+check_for_known_wifi() {
+    # Trigger a fresh scan
+    nmcli dev wifi rescan ifname "$IFACE" 2>/dev/null || true
+    sleep 2
+
+    # Get available SSIDs
+    local available
+    available=$(nmcli -t -f SSID dev wifi list ifname "$IFACE" 2>/dev/null | sort -u | grep -v '^$' || true)
+
+    if [ -z "$available" ]; then
+        return 1
+    fi
+
+    # Get saved connections
+    local saved
+    saved=$(get_saved_wifi)
+
+    if [ -z "$saved" ]; then
+        return 1
+    fi
+
+    # Check if any saved connection matches an available network
+    while IFS= read -r saved_name; do
+        # Get the SSID associated with this saved connection
+        local saved_ssid
+        saved_ssid=$(nmcli -t -f connection.id,802-11-wireless.ssid con show "$saved_name" 2>/dev/null | grep '802-11-wireless.ssid' | cut -d: -f2 || true)
+
+        # If we couldn't get the SSID from connection details, use the connection name as SSID
+        if [ -z "$saved_ssid" ]; then
+            saved_ssid="$saved_name"
+        fi
+
+        if echo "$available" | grep -qxF "$saved_ssid"; then
+            echo "$saved_name"
+            return 0
+        fi
+    done <<< "$saved"
+
+    return 1
+}
+
+# Activate client mode with a known connection
+activate_client_mode() {
+    local con_name="$1"
+    log "Found known network, activating connection: $con_name"
+
+    # Deactivate hotspot if it's active
+    nmcli con down "$HOTSPOT_CON_NAME" 2>/dev/null || true
+
+    # Connect
+    if nmcli con up "$con_name" 2>/dev/null; then
+        local ip
+        ip=$(nmcli -t -f IP4.ADDRESS dev show "$IFACE" 2>/dev/null | head -1 | cut -d: -f2 | cut -d/ -f1 || echo "unknown")
+        log "Client mode active. Connected to '$con_name', IP: $ip"
+        echo "client" > "$MODE_FILE"
+        return 0
+    else
+        log "Failed to connect to '$con_name'"
+        return 1
+    fi
+}
+
+# Activate hotspot mode
+activate_hotspot_mode() {
+    log "No known WiFi found. Activating hotspot mode..."
+
+    # Make sure we're not connected to anything
+    nmcli dev disconnect "$IFACE" 2>/dev/null || true
+
+    # Activate the hotspot profile
+    # NM's shared mode starts built-in dnsmasq for DHCP+DNS.
+    # Our config in /etc/NetworkManager/dnsmasq-shared.d/ makes it
+    # redirect all DNS queries to 10.42.0.1 for captive portal detection.
+    if nmcli con up "$HOTSPOT_CON_NAME" 2>/dev/null; then
+        log "Hotspot active. SSID: $(nmcli -t -f 802-11-wireless.ssid con show "$HOTSPOT_CON_NAME" 2>/dev/null | cut -d: -f2)"
+        log "DNS redirect active via NM dnsmasq (captive portal mode)"
+        echo "hotspot" > "$MODE_FILE"
+        return 0
+    else
+        log "ERROR: Failed to activate hotspot"
+        return 1
+    fi
+}
+
+# Boot mode: scan with retry loop, then fallback to hotspot
+boot_mode() {
+    log "Boot mode: scanning for known WiFi (${SCAN_TIMEOUT}s timeout)..."
+
+    # Wait for NetworkManager
+    if ! wait_for_nm; then
+        log "NetworkManager not available, exiting"
+        exit 1
+    fi
+
+    # Check if hotspot profile exists
+    if ! nmcli con show "$HOTSPOT_CON_NAME" &>/dev/null; then
+        log "Hotspot profile '$HOTSPOT_CON_NAME' not found. Run 'dw wifi setup' first."
+        echo "client" > "$MODE_FILE"
+        exit 0
+    fi
+
+    # Scan for known WiFi with timeout
+    local elapsed=0
+    while [ $elapsed -lt $SCAN_TIMEOUT ]; do
+        local match
+        if match=$(check_for_known_wifi); then
+            activate_client_mode "$match"
+            exit 0
+        fi
+
+        log "No known WiFi found yet, retrying... (${elapsed}s/${SCAN_TIMEOUT}s)"
+        sleep $SCAN_INTERVAL
+        elapsed=$((elapsed + SCAN_INTERVAL))
+    done
+
+    # No known WiFi found after timeout — go to hotspot mode
+    activate_hotspot_mode
+}
+
+# Check mode: quick single-pass check for periodic timer
+check_mode() {
+    # Check if hotspot profile exists
+    if ! nmcli con show "$HOTSPOT_CON_NAME" &>/dev/null; then
+        exit 0
+    fi
+
+    local state
+    state=$(get_current_state)
+
+    case "$state" in
+        client)
+            # Already connected — nothing to do
+            ;;
+        hotspot)
+            # In hotspot mode — check if a known network has appeared
+            local match
+            if match=$(check_for_known_wifi); then
+                log "Known WiFi found while in hotspot mode, switching to client..."
+                activate_client_mode "$match"
+            fi
+            ;;
+        disconnected)
+            # WiFi dropped — try to reconnect or fall back to hotspot
+            log "WiFi disconnected, attempting recovery..."
+            local match
+            if match=$(check_for_known_wifi); then
+                activate_client_mode "$match"
+            else
+                activate_hotspot_mode
+            fi
+            ;;
+    esac
+}
+
+# Main
+case "${1:-}" in
+    --check)
+        check_mode
+        ;;
+    *)
+        boot_mode
+        ;;
+esac

+ 8 - 0
wifi/autohotspot-check.service

@@ -0,0 +1,8 @@
+[Unit]
+Description=Dune Weaver Autohotspot Check
+After=NetworkManager.service
+Wants=NetworkManager.service
+
+[Service]
+Type=oneshot
+ExecStart=/usr/local/bin/autohotspot --check

+ 11 - 0
wifi/autohotspot-check.timer

@@ -0,0 +1,11 @@
+[Unit]
+Description=Dune Weaver Autohotspot periodic check
+After=autohotspot.service
+
+[Timer]
+OnBootSec=60
+OnUnitActiveSec=60
+AccuracySec=10
+
+[Install]
+WantedBy=timers.target

+ 14 - 3
wifi/setup-wifi.sh

@@ -122,15 +122,22 @@ install_configs() {
     print_success "Autohotspot script installed"
 }
 
-# Install and enable the systemd service
+# Install and enable the systemd service and timer
 install_service() {
-    print_step "Installing autohotspot service..."
+    print_step "Installing autohotspot service and timer..."
 
+    # Boot service: runs once at startup with 30s scan
     sudo cp "$SCRIPT_DIR/autohotspot.service" /etc/systemd/system/autohotspot.service
+
+    # Periodic check: runs every 60s to detect WiFi drops and recover
+    sudo cp "$SCRIPT_DIR/autohotspot-check.service" /etc/systemd/system/autohotspot-check.service
+    sudo cp "$SCRIPT_DIR/autohotspot-check.timer" /etc/systemd/system/autohotspot-check.timer
+
     sudo systemctl daemon-reload
     sudo systemctl enable autohotspot.service
+    sudo systemctl enable --now autohotspot-check.timer
 
-    print_success "autohotspot.service installed and enabled"
+    print_success "autohotspot.service and autohotspot-check.timer installed and enabled"
 }
 
 # Main
@@ -154,6 +161,10 @@ main() {
     echo "  2. If found → connect normally"
     echo "  3. If not found → create a '$(nmcli -t -f 802-11-wireless.ssid con show "$HOTSPOT_CON_NAME" 2>/dev/null | cut -d: -f2 || echo "$HOTSPOT_SSID")' hotspot"
     echo ""
+    echo "A periodic check runs every 60 seconds to:"
+    echo "  - Fall back to hotspot if WiFi drops"
+    echo "  - Reconnect to known WiFi when it becomes available"
+    echo ""
     echo "Users can connect to the hotspot and open the Dune Weaver app"
     echo "to configure WiFi credentials."
     echo ""