Add support to multiple wifi interfaces#3726
Add support to multiple wifi interfaces#3726patrickelectric wants to merge 11 commits intobluerobotics:masterfrom
Conversation
Reviewer's GuideImplements multi-interface WiFi support end‑to‑end by extending the NetworkManager backend, introducing a versioned v2 REST API, and updating the frontend to manage and visualize multiple WiFi interfaces while keeping existing v1 behavior intact. Class diagram for multi-interface backend WiFi models and managerclassDiagram
class WifiCredentials {
+str ssid
+str password
}
class ScannedWifiNetwork {
+str ssid
+int signallevel
+int frequency
+str bssid
+str flags
}
class SavedWifiNetwork {
+str ssid
+str nm_uuid
}
class WifiInterface {
+str name
+bool connected
+str ssid
+int signal_strength
+str ip_address
+str mac_address
}
class WifiInterfaceStatus {
+str interface
+str state
+str ssid
+str bssid
+str ip_address
+int signal_strength
+int frequency
+str key_mgmt
}
class WifiInterfaceScanResult {
+str interface
+List~ScannedWifiNetwork~ networks
}
class WifiInterfaceList {
+List~WifiInterface~ interfaces
+str hotspot_interface
}
class ConnectRequest {
+str interface
+WifiCredentials credentials
+bool hidden
}
class DisconnectRequest {
+str interface
}
class AbstractWifiManager {
<<abstract>>
+async status() WifiStatus
+async get_wifi_available() List~ScannedWifiNetwork~
+async try_connect_to_network(credentials, hidden)
+async disconnect()
+async get_saved_wifi_network() List~SavedWifiNetwork~
+async remove_network(ssid)
}
class NetworkManagerWifi {
+Dict~str,str~ _device_paths
+str _device_path
+str _ap_interface
+async start()
+async _get_wifi_devices() Dict~str,str~
+async _create_virtual_interface() bool
+async _autoscan()
+async get_wifi_interfaces() List~WifiInterface~
+async get_interface_status(interface_name) WifiInterface
+async scan_interface(interface_name) List~ScannedWifiNetwork~
+async scan_all_interfaces() List~WifiInterfaceScanResult~
+async _scan_device(device_path) List~ScannedWifiNetwork~
+async get_interface_connection_status(interface_name) WifiInterfaceStatus
+async get_all_interface_status() List~WifiInterfaceStatus~
+async connect_interface(interface_name, credentials, hidden)
+async disconnect_interface(interface_name)
+async get_wifi_available() List~ScannedWifiNetwork~
+async try_connect_to_network(credentials, hidden)
}
AbstractWifiManager <|-- NetworkManagerWifi
WifiInterfaceScanResult "*" --> "*" ScannedWifiNetwork
WifiInterfaceList "*" --> "*" WifiInterface
ConnectRequest --> WifiCredentials
class WifiRouterV2 {
+async scan_interface(interface_name) WifiInterfaceScanResult
+async scan_all_interfaces() List~WifiInterfaceScanResult~
+async get_status(interface_name) WifiInterfaceStatus
+async get_all_status() List~WifiInterfaceStatus~
+async connect(request) dict
+async disconnect(request) dict
+async get_saved_networks() List~SavedWifiNetwork~
+async remove_saved_network(ssid) dict
}
class InterfacesRouterV2 {
+async list_interfaces() WifiInterfaceList
+async get_interface(interface_name) WifiInterface
}
WifiRouterV2 --> AbstractWifiManager
InterfacesRouterV2 --> AbstractWifiManager
WifiRouterV2 --> WifiInterfaceStatus
WifiRouterV2 --> WifiInterfaceScanResult
WifiRouterV2 --> ConnectRequest
WifiRouterV2 --> DisconnectRequest
WifiRouterV2 --> SavedWifiNetwork
InterfacesRouterV2 --> WifiInterface
InterfacesRouterV2 --> WifiInterfaceList
Class diagram for frontend WiFi store and multi-interface typesclassDiagram
class Network {
+str ssid
+int signal
+bool locked
+bool saved
+str bssid
+int frequency
}
class WifiInterface_ts {
+str name
+bool connected
+str ssid
+int signal_strength
+str ip_address
+str mac_address
}
class WifiInterfaceStatus_ts {
+str interface
+str state
+str ssid
+str bssid
+str ip_address
+int signal_strength
+int frequency
+str key_mgmt
}
class WifiInterfaceScanResult_ts {
+str interface
+WPANetwork[] networks
}
class WifiInterfaceList_ts {
+WifiInterface_ts[] interfaces
+str hotspot_interface
}
class WifiStore {
+str API_URL
+str API_URL_V2
+Network current_network
+Network[] available_networks
+Network[] saved_networks
+bool is_loading
+WifiInterface_ts[] wifi_interfaces
+Map~str,Network[]~ interface_scan_results
+Map~str,WifiInterfaceStatus_ts~ interface_status
+setCurrentNetwork(network)
+setAvailableNetworks(networks)
+setSavedNetworks(networks)
+setLoading(loading)
+setWifiInterfaces(interfaces)
+setInterfaceScanResults(payload)
+setInterfaceStatus(payload)
+connectable_networks() Network[]
+getInterface(name) WifiInterface_ts
+getInterfaceNetworks(name) Network[]
+getInterfaceConnectionStatus(name) WifiInterfaceStatus_ts
}
WifiStore --> WifiInterface_ts
WifiStore --> WifiInterfaceStatus_ts
WifiStore --> Network
class WifiUpdater {
+fetch_interfaces_task
+fetch_interface_scans_task
+fetch_interface_status_task
+async fetchInterfaces()
+async fetchInterfaceScans()
+async fetchInterfaceStatus()
}
WifiUpdater --> WifiStore
WifiUpdater --> WifiInterfaceList_ts
WifiUpdater --> WifiInterfaceScanResult_ts
WifiUpdater --> WifiInterfaceStatus_ts
Flow diagram for v2 WiFi interface discovery and scanningflowchart TD
A_start[Start Wifi service] --> B_start_method[NetworkManagerWifi.start]
B_start_method --> C_get_devices[_get_wifi_devices]
C_get_devices --> D_populate_map[Populate _device_paths map keyed by interface name]
D_populate_map --> E_select_primary[Select primary _device_path
prefer wlan0 else first]
E_select_primary --> F_create_virtual[_create_virtual_interface using HOTSPOT_INTERFACE]
F_create_virtual --> G_autoscan_loop[_autoscan over all _device_paths]
subgraph Scan_all_interfaces_v2
G1_call[WifiUpdater.fetchInterfaceScans] --> G2_http[GET /wifi-manager/v2.0/wifi/scan]
G2_http --> G3_router[wifi_router_v2.scan_all_interfaces]
G3_router --> G4_manager[NetworkManagerWifi.scan_all_interfaces]
G4_manager --> G5_scan_device[_scan_device per device_path]
G5_scan_device --> G6_response[List WifiInterfaceScanResult]
G6_response --> G7_store[WifiStore.setInterfaceScanResults per interface]
end
G_autoscan_loop --> G1_call
File-Level Changes
Possibly linked issues
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've found 2 issues, and left some high level feedback:
- In the NetworkManagerWifi methods where you build
ip_addressfromIPv4Config.address_data, you're indexingaddress_data[0]["address"][1]; this assumes"address"is an indexable sequence – double‑check the actual structure (often it's already a string) and adjust to avoid potentialTypeErroror incorrect IP extraction. - The logic to build
WifiInterface/WifiInterfaceStatusobjects (state, ssid, signal, IP, MAC) is duplicated acrossget_wifi_interfaces,get_interface_status, andget_interface_connection_status; consider refactoring this into a shared helper to keep the behavior consistent and make future changes less error‑prone.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- In the NetworkManagerWifi methods where you build `ip_address` from `IPv4Config.address_data`, you're indexing `address_data[0]["address"][1]`; this assumes `"address"` is an indexable sequence – double‑check the actual structure (often it's already a string) and adjust to avoid potential `TypeError` or incorrect IP extraction.
- The logic to build `WifiInterface`/`WifiInterfaceStatus` objects (state, ssid, signal, IP, MAC) is duplicated across `get_wifi_interfaces`, `get_interface_status`, and `get_interface_connection_status`; consider refactoring this into a shared helper to keep the behavior consistent and make future changes less error‑prone.
## Individual Comments
### Comment 1
<location> `core/services/wifi/wifi_handlers/networkmanager/networkmanager.py:394-402` </location>
<code_context>
+ detail=f"Interface '{interface_name}' not found. Multi-interface support requires NetworkManager.",
+ )
+ status_data = await manager.status()
+ return WifiInterfaceStatus(
+ interface="wlan0",
+ state=status_data.state or "unknown",
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Avoid hardcoding key management to WPA-PSK in interface status
`key_mgmt` is always reported as "WPA-PSK", even for open or enterprise networks. This misrepresents the actual security configuration to v2 API consumers. Where possible, derive `key_mgmt` from the active connection/AP; otherwise leave it unset or expose a more accurate representation than a hardcoded string.
</issue_to_address>
### Comment 2
<location> `core/frontend/src/components/wifi/WifiTrayMenu.vue:93-94` </location>
<code_context>
+ wifi_interfaces(): WifiInterface[] {
+ return wifi.wifi_interfaces
+ },
+ has_multiple_interfaces(): boolean {
+ return this.wifi_interfaces.length > 0
+ },
wifi_icon(): string {
</code_context>
<issue_to_address>
**nitpick:** Computed name `has_multiple_interfaces` does not match its current behavior
This returns `true` whenever there is at least one interface, so the multi-interface tray UI is used even for a single interface. Either rename the computed to reflect “v2 UI available” (or similar), or change the condition to `this.wifi_interfaces.length > 1` if it should only be `true` when there are actually multiple interfaces. Aligning the name and condition will make the behavior clearer.
</issue_to_address>Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
| return WifiInterfaceStatus( | ||
| interface=interface_name, | ||
| state="connected", | ||
| ssid=ssid_bytes.decode("utf-8"), | ||
| bssid=await ap.hw_address, | ||
| ip_address=ip_address, | ||
| signal_strength=await ap.strength, | ||
| frequency=await ap.frequency, | ||
| key_mgmt="WPA-PSK", |
There was a problem hiding this comment.
suggestion (bug_risk): Avoid hardcoding key management to WPA-PSK in interface status
key_mgmt is always reported as "WPA-PSK", even for open or enterprise networks. This misrepresents the actual security configuration to v2 API consumers. Where possible, derive key_mgmt from the active connection/AP; otherwise leave it unset or expose a more accurate representation than a hardcoded string.
| has_multiple_interfaces(): boolean { | ||
| return this.wifi_interfaces.length > 0 |
There was a problem hiding this comment.
nitpick: Computed name has_multiple_interfaces does not match its current behavior
This returns true whenever there is at least one interface, so the multi-interface tray UI is used even for a single interface. Either rename the computed to reflect “v2 UI available” (or similar), or change the condition to this.wifi_interfaces.length > 1 if it should only be true when there are actually multiple interfaces. Aligning the name and condition will make the behavior clearer.
f8e0359 to
ed5e27b
Compare
b7edda5 to
5e0f890
Compare
There was a problem hiding this comment.
Sorry @patrickelectric, your pull request is larger than the review limit of 150000 diff characters
That is very confusing for users, and they would also have to re-enter the password... |
Adds support for multiple WiFi interfaces (wlan0, wlan1, etc.): - New API v2 endpoints: /interfaces/, /wifi/scan, /wifi/status, /wifi/connect - NetworkManager handler discovers all WiFi devices dynamically - Filter uap0 virtual interface from user-selectable interfaces - Hotspot support per interface - Use sys.modules pattern in routers to avoid circular imports Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
5e0f890 to
d33ef0d
Compare
- WifiInterfaceTrayMenu: Per-interface icon with dropdown - WifiInterfaceManager: Network list and connection UI per interface - InterfaceConnectionDialog: Connection dialog for v2 API - WifiNetworkCard: Add 2.4G/5G/6G frequency band chip - WifiTrayMenu: Show icon for each detected interface - WifiUpdater: Poll v2 API endpoints for interface data Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
…rength NetworkManager returns percentage (0-100), wpa_supplicant returns dBm (negative). Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
…wifi configuration This is necessary to connect the same wifi in different interfaces Signed-off-by: Patrick José Pereira <patrickelectric@gmail.com>
d33ef0d to
9e10a91
Compare
|
@Williangalvani check now |


Fix #3740
Fix #3708
Tested on Pi5 with multiple interfaces and Pi4 with a single interface
Summary by Sourcery
Add multi-interface WiFi support across backend and frontend while preserving backward-compatible single-interface behavior.
New Features:
Enhancements: