Explorar o 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 hai 4 meses
pai
achega
05a95c9a95
Modificáronse 5 ficheiros con 277 adicións e 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():
 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"
     autohotspot_path = "/usr/local/bin/autohotspot"
     try:
     try:
         if os.path.exists(autohotspot_path):
         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(
             subprocess.Popen(
-                [autohotspot_path],
+                [autohotspot_path, "--check"],
                 stdout=subprocess.DEVNULL,
                 stdout=subprocess.DEVNULL,
                 stderr=subprocess.DEVNULL,
                 stderr=subprocess.DEVNULL,
             )
             )
         else:
         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...")
             logger.info("Autohotspot script not found, activating hotspot directly...")
             run_nmcli("dev", "disconnect", "wlan0")
             run_nmcli("dev", "disconnect", "wlan0")
             run_nmcli("con", "up", HOTSPOT_CON_NAME)
             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"
     print_success "Autohotspot script installed"
 }
 }
 
 
-# Install and enable the systemd service
+# Install and enable the systemd service and timer
 install_service() {
 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
     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 daemon-reload
     sudo systemctl enable autohotspot.service
     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
 # Main
@@ -154,6 +161,10 @@ main() {
     echo "  2. If found → connect normally"
     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 "  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 ""
+    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 "Users can connect to the hotspot and open the Dune Weaver app"
     echo "to configure WiFi credentials."
     echo "to configure WiFi credentials."
     echo ""
     echo ""