Initial commit - Phase 3/4
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
"""WiFi manager for network configuration and mode switching."""
|
||||
import asyncio
|
||||
import logging
|
||||
import re
|
||||
import subprocess
|
||||
from dataclasses import dataclass
|
||||
@@ -9,6 +10,12 @@ from pathlib import Path
|
||||
|
||||
from .. import config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Persistent storage for WiFi mode
|
||||
WIFI_MODE_FILE = Path("/opt/dangerous-pi/data/wifi_mode")
|
||||
WIFI_DATA_DIR = Path("/opt/dangerous-pi/data")
|
||||
|
||||
|
||||
class WiFiMode(str, Enum):
|
||||
"""WiFi operation modes."""
|
||||
@@ -29,6 +36,7 @@ class WiFiInterface:
|
||||
connected: bool
|
||||
ssid: Optional[str] = None
|
||||
ip_address: Optional[str] = None
|
||||
mode: str = "managed" # "AP" or "managed" (client)
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -62,6 +70,68 @@ class WiFiManager:
|
||||
self._current_mode = WiFiMode.AUTO
|
||||
self._interfaces: List[WiFiInterface] = []
|
||||
self._supports_dual = False
|
||||
self._startup_applied = False
|
||||
|
||||
def _save_mode(self, mode: WiFiMode) -> bool:
|
||||
"""Save WiFi mode to persistent storage.
|
||||
|
||||
Args:
|
||||
mode: WiFi mode to save
|
||||
|
||||
Returns:
|
||||
True if saved successfully
|
||||
"""
|
||||
try:
|
||||
# Ensure data directory exists
|
||||
WIFI_DATA_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# Write mode to file
|
||||
WIFI_MODE_FILE.write_text(mode.value)
|
||||
logger.info(f"WiFi mode saved: {mode.value}")
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to save WiFi mode: {e}")
|
||||
return False
|
||||
|
||||
def _load_mode(self) -> Optional[WiFiMode]:
|
||||
"""Load WiFi mode from persistent storage.
|
||||
|
||||
Returns:
|
||||
Saved WiFi mode or None if not found
|
||||
"""
|
||||
try:
|
||||
if WIFI_MODE_FILE.exists():
|
||||
mode_str = WIFI_MODE_FILE.read_text().strip()
|
||||
mode = WiFiMode(mode_str)
|
||||
logger.info(f"WiFi mode loaded: {mode.value}")
|
||||
return mode
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to load WiFi mode: {e}")
|
||||
return None
|
||||
|
||||
async def apply_saved_mode(self) -> bool:
|
||||
"""Apply the saved WiFi mode on startup.
|
||||
|
||||
This should be called once during service startup to restore
|
||||
the previously configured WiFi mode.
|
||||
|
||||
Returns:
|
||||
True if mode was applied successfully
|
||||
"""
|
||||
if self._startup_applied:
|
||||
logger.debug("Saved mode already applied, skipping")
|
||||
return True
|
||||
|
||||
saved_mode = self._load_mode()
|
||||
if saved_mode:
|
||||
logger.info(f"Applying saved WiFi mode: {saved_mode.value}")
|
||||
success = await self.set_mode(saved_mode, persist=False) # Don't re-save
|
||||
self._startup_applied = True
|
||||
return success
|
||||
else:
|
||||
logger.info("No saved WiFi mode found, using default (AUTO)")
|
||||
self._startup_applied = True
|
||||
return True
|
||||
|
||||
async def detect_interfaces(self) -> List[WiFiInterface]:
|
||||
"""Detect available WiFi interfaces.
|
||||
@@ -79,6 +149,15 @@ class WiFiManager:
|
||||
return interfaces
|
||||
|
||||
# Parse iw dev output
|
||||
# Example output:
|
||||
# phy#0
|
||||
# Interface wlan0
|
||||
# ifindex 2
|
||||
# wdev 0x1
|
||||
# addr 2c:cf:67:87:db:13
|
||||
# ssid Dangerous-Pi
|
||||
# type AP
|
||||
# channel 6 (2437 MHz), width: 20 MHz
|
||||
current_interface = None
|
||||
for line in result.split('\n'):
|
||||
line = line.strip()
|
||||
@@ -95,7 +174,8 @@ class WiFiManager:
|
||||
'is_up': False,
|
||||
'connected': False,
|
||||
'ssid': None,
|
||||
'ip_address': None
|
||||
'ip_address': None,
|
||||
'mode': 'managed' # Default to managed (client) mode
|
||||
}
|
||||
|
||||
elif current_interface and line.startswith('addr '):
|
||||
@@ -103,7 +183,14 @@ class WiFiManager:
|
||||
|
||||
elif current_interface and line.startswith('ssid '):
|
||||
current_interface['ssid'] = ' '.join(line.split()[1:])
|
||||
current_interface['connected'] = True
|
||||
|
||||
elif current_interface and line.startswith('type '):
|
||||
# Parse interface type: "AP" or "managed"
|
||||
iface_type = line.split()[1]
|
||||
current_interface['mode'] = iface_type
|
||||
# Only mark as "connected" if in managed (client) mode with SSID
|
||||
if iface_type == 'managed' and current_interface.get('ssid'):
|
||||
current_interface['connected'] = True
|
||||
|
||||
if current_interface:
|
||||
interfaces.append(current_interface)
|
||||
@@ -117,8 +204,9 @@ class WiFiManager:
|
||||
if iface_dict['is_up']:
|
||||
iface_dict['ip_address'] = await self._get_interface_ip(iface_dict['name'])
|
||||
|
||||
# Create WiFiInterface object
|
||||
wifi_iface = WiFiInterface(**iface_dict)
|
||||
# For AP mode, mark connected if we have IP (AP is "active")
|
||||
if iface_dict['mode'] == 'AP' and iface_dict['ip_address']:
|
||||
iface_dict['connected'] = True
|
||||
|
||||
# Convert dicts to WiFiInterface objects
|
||||
self._interfaces = [WiFiInterface(**iface) for iface in interfaces]
|
||||
@@ -206,28 +294,35 @@ class WiFiManager:
|
||||
ap_ip = None
|
||||
|
||||
for iface in self._interfaces:
|
||||
if iface.connected and iface.ssid:
|
||||
# Check for AP mode interface
|
||||
if iface.mode == 'AP':
|
||||
ap_ip = iface.ip_address
|
||||
ap_ssid = iface.ssid # SSID from iw dev output
|
||||
# If no SSID from iw, try hostapd config
|
||||
if not ap_ssid:
|
||||
ap_ssid = await self._get_ap_ssid()
|
||||
|
||||
# Check for client mode interface that's connected
|
||||
elif iface.mode == 'managed' and iface.connected and iface.ssid:
|
||||
client_ssid = iface.ssid
|
||||
client_ip = iface.ip_address
|
||||
|
||||
# Check if running as AP (typically 10.3.141.1)
|
||||
if iface.ip_address and iface.ip_address.startswith("10.3.141"):
|
||||
ap_ip = iface.ip_address
|
||||
# Try to get AP SSID from hostapd
|
||||
ap_ssid = await self._get_ap_ssid()
|
||||
# Fallback: try to get AP SSID from hostapd if we detected AP mode
|
||||
if current_mode == WiFiMode.AP and not ap_ssid:
|
||||
ap_ssid = await self._get_ap_ssid()
|
||||
|
||||
return WiFiStatus(
|
||||
mode=current_mode,
|
||||
interfaces=self._interfaces,
|
||||
current_ssid=client_ssid,
|
||||
current_ip=client_ip,
|
||||
ap_ssid=ap_ssid or "raspi-webgui", # Default from existing setup
|
||||
ap_ip=ap_ip or "10.3.141.1",
|
||||
ap_ssid=ap_ssid or "Dangerous-Pi", # Default
|
||||
ap_ip=ap_ip,
|
||||
supports_dual=self._supports_dual
|
||||
)
|
||||
|
||||
async def _detect_current_mode(self) -> WiFiMode:
|
||||
"""Detect current WiFi mode.
|
||||
"""Detect current WiFi mode based on interface types.
|
||||
|
||||
Returns:
|
||||
Current WiFi mode
|
||||
@@ -235,14 +330,16 @@ class WiFiManager:
|
||||
if not self._interfaces:
|
||||
return WiFiMode.OFF
|
||||
|
||||
# Check if any interface is in AP mode
|
||||
has_ap = any(
|
||||
iface.ip_address and iface.ip_address.startswith("10.3.141")
|
||||
# Check interface modes from iw dev output
|
||||
has_ap = any(iface.mode == 'AP' for iface in self._interfaces)
|
||||
has_client = any(
|
||||
iface.mode == 'managed' and iface.connected
|
||||
for iface in self._interfaces
|
||||
)
|
||||
|
||||
# Check if any interface is connected as client
|
||||
has_client = any(iface.connected for iface in self._interfaces)
|
||||
# Also check if hostapd is running as backup detection
|
||||
if not has_ap:
|
||||
has_ap = await self._is_hostapd_running()
|
||||
|
||||
if has_ap and has_client:
|
||||
return WiFiMode.DUAL
|
||||
@@ -250,8 +347,25 @@ class WiFiManager:
|
||||
return WiFiMode.AP
|
||||
elif has_client:
|
||||
return WiFiMode.CLIENT
|
||||
elif any(iface.is_up for iface in self._interfaces):
|
||||
return WiFiMode.AUTO # Interface up but not in AP or connected
|
||||
else:
|
||||
return WiFiMode.AUTO
|
||||
return WiFiMode.OFF
|
||||
|
||||
async def _is_hostapd_running(self) -> bool:
|
||||
"""Check if hostapd service is running.
|
||||
|
||||
Returns:
|
||||
True if hostapd is active
|
||||
"""
|
||||
try:
|
||||
result = await self._run_command(
|
||||
"systemctl is-active hostapd",
|
||||
check=False
|
||||
)
|
||||
return result.strip() == "active"
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
async def _get_ap_ssid(self) -> Optional[str]:
|
||||
"""Get AP SSID from hostapd config.
|
||||
@@ -277,6 +391,10 @@ class WiFiManager:
|
||||
Returns:
|
||||
List of available networks
|
||||
"""
|
||||
# Ensure interfaces are detected
|
||||
if not self._interfaces:
|
||||
await self.detect_interfaces()
|
||||
|
||||
if not interface:
|
||||
# Use first non-USB interface, or first available
|
||||
for iface in self._interfaces:
|
||||
@@ -292,13 +410,14 @@ class WiFiManager:
|
||||
|
||||
try:
|
||||
# Request scan
|
||||
await self._run_command(f"sudo iw dev {interface} scan trigger", check=False)
|
||||
# Note: Requires CAP_NET_ADMIN capability (set via systemd service)
|
||||
await self._run_command(f"iw dev {interface} scan trigger", check=False)
|
||||
|
||||
# Wait for scan to complete
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Get scan results
|
||||
result = await self._run_command(f"sudo iw dev {interface} scan")
|
||||
result = await self._run_command(f"iw dev {interface} scan")
|
||||
|
||||
networks = []
|
||||
current_network = None
|
||||
@@ -306,11 +425,13 @@ class WiFiManager:
|
||||
for line in result.split('\n'):
|
||||
line = line.strip()
|
||||
|
||||
if line.startswith('BSS '):
|
||||
if line.startswith('BSS ') and line.split()[1].count(':') >= 5:
|
||||
# Match BSS lines with MAC addresses (xx:xx:xx:xx:xx:xx), not "BSS Load:" etc.
|
||||
if current_network:
|
||||
networks.append(current_network)
|
||||
|
||||
bssid = line.split()[1].rstrip('(')
|
||||
# Handle format: BSS aa:bb:cc:dd:ee:ff(on wlan0)
|
||||
bssid = line.split()[1].split('(')[0]
|
||||
current_network = {
|
||||
'ssid': '',
|
||||
'bssid': bssid,
|
||||
@@ -324,7 +445,8 @@ class WiFiManager:
|
||||
if line.startswith('SSID: '):
|
||||
current_network['ssid'] = line[6:]
|
||||
elif line.startswith('freq: '):
|
||||
current_network['frequency'] = int(line[6:])
|
||||
# freq can be "2437" or "2437.0" depending on iw version
|
||||
current_network['frequency'] = int(float(line[6:]))
|
||||
elif line.startswith('signal: '):
|
||||
# Parse signal strength (e.g., "-50.00 dBm")
|
||||
signal = line[8:].split()[0]
|
||||
@@ -346,11 +468,12 @@ class WiFiManager:
|
||||
print(f"Error scanning networks: {e}")
|
||||
return []
|
||||
|
||||
async def set_mode(self, mode: WiFiMode) -> bool:
|
||||
async def set_mode(self, mode: WiFiMode, persist: bool = True) -> bool:
|
||||
"""Set WiFi mode.
|
||||
|
||||
Args:
|
||||
mode: Desired WiFi mode
|
||||
persist: If True, save mode to persistent storage for boot persistence
|
||||
|
||||
Returns:
|
||||
True if mode was set successfully
|
||||
@@ -371,40 +494,122 @@ class WiFiManager:
|
||||
await self._enable_auto_mode()
|
||||
|
||||
self._current_mode = mode
|
||||
|
||||
# Persist mode for boot persistence
|
||||
if persist:
|
||||
self._save_mode(mode)
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error setting WiFi mode: {e}")
|
||||
return False
|
||||
|
||||
async def _systemctl(self, action: str, service: str):
|
||||
"""Control systemd service via dbus (no sudo required with polkit rule).
|
||||
|
||||
Args:
|
||||
action: "start", "stop", or "restart"
|
||||
service: Service name (e.g., "hostapd.service")
|
||||
"""
|
||||
if not service.endswith(".service"):
|
||||
service = f"{service}.service"
|
||||
|
||||
method = {
|
||||
"start": "StartUnit",
|
||||
"stop": "StopUnit",
|
||||
"restart": "RestartUnit"
|
||||
}.get(action, "StartUnit")
|
||||
|
||||
cmd = (
|
||||
f'busctl call org.freedesktop.systemd1 '
|
||||
f'/org/freedesktop/systemd1 org.freedesktop.systemd1.Manager '
|
||||
f'{method} ss "{service}" "replace"'
|
||||
)
|
||||
await self._run_command(cmd, check=False)
|
||||
|
||||
async def _enable_nm_management(self):
|
||||
"""Enable NetworkManager to manage wlan0 for client mode.
|
||||
|
||||
Uses nmcli to set wlan0 as managed (no file permissions needed).
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check current device status
|
||||
status = await self._run_command("nmcli device status", check=False)
|
||||
logger.warning(f"[NM Management] Current status:\n{status}")
|
||||
|
||||
# Use nmcli to set device as managed (works without root)
|
||||
logger.warning("[NM Management] Setting wlan0 as managed...")
|
||||
result = await self._run_command("nmcli device set wlan0 managed yes", check=False)
|
||||
logger.warning(f"[NM Management] nmcli set managed result: '{result}'")
|
||||
|
||||
# Give NM time to pick up the device
|
||||
logger.warning("[NM Management] Waiting 2s...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Verify device is now managed
|
||||
status = await self._run_command("nmcli device status", check=False)
|
||||
logger.warning(f"[NM Management] Status after:\n{status}")
|
||||
|
||||
async def _disable_nm_management(self):
|
||||
"""Disable NetworkManager management of wlan0 for AP mode.
|
||||
|
||||
Uses nmcli to set wlan0 as unmanaged (for hostapd).
|
||||
"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Use nmcli to set device as unmanaged
|
||||
logger.warning("[NM Management] Setting wlan0 as unmanaged...")
|
||||
result = await self._run_command("nmcli device set wlan0 managed no", check=False)
|
||||
logger.warning(f"[NM Management] nmcli set unmanaged result: '{result}'")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
async def _enable_ap_mode(self):
|
||||
"""Enable Access Point mode."""
|
||||
# Ensure NM doesn't manage wlan0
|
||||
await self._disable_nm_management()
|
||||
# Start hostapd and dnsmasq for AP
|
||||
await self._run_command("sudo systemctl start hostapd")
|
||||
await self._run_command("sudo systemctl start dnsmasq")
|
||||
await self._systemctl("start", "hostapd")
|
||||
await self._systemctl("start", "dnsmasq")
|
||||
# Set static IP for AP
|
||||
await self._run_command("ip addr add 192.168.4.1/24 dev wlan0", check=False)
|
||||
print("AP mode enabled")
|
||||
|
||||
async def _enable_client_mode(self):
|
||||
"""Enable Client mode."""
|
||||
"""Enable Client mode using NetworkManager."""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Stop AP services
|
||||
await self._run_command("sudo systemctl stop hostapd", check=False)
|
||||
await self._run_command("sudo systemctl stop dnsmasq", check=False)
|
||||
# Start wpa_supplicant
|
||||
await self._run_command("sudo systemctl start wpa_supplicant")
|
||||
print("Client mode enabled")
|
||||
logger.warning("[Client Mode] Stopping hostapd...")
|
||||
await self._systemctl("stop", "hostapd")
|
||||
logger.warning("[Client Mode] Stopping dnsmasq...")
|
||||
await self._systemctl("stop", "dnsmasq")
|
||||
|
||||
# Check hostapd status
|
||||
hostapd_status = await self._run_command("systemctl is-active hostapd", check=False)
|
||||
logger.warning(f"[Client Mode] hostapd status after stop: {hostapd_status.strip()}")
|
||||
|
||||
# Enable NetworkManager to manage wlan0
|
||||
logger.warning("[Client Mode] Enabling NM management...")
|
||||
await self._enable_nm_management()
|
||||
logger.warning("[Client Mode] Client mode enabled")
|
||||
|
||||
async def _enable_dual_mode(self):
|
||||
"""Enable Dual mode (AP + Client)."""
|
||||
# Enable both AP and client
|
||||
await self._run_command("sudo systemctl start hostapd")
|
||||
await self._run_command("sudo systemctl start dnsmasq")
|
||||
await self._run_command("sudo systemctl start wpa_supplicant")
|
||||
await self._systemctl("start", "hostapd")
|
||||
await self._systemctl("start", "dnsmasq")
|
||||
await self._systemctl("start", "wpa_supplicant")
|
||||
print("Dual mode enabled")
|
||||
|
||||
async def _disable_wifi(self):
|
||||
"""Disable all WiFi."""
|
||||
await self._run_command("sudo systemctl stop hostapd", check=False)
|
||||
await self._run_command("sudo systemctl stop dnsmasq", check=False)
|
||||
await self._run_command("sudo systemctl stop wpa_supplicant", check=False)
|
||||
await self._systemctl("stop", "hostapd")
|
||||
await self._systemctl("stop", "dnsmasq")
|
||||
await self._systemctl("stop", "wpa_supplicant")
|
||||
print("WiFi disabled")
|
||||
|
||||
async def _enable_auto_mode(self):
|
||||
@@ -426,19 +631,25 @@ class WiFiManager:
|
||||
ssid: str,
|
||||
password: Optional[str] = None,
|
||||
interface: Optional[str] = None,
|
||||
hidden: bool = False
|
||||
hidden: bool = False,
|
||||
fallback_to_ap: bool = True
|
||||
) -> bool:
|
||||
"""Connect to a WiFi network.
|
||||
"""Connect to a WiFi network using NetworkManager.
|
||||
|
||||
Args:
|
||||
ssid: Network SSID
|
||||
password: Network password (if encrypted)
|
||||
interface: Interface to use (default: first available)
|
||||
hidden: Whether network is hidden
|
||||
fallback_to_ap: If True, revert to AP mode on connection failure
|
||||
|
||||
Returns:
|
||||
True if connection successful
|
||||
"""
|
||||
# Ensure interfaces are detected before trying to connect
|
||||
if not self._interfaces:
|
||||
await self.detect_interfaces()
|
||||
|
||||
if not interface:
|
||||
# Use first non-USB interface, or first available
|
||||
for iface in self._interfaces:
|
||||
@@ -449,52 +660,79 @@ class WiFiManager:
|
||||
interface = self._interfaces[0].name
|
||||
|
||||
if not interface:
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.warning("[WiFi Connect] No interface found!")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Add network to wpa_supplicant configuration
|
||||
config_path = Path("/etc/wpa_supplicant/wpa_supplicant.conf")
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
logger.warning(f"[WiFi Connect] Starting connect to {ssid}")
|
||||
|
||||
# Switch to client mode (stops hostapd, enables NM management)
|
||||
logger.warning("[WiFi Connect] Step 1: Enabling client mode...")
|
||||
await self._enable_client_mode()
|
||||
logger.warning("[WiFi Connect] Step 1: Client mode enabled")
|
||||
|
||||
# Wait for NM to be ready and detect wlan0
|
||||
logger.warning("[WiFi Connect] Step 2: Waiting 2s for NM...")
|
||||
await asyncio.sleep(2)
|
||||
|
||||
# Check device status
|
||||
dev_status = await self._run_command("nmcli device status", check=False)
|
||||
logger.warning(f"[WiFi Connect] Device status after wait:\n{dev_status}")
|
||||
|
||||
# Delete any existing saved connection for this SSID
|
||||
# (handles corrupted connections with missing key-mgmt property)
|
||||
ssid_escaped = ssid.replace("'", "'\\''")
|
||||
logger.warning(f"[WiFi Connect] Step 3: Deleting existing connection for {ssid}...")
|
||||
del_result = await self._run_command(
|
||||
f"nmcli connection delete '{ssid_escaped}'",
|
||||
check=False
|
||||
)
|
||||
logger.warning(f"[WiFi Connect] Delete result: {del_result}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Build nmcli connect command
|
||||
cmd = f"nmcli device wifi connect '{ssid_escaped}'"
|
||||
|
||||
# Create network block
|
||||
if password:
|
||||
# Encrypted network
|
||||
network_block = f"""
|
||||
network={{
|
||||
ssid="{ssid}"
|
||||
psk="{password}"
|
||||
scan_ssid={1 if hidden else 0}
|
||||
priority=1
|
||||
}}
|
||||
"""
|
||||
else:
|
||||
# Open network
|
||||
network_block = f"""
|
||||
network={{
|
||||
ssid="{ssid}"
|
||||
key_mgmt=NONE
|
||||
scan_ssid={1 if hidden else 0}
|
||||
priority=1
|
||||
}}
|
||||
"""
|
||||
password_escaped = password.replace("'", "'\\''")
|
||||
cmd += f" password '{password_escaped}'"
|
||||
|
||||
# Append to config (or use wpa_cli)
|
||||
# Using wpa_cli for immediate connection
|
||||
await self._add_network_via_wpa_cli(ssid, password, hidden)
|
||||
if hidden:
|
||||
cmd += " hidden yes"
|
||||
|
||||
# Reconnect wpa_supplicant
|
||||
await self._run_command(f"sudo wpa_cli -i {interface} reconfigure")
|
||||
# Attempt connection
|
||||
logger.warning(f"[WiFi Connect] Step 4: Running nmcli connect...")
|
||||
result = await self._run_command(cmd, check=False)
|
||||
logger.warning(f"[WiFi Connect] Connect result: {result}")
|
||||
|
||||
# Wait for connection
|
||||
await asyncio.sleep(3)
|
||||
# Check if connection was successful
|
||||
if "successfully activated" in result.lower():
|
||||
logger.warning(f"[WiFi Connect] SUCCESS - connected to {ssid}")
|
||||
|
||||
# Verify connection
|
||||
result = await self._run_command(f"sudo wpa_cli -i {interface} status")
|
||||
if f'ssid={ssid}' in result and 'wpa_state=COMPLETED' in result:
|
||||
print(f"Successfully connected to {ssid}")
|
||||
return True
|
||||
else:
|
||||
print(f"Connection to {ssid} failed")
|
||||
return False
|
||||
# Verify we have an IP address
|
||||
await asyncio.sleep(2)
|
||||
ip_result = await self._run_command(f"ip addr show {interface}")
|
||||
logger.warning(f"[WiFi Connect] IP result: {ip_result}")
|
||||
if "inet " in ip_result and "192.168.4." not in ip_result:
|
||||
# Save client mode for boot persistence
|
||||
self._current_mode = WiFiMode.CLIENT
|
||||
self._save_mode(WiFiMode.CLIENT)
|
||||
logger.info(f"WiFi client mode saved for boot persistence")
|
||||
return True
|
||||
|
||||
# Connection failed
|
||||
logger.warning(f"[WiFi Connect] FAILED - {result}")
|
||||
|
||||
if fallback_to_ap:
|
||||
print("Falling back to AP mode...")
|
||||
await self._enable_ap_mode()
|
||||
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error connecting to network: {e}")
|
||||
@@ -532,7 +770,7 @@ network={{
|
||||
return False
|
||||
|
||||
async def disconnect_from_network(self, interface: Optional[str] = None) -> bool:
|
||||
"""Disconnect from current network.
|
||||
"""Disconnect from current network and return to AP mode.
|
||||
|
||||
Args:
|
||||
interface: Interface to disconnect (default: first connected)
|
||||
@@ -550,30 +788,36 @@ network={{
|
||||
return False
|
||||
|
||||
try:
|
||||
await self._run_command(f"sudo wpa_cli -i {interface} disconnect")
|
||||
# Disconnect using nmcli
|
||||
await self._run_command(f"nmcli device disconnect {interface}", check=False)
|
||||
# Return to AP mode
|
||||
await self._enable_ap_mode()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Error disconnecting: {e}")
|
||||
return False
|
||||
|
||||
async def get_saved_networks(self) -> List[Dict[str, str]]:
|
||||
"""Get list of saved networks from wpa_supplicant.
|
||||
"""Get list of saved WiFi networks from NetworkManager.
|
||||
|
||||
Returns:
|
||||
List of saved networks with SSID and other info
|
||||
"""
|
||||
try:
|
||||
result = await self._run_command("sudo wpa_cli list_networks")
|
||||
result = await self._run_command(
|
||||
"nmcli -t -f NAME,TYPE connection show",
|
||||
check=False
|
||||
)
|
||||
networks = []
|
||||
|
||||
for line in result.split('\n')[1:]: # Skip header
|
||||
for line in result.split('\n'):
|
||||
if line.strip():
|
||||
parts = line.split('\t')
|
||||
if len(parts) >= 2:
|
||||
parts = line.split(':')
|
||||
if len(parts) >= 2 and parts[1] == '802-11-wireless':
|
||||
networks.append({
|
||||
'id': parts[0].strip(),
|
||||
'ssid': parts[1].strip(),
|
||||
'enabled': 'CURRENT' in line or 'ENABLED' in line
|
||||
'ssid': parts[0],
|
||||
'type': 'wifi',
|
||||
'enabled': True
|
||||
})
|
||||
|
||||
return networks
|
||||
@@ -591,23 +835,14 @@ network={{
|
||||
True if network was forgotten
|
||||
"""
|
||||
try:
|
||||
# Get network ID
|
||||
networks = await self.get_saved_networks()
|
||||
network_id = None
|
||||
# Delete connection using nmcli
|
||||
ssid_escaped = ssid.replace("'", "'\\''")
|
||||
result = await self._run_command(
|
||||
f"nmcli connection delete '{ssid_escaped}'",
|
||||
check=False
|
||||
)
|
||||
|
||||
for net in networks:
|
||||
if net['ssid'] == ssid:
|
||||
network_id = net['id']
|
||||
break
|
||||
|
||||
if network_id is None:
|
||||
return False
|
||||
|
||||
# Remove network
|
||||
await self._run_command(f"sudo wpa_cli remove_network {network_id}")
|
||||
await self._run_command("sudo wpa_cli save_config")
|
||||
|
||||
return True
|
||||
return "successfully deleted" in result.lower()
|
||||
except Exception as e:
|
||||
print(f"Error forgetting network: {e}")
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user