diff --git a/docs/WIFI_SETUP.md b/docs/WIFI_SETUP.md index 08966620..3415f829 100644 --- a/docs/WIFI_SETUP.md +++ b/docs/WIFI_SETUP.md @@ -51,10 +51,9 @@ You should see output indicating the service is active and running. ### Accessing the WiFi Setup Interface -1. **If WiFi is NOT connected**: The Raspberry Pi will automatically create an access point - - Connect to the WiFi network: **LEDMatrix-Setup** - - Password: **ledmatrix123** (default) - - Open a web browser and navigate to: `http://192.168.4.1:5000` +1. **If WiFi is NOT connected**: The Raspberry Pi will automatically create an access point (after a 90-second grace period) + - Connect to the WiFi network: **LEDMatrix-Setup** (open network, no password required) + - Open a web browser and navigate to: `http://192.168.4.1:5000` or `http://192.168.4.1` (captive portal may redirect) - Or use the IP address shown in the web interface 2. **If WiFi IS connected**: Access the web interface normally @@ -92,7 +91,7 @@ The WiFi monitor daemon (`wifi_monitor_daemon.py`) runs as a background service 3. Automatically disables AP mode when WiFi or Ethernet connection is established 4. Logs all state changes for troubleshooting -**Note**: By default, `auto_enable_ap_mode` is `true`, meaning AP mode will automatically activate when both WiFi and Ethernet are disconnected. This ensures you can always configure the device even when it has no network connection. +**Note**: By default, `auto_enable_ap_mode` is `true`, meaning AP mode will automatically activate when both WiFi and Ethernet are disconnected. However, there's a 90-second grace period (3 consecutive checks at 30-second intervals) to prevent AP mode from enabling on transient network hiccups. This ensures you can always configure the device even when it has no network connection. ### WiFi Manager Module @@ -125,13 +124,14 @@ WiFi settings are stored in `config/wifi_config.json`: **Configuration Options:** - `ap_ssid`: SSID for the access point (default: "LEDMatrix-Setup") -- `ap_password`: Password for the access point (default: "ledmatrix123") - `ap_channel`: WiFi channel for AP mode (default: 7) - `auto_enable_ap_mode`: Automatically enable AP mode when WiFi/Ethernet disconnect (default: `true`) - - When `true`: AP mode automatically enables when both WiFi and Ethernet are disconnected + - When `true`: AP mode automatically enables after a 90-second grace period when both WiFi and Ethernet are disconnected - When `false`: AP mode must be manually enabled through the web interface - `saved_networks`: List of saved WiFi network credentials +**Note**: The access point is configured as an open network (no password required) for ease of initial setup. This allows any device to connect without credentials. + ### Access Point Configuration The AP mode uses `hostapd` and `dnsmasq` for access point functionality: @@ -141,6 +141,31 @@ The AP mode uses `hostapd` and `dnsmasq` for access point functionality: - **Gateway**: 192.168.4.1 - **Channel**: 7 (configurable) +## Verification + +### Running the WiFi Verification Script + +Use the comprehensive verification script to check your WiFi setup: + +```bash +cd /home/ledpi/LEDMatrix +./scripts/verify_wifi_setup.sh +``` + +This script checks: +- Required packages are installed +- WiFi monitor service is running +- Configuration files are valid +- WiFi permissions are configured +- WiFi interface is available +- WiFi radio status +- Current connection status +- AP mode status +- WiFi Manager module availability +- Web interface API accessibility + +The script provides a summary with passed/warning/failed checks to help diagnose issues. + ## Troubleshooting ### WiFi Monitor Service Not Starting @@ -254,12 +279,12 @@ sudo systemctl restart ledmatrix-wifi-monitor ## Security Considerations -- **Default AP Password**: The default AP password is "ledmatrix123". Consider changing this in `config/wifi_config.json` for production use +- **Open AP Network**: The access point is configured as an open network (no password) for ease of initial setup. This allows any device within range to connect to the setup network. Consider your deployment environment when using this feature. - **WiFi Credentials**: Saved WiFi credentials are stored in `config/wifi_config.json`. Ensure proper file permissions: ```bash sudo chmod 600 config/wifi_config.json ``` -- **Network Access**: When in AP mode, anyone within range can connect to the setup network. Use strong passwords for production deployments +- **Network Access**: When in AP mode, anyone within range can connect to the setup network. This is by design to allow easy initial configuration. For production deployments in secure environments, consider using the web interface when connected to WiFi instead. ## API Endpoints @@ -291,11 +316,13 @@ The system supports multiple scanning methods: AP mode configuration: -- Uses `hostapd` for WiFi access point functionality -- Uses `dnsmasq` for DHCP and DNS services +- Uses `hostapd` (preferred) or `nmcli hotspot` (fallback) for WiFi access point functionality +- Uses `dnsmasq` for DHCP and DNS services (hostapd mode only) - Configures wlan0 interface in AP mode - Provides DHCP range: 192.168.4.2-20 - Gateway IP: 192.168.4.1 +- **Open network**: No password required (configures as open network for easy setup) +- Captive portal: DNS redirection for automatic browser redirects (hostapd mode only) ## Development diff --git a/scripts/utils/wifi_monitor_daemon.py b/scripts/utils/wifi_monitor_daemon.py index 78481f29..6e58aaab 100755 --- a/scripts/utils/wifi_monitor_daemon.py +++ b/scripts/utils/wifi_monitor_daemon.py @@ -58,39 +58,69 @@ def run(self): logger.info("WiFi Monitor Daemon started") logger.info(f"Check interval: {self.check_interval} seconds") + # Log initial configuration + auto_enable = self.wifi_manager.config.get("auto_enable_ap_mode", True) + ap_ssid = self.wifi_manager.config.get("ap_ssid", "LEDMatrix-Setup") + logger.info(f"Configuration: auto_enable_ap_mode={auto_enable}, ap_ssid={ap_ssid}") + + # Log initial status + initial_status = self.wifi_manager.get_wifi_status() + initial_ethernet = self.wifi_manager._is_ethernet_connected() + logger.info(f"Initial status: WiFi connected={initial_status.connected}, " + f"Ethernet connected={initial_ethernet}, AP active={initial_status.ap_mode_active}") + if initial_status.connected: + logger.info(f" WiFi SSID: {initial_status.ssid}, IP: {initial_status.ip_address}, Signal: {initial_status.signal}%") + while self.running: try: + # Get current status before checking + status = self.wifi_manager.get_wifi_status() + ethernet_connected = self.wifi_manager._is_ethernet_connected() + # Check WiFi status and manage AP mode state_changed = self.wifi_manager.check_and_manage_ap_mode() - # Get current status for logging - status = self.wifi_manager.get_wifi_status() - ethernet_connected = self.wifi_manager._is_ethernet_connected() + # Get updated status after check + updated_status = self.wifi_manager.get_wifi_status() + updated_ethernet = self.wifi_manager._is_ethernet_connected() + current_state = { - 'connected': status.connected, - 'ethernet_connected': ethernet_connected, - 'ap_active': status.ap_mode_active, - 'ssid': status.ssid + 'connected': updated_status.connected, + 'ethernet_connected': updated_ethernet, + 'ap_active': updated_status.ap_mode_active, + 'ssid': updated_status.ssid } - # Log state changes + # Log state changes with detailed information if current_state != self.last_state: - if status.connected: - logger.info(f"WiFi connected: {status.ssid} (IP: {status.ip_address})") + logger.info("=== State Change Detected ===") + if updated_status.connected: + logger.info(f"WiFi connected: {updated_status.ssid} (IP: {updated_status.ip_address}, Signal: {updated_status.signal}%)") else: - logger.info("WiFi disconnected") + logger.info("WiFi disconnected (no active connection)") - if ethernet_connected: + if updated_ethernet: logger.info("Ethernet connected") else: - logger.debug("Ethernet disconnected") + logger.debug("Ethernet not connected") - if status.ap_mode_active: - logger.info("AP mode active") + if updated_status.ap_mode_active: + logger.info(f"AP mode ACTIVE - SSID: {ap_ssid} (IP: 192.168.4.1)") else: logger.debug("AP mode inactive") + if state_changed: + logger.info("AP mode state was changed by check_and_manage_ap_mode()") + + logger.info("=============================") self.last_state = current_state.copy() + else: + # Log periodic status (less verbose) + if updated_status.connected: + logger.debug(f"Status check: WiFi={updated_status.ssid} ({updated_status.signal}%), " + f"Ethernet={updated_ethernet}, AP={updated_status.ap_mode_active}") + else: + logger.debug(f"Status check: WiFi=disconnected, Ethernet={updated_ethernet}, AP={updated_status.ap_mode_active}") # Sleep until next check time.sleep(self.check_interval) @@ -101,23 +131,40 @@ def run(self): break except Exception as e: logger.error(f"Error in monitor loop: {e}", exc_info=True) + logger.error(f"Error details - type: {type(e).__name__}, args: {e.args}") + # Log current state for debugging + try: + error_status = self.wifi_manager.get_wifi_status() + logger.error(f"State at error: WiFi={error_status.connected}, AP={error_status.ap_mode_active}") + except Exception as state_error: + logger.error(f"Could not get state at error: {state_error}") # Continue running even if there's an error time.sleep(self.check_interval) logger.info("WiFi Monitor Daemon stopped") # Ensure AP mode is disabled on shutdown if WiFi or Ethernet is connected + logger.info("Performing cleanup on shutdown...") try: status = self.wifi_manager.get_wifi_status() ethernet_connected = self.wifi_manager._is_ethernet_connected() + logger.info(f"Final status: WiFi={status.connected}, Ethernet={ethernet_connected}, AP={status.ap_mode_active}") + if (status.connected or ethernet_connected) and status.ap_mode_active: if status.connected: - logger.info("Disabling AP mode on shutdown (WiFi is connected)") + logger.info(f"Disabling AP mode on shutdown (WiFi is connected to {status.ssid})") elif ethernet_connected: logger.info("Disabling AP mode on shutdown (Ethernet is connected)") - self.wifi_manager.disable_ap_mode() + + success, message = self.wifi_manager.disable_ap_mode() + if success: + logger.info(f"AP mode disabled successfully: {message}") + else: + logger.warning(f"Failed to disable AP mode: {message}") + else: + logger.debug("AP mode cleanup not needed (not active or no network connection)") except Exception as e: - logger.error(f"Error disabling AP mode on shutdown: {e}") + logger.error(f"Error during shutdown cleanup: {e}", exc_info=True) def main(): diff --git a/scripts/verify_wifi_setup.sh b/scripts/verify_wifi_setup.sh new file mode 100755 index 00000000..2816a582 --- /dev/null +++ b/scripts/verify_wifi_setup.sh @@ -0,0 +1,332 @@ +#!/bin/bash +# WiFi Setup Verification Script +# Comprehensive health check for WiFi management system + +set -u # Fail on undefined variables + +echo "==========================================" +echo "WiFi Setup Verification" +echo "==========================================" +echo "" + +# Colors +GREEN='\033[0;32m' +RED='\033[0;31m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Counters +PASSED=0 +FAILED=0 +WARNINGS=0 + +check_pass() { + echo -e "${GREEN}✓${NC} $1" + PASSED=$((PASSED + 1)) +} + +check_fail() { + echo -e "${RED}✗${NC} $1" + FAILED=$((FAILED + 1)) +} + +check_warn() { + echo -e "${YELLOW}⚠${NC} $1" + WARNINGS=$((WARNINGS + 1)) +} + +info() { + echo -e "${BLUE}ℹ${NC} $1" +} + +# Determine project root +if [ -f "run.py" ]; then + PROJECT_ROOT="$(pwd)" +elif [ -f "../run.py" ]; then + PROJECT_ROOT="$(cd .. && pwd)" +else + echo "Error: Could not find project root. Please run this script from the LEDMatrix directory." + exit 1 +fi + +echo "Project root: $PROJECT_ROOT" +echo "" + +# 1. Check required packages +echo "=== Required Packages ===" +PACKAGES=("nmcli" "hostapd" "dnsmasq") +MISSING_PACKAGES=() + +for pkg in "${PACKAGES[@]}"; do + if command -v "$pkg" >/dev/null 2>&1; then + check_pass "$pkg is installed" + else + check_fail "$pkg is NOT installed" + MISSING_PACKAGES+=("$pkg") + fi +done + +if [ ${#MISSING_PACKAGES[@]} -gt 0 ]; then + echo "" + info "To install missing packages:" + echo " sudo apt update && sudo apt install -y ${MISSING_PACKAGES[*]}" +fi +echo "" + +# 2. Check WiFi monitor service +echo "=== WiFi Monitor Service ===" +if systemctl list-unit-files | grep -q "ledmatrix-wifi-monitor.service"; then + check_pass "WiFi monitor service is installed" + + if systemctl is-enabled --quiet ledmatrix-wifi-monitor.service 2>/dev/null; then + check_pass "WiFi monitor service is enabled" + else + check_warn "WiFi monitor service is installed but not enabled" + info "To enable: sudo systemctl enable ledmatrix-wifi-monitor.service" + fi + + if systemctl is-active --quiet ledmatrix-wifi-monitor.service 2>/dev/null; then + check_pass "WiFi monitor service is running" + else + check_warn "WiFi monitor service is not running" + info "To start: sudo systemctl start ledmatrix-wifi-monitor.service" + info "Check logs: sudo journalctl -u ledmatrix-wifi-monitor -n 50" + fi +else + check_fail "WiFi monitor service is NOT installed" + info "To install: sudo $PROJECT_ROOT/scripts/install/install_wifi_monitor.sh" +fi +echo "" + +# 3. Check WiFi configuration file +echo "=== Configuration Files ===" +WIFI_CONFIG="$PROJECT_ROOT/config/wifi_config.json" +if [ -f "$WIFI_CONFIG" ]; then + check_pass "WiFi config file exists: $WIFI_CONFIG" + + # Check if JSON is valid + if python3 -m json.tool "$WIFI_CONFIG" >/dev/null 2>&1; then + check_pass "WiFi config file is valid JSON" + + # Check for required fields + if grep -q "ap_ssid" "$WIFI_CONFIG"; then + AP_SSID=$(python3 -c "import json; print(json.load(open('$WIFI_CONFIG')).get('ap_ssid', 'N/A'))" 2>/dev/null) + info "AP SSID: $AP_SSID" + else + check_warn "ap_ssid not found in config" + fi + + if grep -q "auto_enable_ap_mode" "$WIFI_CONFIG"; then + AUTO_ENABLE=$(python3 -c "import json; print(json.load(open('$WIFI_CONFIG')).get('auto_enable_ap_mode', 'N/A'))" 2>/dev/null) + info "Auto-enable AP mode: $AUTO_ENABLE" + else + check_warn "auto_enable_ap_mode not found in config" + fi + else + check_fail "WiFi config file is NOT valid JSON" + fi +else + check_warn "WiFi config file does not exist (will be created on first use)" +fi +echo "" + +# 4. Check WiFi permissions +echo "=== WiFi Permissions ===" +if [ -f "/etc/sudoers.d/ledmatrix_wifi" ]; then + check_pass "WiFi sudoers file exists" + + # Check if file is readable + if sudo -n test -r "/etc/sudoers.d/ledmatrix_wifi" 2>/dev/null; then + check_pass "WiFi sudoers file is readable" + else + check_warn "WiFi sudoers file may not be readable" + fi +else + check_warn "WiFi sudoers file does not exist" + info "To configure: $PROJECT_ROOT/scripts/install/configure_wifi_permissions.sh" +fi + +if [ -f "/etc/polkit-1/rules.d/10-ledmatrix-wifi.rules" ]; then + check_pass "WiFi PolicyKit rule exists" +else + check_warn "WiFi PolicyKit rule does not exist" + info "To configure: $PROJECT_ROOT/scripts/install/configure_wifi_permissions.sh" +fi +echo "" + +# 5. Check WiFi interface +echo "=== WiFi Interface ===" +if ip link show wlan0 >/dev/null 2>&1; then + check_pass "WiFi interface wlan0 exists" + + # Check if interface is up + if ip link show wlan0 | grep -q "state UP"; then + check_pass "WiFi interface wlan0 is UP" + else + check_warn "WiFi interface wlan0 is DOWN" + fi +else + check_fail "WiFi interface wlan0 does NOT exist" + info "Check if WiFi adapter is connected (USB WiFi or built-in)" +fi +echo "" + +# 6. Check WiFi radio status +echo "=== WiFi Radio Status ===" +if command -v nmcli >/dev/null 2>&1; then + WIFI_RADIO=$(nmcli radio wifi 2>/dev/null || echo "unknown") + if echo "$WIFI_RADIO" | grep -qi "enabled"; then + check_pass "WiFi radio is enabled" + elif echo "$WIFI_RADIO" | grep -qi "disabled"; then + check_warn "WiFi radio is disabled" + info "To enable: sudo nmcli radio wifi on" + else + check_warn "WiFi radio status unknown: $WIFI_RADIO" + fi +elif command -v rfkill >/dev/null 2>&1; then + RFKILL_WIFI=$(rfkill list wifi 2>/dev/null || echo "") + if echo "$RFKILL_WIFI" | grep -q "Soft blocked: yes"; then + check_warn "WiFi is soft-blocked" + info "To unblock: sudo rfkill unblock wifi" + elif echo "$RFKILL_WIFI" | grep -q "Hard blocked: yes"; then + check_fail "WiFi is hard-blocked (hardware switch)" + else + check_pass "WiFi is not blocked" + fi +else + check_warn "Cannot check WiFi radio status (nmcli and rfkill not available)" +fi +echo "" + +# 7. Check current WiFi connection +echo "=== Current WiFi Status ===" +if command -v nmcli >/dev/null 2>&1; then + WIFI_STATUS=$(nmcli -t -f DEVICE,TYPE,STATE device status 2>/dev/null | grep -E "wifi|wlan0" || echo "") + if echo "$WIFI_STATUS" | grep -q "connected"; then + SSID=$(nmcli -t -f active,ssid device wifi 2>/dev/null | grep "^yes:" | cut -d: -f2 | head -1) + IP=$(nmcli -t -f IP4.ADDRESS device show wlan0 2>/dev/null | cut -d: -f2 | cut -d/ -f1 | head -1) + SIGNAL=$(nmcli -t -f WIFI.SIGNAL device show wlan0 2>/dev/null | cut -d: -f2 | head -1) + check_pass "WiFi is connected" + info "SSID: $SSID" + info "IP Address: $IP" + info "Signal: $SIGNAL%" + else + check_warn "WiFi is not connected" + fi +elif command -v iwconfig >/dev/null 2>&1; then + if iwconfig wlan0 2>/dev/null | grep -q "ESSID:"; then + SSID=$(iwconfig wlan0 2>/dev/null | grep -oP 'ESSID:"\K[^"]*') + check_pass "WiFi is connected to: $SSID" + else + check_warn "WiFi is not connected" + fi +else + check_warn "Cannot check WiFi connection status" +fi +echo "" + +# 8. Check Ethernet connection +echo "=== Ethernet Status ===" +ETH_CONNECTED=false +if command -v nmcli >/dev/null 2>&1; then + ETH_STATUS=$(nmcli -t -f DEVICE,TYPE,STATE device status 2>/dev/null | grep -E "ethernet|eth" | grep "connected" || echo "") + if [ -n "$ETH_STATUS" ]; then + ETH_CONNECTED=true + check_pass "Ethernet is connected" + else + check_warn "Ethernet is not connected" + fi +elif command -v ip >/dev/null 2>&1; then + if ip addr show eth0 2>/dev/null | grep -q "inet " || ip addr show 2>/dev/null | grep -E "eth|enp" | grep -q "inet "; then + ETH_CONNECTED=true + check_pass "Ethernet appears to be connected" + else + check_warn "Ethernet does not appear to be connected" + fi +fi +echo "" + +# 9. Check AP mode status +echo "=== AP Mode Status ===" +AP_ACTIVE=false + +# Check hostapd +if systemctl is-active --quiet hostapd 2>/dev/null; then + AP_ACTIVE=true + check_warn "AP mode is ACTIVE (hostapd running)" + info "SSID: LEDMatrix-Setup (from config)" + info "IP: 192.168.4.1" +elif systemctl is-active --quiet dnsmasq 2>/dev/null; then + # dnsmasq might be running for other purposes, check if it's configured for AP + if grep -q "interface=wlan0" /etc/dnsmasq.conf 2>/dev/null; then + AP_ACTIVE=true + check_warn "AP mode appears to be active (dnsmasq configured for wlan0)" + fi +fi + +# Check nmcli hotspot +if command -v nmcli >/dev/null 2>&1; then + HOTSPOT=$(nmcli -t -f NAME,TYPE connection show --active 2>/dev/null | grep -i hotspot || echo "") + if [ -n "$HOTSPOT" ]; then + AP_ACTIVE=true + CONN_NAME=$(echo "$HOTSPOT" | cut -d: -f1) + check_warn "AP mode is ACTIVE (nmcli hotspot: $CONN_NAME)" + info "IP: 192.168.4.1" + fi +fi + +if [ "$AP_ACTIVE" = false ]; then + check_pass "AP mode is not active" +fi +echo "" + +# 10. Check WiFi Manager Python module +echo "=== WiFi Manager Module ===" +if python3 -c "from src.wifi_manager import WiFiManager" 2>/dev/null; then + check_pass "WiFi Manager module can be imported" + + # Try to instantiate (but don't fail if it errors - may need config) + if python3 -c "import sys; sys.path.insert(0, '$PROJECT_ROOT'); from src.wifi_manager import WiFiManager; wm = WiFiManager(); print('OK')" 2>/dev/null; then + check_pass "WiFi Manager can be instantiated" + else + check_warn "WiFi Manager instantiation failed (may be expected)" + fi +else + check_fail "WiFi Manager module cannot be imported" +fi +echo "" + +# 11. Check web interface WiFi API +echo "=== Web Interface WiFi API ===" +if systemctl is-active --quiet ledmatrix-web.service 2>/dev/null; then + # Try to test the WiFi status API endpoint + if curl -s -f "http://localhost:5001/api/v3/wifi/status" >/dev/null 2>&1; then + check_pass "WiFi status API endpoint is accessible" + else + check_warn "WiFi status API endpoint is not accessible (may be expected if web interface requires auth)" + fi +else + check_warn "Web interface service is not running (cannot test API)" +fi +echo "" + +# Summary +echo "==========================================" +echo "Summary" +echo "==========================================" +echo -e "${GREEN}Passed: $PASSED${NC}" +echo -e "${YELLOW}Warnings: $WARNINGS${NC}" +echo -e "${RED}Failed: $FAILED${NC}" +echo "" + +if [ $FAILED -eq 0 ] && [ $WARNINGS -eq 0 ]; then + echo -e "${GREEN}✓ All checks passed! WiFi setup looks good.${NC}" + exit 0 +elif [ $FAILED -eq 0 ]; then + echo -e "${YELLOW}⚠ Setup looks mostly good, but there are some warnings.${NC}" + exit 0 +else + echo -e "${RED}✗ Some checks failed. Please review the issues above.${NC}" + exit 1 +fi diff --git a/src/plugin_system/store_manager.py b/src/plugin_system/store_manager.py index e49a1418..3062c0c0 100644 --- a/src/plugin_system/store_manager.py +++ b/src/plugin_system/store_manager.py @@ -1415,10 +1415,10 @@ def _safe_remove_directory(self, path: Path) -> bool: try: # Try to fix permissions on __pycache__ directories recursively import stat - for root, _dirs, files in os.walk(path): + for root, dirs, files in os.walk(path): root_path = Path(root) try: - # Make directory writable (0o777 is acceptable here - temporary before deletion) + # Make directory writable os.chmod(root_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO) except (OSError, PermissionError): pass @@ -1436,7 +1436,7 @@ def _safe_remove_directory(self, path: Path) -> bool: self.logger.info(f"Successfully removed {path} after fixing permissions") return True except Exception as e2: - self.logger.exception(f"Failed to remove {path} even after fixing permissions: {e2}") + self.logger.error(f"Failed to remove {path} even after fixing permissions: {e2}") # Last resort: try with ignore_errors try: shutil.rmtree(path, ignore_errors=True) @@ -1448,10 +1448,10 @@ def _safe_remove_directory(self, path: Path) -> bool: self.logger.error(f"Could not remove {path} even with ignore_errors") return False except Exception as e3: - self.logger.exception(f"Final removal attempt failed for {path}: {e3}") + self.logger.error(f"Final removal attempt failed for {path}: {e3}") return False except Exception as e: - self.logger.exception(f"Unexpected error removing {path}: {e}") + self.logger.error(f"Unexpected error removing {path}: {e}") return False def _find_plugin_path(self, plugin_id: str) -> Optional[Path]: