Feature/wifi setup improvements (#187)

* fix: Handle permission errors when removing plugin directories

- Added _safe_remove_directory() method to handle permission errors gracefully
- Fixes permissions on __pycache__ directories before removal
- Updates uninstall_plugin() and install methods to use safe removal
- Resolves [Errno 13] Permission denied errors during plugin install/uninstall

* feat(wifi): Add grace period for AP mode and improve setup documentation

- Add 90-second grace period (3 checks at 30s intervals) before enabling AP mode
- Change AP to open network (no password) for easier initial setup
- Add verification script for WiFi setup
- Update documentation with grace period details and open network info
- Improve WiFi monitor daemon logging and error handling

* feat(wifi): Add Trixie compatibility and dynamic interface discovery

- Add dynamic WiFi interface discovery instead of hardcoded wlan0
  - Supports traditional (wlan0), predictable (wlp2s0), and USB naming
  - Falls back gracefully if detection fails

- Add Raspberry Pi OS Trixie (Debian 13) detection and compatibility
  - Detect Netplan configuration and connection file locations
  - Disable PMF (Protected Management Frames) on Trixie for better
    client compatibility with certain WiFi adapters

- Improve nmcli hotspot setup for Trixie
  - Add explicit IP configuration (192.168.4.1/24)
  - Add channel configuration to hotspot creation
  - Handle Trixie's default 10.42.0.1 IP override

- Add dnsmasq conflict detection
  - Warn if Pi-hole or other DNS services are using dnsmasq
  - Create backup before overwriting config

- Improve error handling
  - Replace bare except clauses with specific exceptions
  - All subprocess calls now have explicit timeouts

- Document sudoers requirements in module docstring
  - List all required NOPASSWD entries for ledpi user

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(wifi): Use NM_CONNECTIONS_PATHS constant in _detect_trixie

Replace hardcoded Path instances with references to the
NM_CONNECTIONS_PATHS constant for consistency.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(verify): Use ETH_CONNECTED and AP_ACTIVE in summary output

Add connectivity summary section that displays Ethernet and AP mode
status using the previously unused ETH_CONNECTED and AP_ACTIVE flags.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Chuck <chuck@example.com>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chuck
2026-01-19 16:43:02 -05:00
committed by GitHub
parent 2381ead03f
commit 1833e30c1d
5 changed files with 771 additions and 121 deletions

View File

@@ -58,39 +58,69 @@ class WiFiMonitorDaemon:
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 @@ class WiFiMonitorDaemon:
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():

346
scripts/verify_wifi_setup.sh Executable file
View File

@@ -0,0 +1,346 @@
#!/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 ""
# Show connectivity summary
echo "=== Connectivity ==="
if [ "$ETH_CONNECTED" = true ]; then
info "Ethernet: Connected"
else
info "Ethernet: Not connected"
fi
if [ "$AP_ACTIVE" = true ]; then
info "AP Mode: Active"
else
info "AP Mode: Inactive"
fi
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