diff --git a/cmd/repeater/static/app.js b/cmd/repeater/static/app.js index eacedee..3bb7f82 100644 --- a/cmd/repeater/static/app.js +++ b/cmd/repeater/static/app.js @@ -4,7 +4,9 @@ const appState = { hotspotEnabled: true, autoScrollLogs: true, ws: null, + wifiWs: null, reconnectAttempts: 0, + wifiReconnectAttempts: 0, maxReconnectAttempts: 5, connectedSSID: null, networks: [], @@ -28,8 +30,9 @@ async function initializeApp() { loadLogs() ]); - // Set up WebSocket for real-time logs + // Set up WebSockets for real-time updates connectWebSocket(); + connectWifiWebSocket(); // Start periodic updates startPeriodicUpdates(); @@ -72,8 +75,20 @@ async function scanWifi() { showToast('success', 'Scan WiFi', `${networks.length} réseau(x) trouvé(s)`); } catch (error) { console.error('Error scanning WiFi:', error); - wifiList.innerHTML = '
Erreur lors du scan
'; - showToast('error', 'Erreur', 'Échec du scan WiFi'); + + // Fallback to cached networks + try { + const fallbackResponse = await fetch('/api/wifi/networks'); + const cachedNetworks = await fallbackResponse.json(); + + appState.networks = cachedNetworks; + displayWifiNetworks(cachedNetworks); + showToast('warning', 'Scan échoué', `Affichage des réseaux en cache (${cachedNetworks.length})`); + } catch (fallbackError) { + console.error('Error loading cached networks:', fallbackError); + wifiList.innerHTML = '
Erreur lors du scan
'; + showToast('error', 'Erreur', 'Échec du scan WiFi'); + } } finally { if (scanBtn) { scanBtn.disabled = false; @@ -491,6 +506,108 @@ function connectWebSocket() { } } +function connectWifiWebSocket() { + const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; + const wsUrl = `${protocol}//${window.location.host}/ws/wifi`; + + try { + appState.wifiWs = new WebSocket(wsUrl); + + appState.wifiWs.onopen = function() { + console.log('WiFi WebSocket connected'); + appState.wifiReconnectAttempts = 0; + }; + + appState.wifiWs.onmessage = function(event) { + try { + const msg = JSON.parse(event.data); + handleWifiEvent(msg); + } catch (error) { + console.error('Error parsing WiFi WebSocket message:', error); + } + }; + + appState.wifiWs.onerror = function(error) { + console.error('WiFi WebSocket error:', error); + }; + + appState.wifiWs.onclose = function() { + console.log('WiFi WebSocket disconnected'); + + // Attempt to reconnect + if (appState.wifiReconnectAttempts < appState.maxReconnectAttempts) { + appState.wifiReconnectAttempts++; + setTimeout(connectWifiWebSocket, 5000); + } + }; + } catch (error) { + console.error('Error creating WiFi WebSocket:', error); + } +} + +function handleWifiEvent(event) { + console.log('WiFi event received:', event.type, event); + + switch (event.type) { + case 'scan_update': + handleScanUpdate(event.data); + break; + case 'state_change': + handleStateChange(event.data); + break; + case 'signal_update': + handleSignalUpdate(event.data); + break; + default: + console.warn('Unknown WiFi event type:', event.type); + } +} + +function handleScanUpdate(data) { + // Update the network list in real-time + appState.networks = data.networks; + displayWifiNetworks(data.networks); + console.log(`Scan update: ${data.networks.length} network(s) found`); +} + +function handleStateChange(data) { + // Update WiFi status badge + const wifiStatus = document.getElementById('wifiStatus'); + const wifiDot = wifiStatus.querySelector('.status-dot'); + const wifiText = wifiStatus.querySelector('.status-text'); + + if (data.state === 'connected') { + wifiDot.className = 'status-dot active'; + wifiText.textContent = `Connecté: ${data.ssid}`; + appState.connectedSSID = data.ssid; + + // Refresh network list to show connected network + if (appState.networks.length > 0) { + displayWifiNetworks(appState.networks); + } + } else if (data.state === 'disconnected') { + wifiDot.className = 'status-dot offline'; + wifiText.textContent = 'Déconnecté'; + appState.connectedSSID = null; + + // Refresh network list to remove connected highlighting + if (appState.networks.length > 0) { + displayWifiNetworks(appState.networks); + } + } else if (data.state === 'connecting') { + wifiDot.className = 'status-dot'; + wifiText.textContent = `Connexion à ${data.ssid}...`; + } + + console.log(`WiFi state changed: ${data.previous_state} → ${data.state}`, data.ssid); +} + +function handleSignalUpdate(data) { + // Update signal strength display if needed + console.log(`Signal update for ${data.ssid}: ${data.signal}/5 (${data.dbm} dBm)`); + // Could update the network list to reflect new signal strength +} + // ===== Utility Functions ===== function generateSignalBars(strength) { diff --git a/internal/api/handlers/handlers.go b/internal/api/handlers/handlers.go index fefa8ef..6ef8ec1 100644 --- a/internal/api/handlers/handlers.go +++ b/internal/api/handlers/handlers.go @@ -12,6 +12,18 @@ import ( "github.com/nemunaire/repeater/internal/wifi" ) +// GetWiFiNetworks returns cached WiFi networks without scanning +func GetWiFiNetworks(c *gin.Context) { + networks, err := wifi.GetCachedNetworks() + if err != nil { + logging.AddLog("WiFi", "Erreur lors de la récupération des réseaux: "+err.Error()) + c.JSON(http.StatusInternalServerError, gin.H{"error": "Erreur lors de la récupération des réseaux"}) + return + } + + c.JSON(http.StatusOK, networks) +} + // ScanWiFi handles WiFi network scanning func ScanWiFi(c *gin.Context) { networks, err := wifi.ScanNetworks() diff --git a/internal/api/handlers/websocket_wifi.go b/internal/api/handlers/websocket_wifi.go new file mode 100644 index 0000000..ff8d132 --- /dev/null +++ b/internal/api/handlers/websocket_wifi.go @@ -0,0 +1,30 @@ +package handlers + +import ( + "log" + + "github.com/gin-gonic/gin" + "github.com/nemunaire/repeater/internal/wifi" +) + +// WebSocketWifi handles WebSocket connections for real-time WiFi events +func WebSocketWifi(c *gin.Context) { + conn, err := upgrader.Upgrade(c.Writer, c.Request, nil) + if err != nil { + log.Printf("Erreur WebSocket WiFi: %v", err) + return + } + defer conn.Close() + + // Register client + wifi.RegisterWebSocketClient(conn) + defer wifi.UnregisterWebSocketClient(conn) + + // Keep connection alive + for { + _, _, err := conn.ReadMessage() + if err != nil { + break + } + } +} diff --git a/internal/api/router.go b/internal/api/router.go index a06c328..956661e 100644 --- a/internal/api/router.go +++ b/internal/api/router.go @@ -24,6 +24,7 @@ func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.F // WiFi endpoints wifi := api.Group("/wifi") { + wifi.GET("/networks", handlers.GetWiFiNetworks) wifi.GET("/scan", handlers.ScanWiFi) wifi.POST("/connect", handlers.ConnectWiFi) wifi.POST("/disconnect", handlers.DisconnectWiFi) @@ -53,8 +54,9 @@ func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.F api.DELETE("/logs", handlers.ClearLogs) } - // WebSocket endpoint + // WebSocket endpoints r.GET("/ws/logs", handlers.WebSocketLogs) + r.GET("/ws/wifi", handlers.WebSocketWifi) // Serve static files sub, err := fs.Sub(assets, "static") diff --git a/internal/app/app.go b/internal/app/app.go index 5cbf5f5..de556da 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -53,6 +53,12 @@ func (a *App) Initialize(cfg *config.Config) error { return err } + // Start WiFi event monitoring + if err := wifi.StartEventMonitoring(); err != nil { + log.Printf("Warning: WiFi event monitoring failed: %v", err) + // Don't fail - polling fallback still works + } + // Start periodic tasks go a.periodicStatusUpdate() go a.periodicDeviceUpdate() @@ -71,6 +77,7 @@ func (a *App) Run(addr string) error { // Shutdown gracefully shuts down the application func (a *App) Shutdown() { + wifi.StopEventMonitoring() wifi.Close() logging.AddLog("Système", "Application arrêtée") } diff --git a/internal/wifi/broadcaster.go b/internal/wifi/broadcaster.go new file mode 100644 index 0000000..931ad3e --- /dev/null +++ b/internal/wifi/broadcaster.go @@ -0,0 +1,140 @@ +package wifi + +import ( + "log" + "sync" + + "github.com/gorilla/websocket" + "github.com/nemunaire/repeater/internal/models" +) + +// WifiBroadcaster manages WebSocket clients and broadcasts WiFi events +type WifiBroadcaster struct { + clients map[*websocket.Conn]bool + clientsMu sync.RWMutex + + // State deduplication + lastState string + lastSSID string + lastNetworks []models.WiFiNetwork + stateMu sync.RWMutex +} + +// NewWifiBroadcaster creates a new WiFi broadcaster +func NewWifiBroadcaster() *WifiBroadcaster { + return &WifiBroadcaster{ + clients: make(map[*websocket.Conn]bool), + } +} + +// RegisterClient registers a new WebSocket client +func (wb *WifiBroadcaster) RegisterClient(conn *websocket.Conn) { + wb.clientsMu.Lock() + wb.clients[conn] = true + wb.clientsMu.Unlock() + + // Send initial state to the new client + wb.sendInitialState(conn) +} + +// UnregisterClient removes a WebSocket client +func (wb *WifiBroadcaster) UnregisterClient(conn *websocket.Conn) { + wb.clientsMu.Lock() + delete(wb.clients, conn) + wb.clientsMu.Unlock() +} + +// sendInitialState sends the current WiFi state to a newly connected client +func (wb *WifiBroadcaster) sendInitialState(conn *websocket.Conn) { + wb.stateMu.RLock() + lastState := wb.lastState + lastSSID := wb.lastSSID + lastNetworks := make([]models.WiFiNetwork, len(wb.lastNetworks)) + copy(lastNetworks, wb.lastNetworks) + wb.stateMu.RUnlock() + + // Send last known state if available + if lastState != "" { + event := NewStateChangeEvent(lastState, lastSSID, "") + conn.WriteJSON(event) + } + + // Send last known network list if available + if len(lastNetworks) > 0 { + event := NewScanUpdateEvent(lastNetworks) + conn.WriteJSON(event) + } +} + +// BroadcastScanUpdate broadcasts a scan update event to all clients +func (wb *WifiBroadcaster) BroadcastScanUpdate(networks []models.WiFiNetwork) { + // Check for changes to avoid duplicate broadcasts + wb.stateMu.Lock() + if networksEqual(wb.lastNetworks, networks) { + wb.stateMu.Unlock() + return + } + wb.lastNetworks = make([]models.WiFiNetwork, len(networks)) + copy(wb.lastNetworks, networks) + wb.stateMu.Unlock() + + event := NewScanUpdateEvent(networks) + wb.broadcast(event) +} + +// BroadcastStateChange broadcasts a state change event to all clients +func (wb *WifiBroadcaster) BroadcastStateChange(state, ssid string) { + // Check for changes to avoid duplicate broadcasts + wb.stateMu.Lock() + if wb.lastState == state && wb.lastSSID == ssid { + wb.stateMu.Unlock() + return + } + previousState := wb.lastState + wb.lastState = state + wb.lastSSID = ssid + wb.stateMu.Unlock() + + event := NewStateChangeEvent(state, ssid, previousState) + wb.broadcast(event) +} + +// BroadcastSignalUpdate broadcasts a signal update event to all clients +func (wb *WifiBroadcaster) BroadcastSignalUpdate(ssid string, signal, dbm int) { + event := NewSignalUpdateEvent(ssid, signal, dbm) + wb.broadcast(event) +} + +// broadcast sends an event to all connected clients +func (wb *WifiBroadcaster) broadcast(event WifiEvent) { + // Get list of clients with read lock + wb.clientsMu.RLock() + clients := make([]*websocket.Conn, 0, len(wb.clients)) + for client := range wb.clients { + clients = append(clients, client) + } + wb.clientsMu.RUnlock() + + // Broadcast to all clients + for _, client := range clients { + err := client.WriteJSON(event) + if err != nil { + log.Printf("Erreur lors de l'envoi WebSocket WiFi: %v", err) + client.Close() + wb.UnregisterClient(client) + } + } +} + +// networksEqual compares two network slices for equality +func networksEqual(a, b []models.WiFiNetwork) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i].SSID != b[i].SSID || a[i].Signal != b[i].Signal { + return false + } + } + return true +} diff --git a/internal/wifi/events.go b/internal/wifi/events.go new file mode 100644 index 0000000..c8f3672 --- /dev/null +++ b/internal/wifi/events.go @@ -0,0 +1,70 @@ +package wifi + +import ( + "time" + + "github.com/nemunaire/repeater/internal/models" +) + +// WifiEvent represents a WiFi event to be sent over WebSocket +type WifiEvent struct { + Type string `json:"type"` + Timestamp time.Time `json:"timestamp"` + Data interface{} `json:"data"` +} + +// ScanUpdateData contains network list update information +type ScanUpdateData struct { + Networks []models.WiFiNetwork `json:"networks"` +} + +// StateChangeData contains connection state change information +type StateChangeData struct { + State string `json:"state"` + SSID string `json:"ssid,omitempty"` + PreviousState string `json:"previous_state,omitempty"` +} + +// SignalUpdateData contains signal strength update information +type SignalUpdateData struct { + SSID string `json:"ssid"` + Signal int `json:"signal"` // 1-5 scale + DBm int `json:"dbm"` // Raw dBm value +} + +// NewScanUpdateEvent creates a new scan update event +func NewScanUpdateEvent(networks []models.WiFiNetwork) WifiEvent { + return WifiEvent{ + Type: "scan_update", + Timestamp: time.Now(), + Data: ScanUpdateData{ + Networks: networks, + }, + } +} + +// NewStateChangeEvent creates a new state change event +func NewStateChangeEvent(state, ssid, previousState string) WifiEvent { + return WifiEvent{ + Type: "state_change", + Timestamp: time.Now(), + Data: StateChangeData{ + State: state, + SSID: ssid, + PreviousState: previousState, + }, + } +} + +// NewSignalUpdateEvent creates a new signal update event +func NewSignalUpdateEvent(ssid string, signal, dbm int) WifiEvent { + return WifiEvent{ + Type: "signal_update", + Timestamp: time.Now(), + Data: SignalUpdateData{ + SSID: ssid, + Signal: signal, + DBm: dbm, + }, + } +} diff --git a/internal/wifi/iwd/signals.go b/internal/wifi/iwd/signals.go new file mode 100644 index 0000000..90ed32e --- /dev/null +++ b/internal/wifi/iwd/signals.go @@ -0,0 +1,223 @@ +package iwd + +import ( + "log" + "sync" + + "github.com/godbus/dbus/v5" +) + +// SignalMonitor monitors D-Bus signals from iwd +type SignalMonitor struct { + conn *dbus.Conn + station *Station + + // Signal channel + signalChan chan *dbus.Signal + + // Callbacks + onStateChange func(state StationState, ssid string) + onScanComplete func() + + // Control + stopChan chan struct{} + mu sync.RWMutex + running bool + + // State tracking + lastScanning bool +} + +// NewSignalMonitor creates a new signal monitor +func NewSignalMonitor(conn *dbus.Conn, station *Station) *SignalMonitor { + return &SignalMonitor{ + conn: conn, + station: station, + signalChan: make(chan *dbus.Signal, 100), + stopChan: make(chan struct{}), + } +} + +// OnStateChange registers a callback for state changes +func (sm *SignalMonitor) OnStateChange(callback func(state StationState, ssid string)) { + sm.mu.Lock() + defer sm.mu.Unlock() + sm.onStateChange = callback +} + +// OnScanComplete registers a callback for scan completion +func (sm *SignalMonitor) OnScanComplete(callback func()) { + sm.mu.Lock() + defer sm.mu.Unlock() + sm.onScanComplete = callback +} + +// Start begins monitoring D-Bus signals +func (sm *SignalMonitor) Start() error { + sm.mu.Lock() + if sm.running { + sm.mu.Unlock() + return nil + } + sm.running = true + sm.mu.Unlock() + + // Subscribe to PropertiesChanged signals for Station interface + stationPath := sm.station.GetPath() + + // Add signal match for PropertiesChanged on Station interface + matchOptions := []dbus.MatchOption{ + dbus.WithMatchObjectPath(stationPath), + dbus.WithMatchInterface("org.freedesktop.DBus.Properties"), + dbus.WithMatchMember("PropertiesChanged"), + } + + if err := sm.conn.AddMatchSignal(matchOptions...); err != nil { + sm.mu.Lock() + sm.running = false + sm.mu.Unlock() + return err + } + + // Register signal channel + sm.conn.Signal(sm.signalChan) + + // Get initial scanning state + scanning, err := sm.station.IsScanning() + if err == nil { + sm.lastScanning = scanning + } + + // Start monitoring goroutine + go sm.monitor() + + log.Printf("D-Bus signal monitoring started for station %s", stationPath) + return nil +} + +// Stop stops monitoring D-Bus signals +func (sm *SignalMonitor) Stop() { + sm.mu.Lock() + if !sm.running { + sm.mu.Unlock() + return + } + sm.running = false + sm.mu.Unlock() + + // Signal stop + close(sm.stopChan) + + // Remove signal channel + sm.conn.RemoveSignal(sm.signalChan) + + log.Printf("D-Bus signal monitoring stopped") +} + +// monitor is the main signal processing loop +func (sm *SignalMonitor) monitor() { + for { + select { + case sig := <-sm.signalChan: + sm.handleSignal(sig) + case <-sm.stopChan: + return + } + } +} + +// handleSignal processes a D-Bus signal +func (sm *SignalMonitor) handleSignal(sig *dbus.Signal) { + // Only process PropertiesChanged signals + if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" { + return + } + + // Verify signal is from Station interface + if len(sig.Body) < 2 { + return + } + + interfaceName, ok := sig.Body[0].(string) + if !ok || interfaceName != StationInterface { + return + } + + // Parse changed properties + changedProps, ok := sig.Body[1].(map[string]dbus.Variant) + if !ok { + return + } + + // Check for State property change + if stateVariant, ok := changedProps["State"]; ok { + if state, ok := stateVariant.Value().(string); ok { + sm.handleStateChange(StationState(state)) + } + } + + // Check for Scanning property change + if scanningVariant, ok := changedProps["Scanning"]; ok { + if scanning, ok := scanningVariant.Value().(bool); ok { + sm.handleScanningChange(scanning) + } + } + + // Check for ConnectedNetwork property change + if _, ok := changedProps["ConnectedNetwork"]; ok { + // Network connection changed, trigger state update + sm.handleConnectionChange() + } +} + +// handleStateChange processes a state change +func (sm *SignalMonitor) handleStateChange(state StationState) { + sm.mu.RLock() + callback := sm.onStateChange + sm.mu.RUnlock() + + if callback == nil { + return + } + + // Get connected SSID if connected + ssid := "" + if state == StateConnected { + network, err := sm.station.GetConnectedNetwork() + if err == nil { + props, err := network.GetProperties() + if err == nil { + ssid = props.Name + } + } + } + + callback(state, ssid) +} + +// handleScanningChange processes scanning state changes +func (sm *SignalMonitor) handleScanningChange(scanning bool) { + // Detect scan completion (transition from true to false) + if sm.lastScanning && !scanning { + sm.mu.RLock() + callback := sm.onScanComplete + sm.mu.RUnlock() + + if callback != nil { + callback() + } + } + + sm.lastScanning = scanning +} + +// handleConnectionChange processes connection changes +func (sm *SignalMonitor) handleConnectionChange() { + // Get current state and trigger state change callback + state, err := sm.station.GetState() + if err != nil { + return + } + + sm.handleStateChange(state) +} diff --git a/internal/wifi/iwd/station.go b/internal/wifi/iwd/station.go index 7abed90..54014fd 100644 --- a/internal/wifi/iwd/station.go +++ b/internal/wifi/iwd/station.go @@ -135,3 +135,8 @@ func (s *Station) GetConnectedNetwork() (*Network, error) { return NewNetwork(s.conn, path), nil } + +// GetPath returns the D-Bus object path for this station +func (s *Station) GetPath() dbus.ObjectPath { + return s.path +} diff --git a/internal/wifi/wifi.go b/internal/wifi/wifi.go index 5dd6656..b242e2a 100644 --- a/internal/wifi/wifi.go +++ b/internal/wifi/wifi.go @@ -7,6 +7,7 @@ import ( "time" "github.com/godbus/dbus/v5" + "github.com/gorilla/websocket" "github.com/nemunaire/repeater/internal/models" "github.com/nemunaire/repeater/internal/wifi/iwd" ) @@ -16,12 +17,14 @@ const ( ) var ( - wlanInterface string - dbusConn *dbus.Conn - iwdManager *iwd.Manager - station *iwd.Station - agent *iwd.Agent - agentManager *iwd.AgentManager + wlanInterface string + dbusConn *dbus.Conn + iwdManager *iwd.Manager + station *iwd.Station + agent *iwd.Agent + agentManager *iwd.AgentManager + eventMonitor *iwd.SignalMonitor + wifiBroadcaster *WifiBroadcaster ) // Initialize initializes the WiFi service with iwd D-Bus connection @@ -68,6 +71,48 @@ func Close() { } } +// GetCachedNetworks returns previously discovered networks without triggering a scan +func GetCachedNetworks() ([]models.WiFiNetwork, error) { + // Get ordered networks without scanning + networkInfos, err := station.GetOrderedNetworks() + if err != nil { + return nil, fmt.Errorf("erreur lors de la récupération des réseaux: %v", err) + } + + var networks []models.WiFiNetwork + seenSSIDs := make(map[string]bool) + + for _, netInfo := range networkInfos { + network := iwd.NewNetwork(dbusConn, netInfo.Path) + props, err := network.GetProperties() + if err != nil { + continue + } + + if props.Name == "" || seenSSIDs[props.Name] { + continue + } + seenSSIDs[props.Name] = true + + wifiNet := models.WiFiNetwork{ + SSID: props.Name, + Signal: signalToStrength(int(netInfo.Signal) / 100), + Security: mapSecurityType(props.Type), + BSSID: generateSyntheticBSSID(props.Name), + Channel: 0, + } + + networks = append(networks, wifiNet) + } + + // Sort by signal strength (descending) + sort.Slice(networks, func(i, j int) bool { + return networks[i].Signal > networks[j].Signal + }) + + return networks, nil +} + // ScanNetworks scans for available WiFi networks func ScanNetworks() ([]models.WiFiNetwork, error) { // Check if already scanning @@ -120,6 +165,11 @@ func ScanNetworks() ([]models.WiFiNetwork, error) { return networks[i].Signal > networks[j].Signal }) + // Broadcast to WebSocket clients if available + if wifiBroadcaster != nil { + wifiBroadcaster.BroadcastScanUpdate(networks) + } + return networks, nil } @@ -191,6 +241,59 @@ func GetConnectedSSID() string { return props.Name } +// StartEventMonitoring initializes D-Bus signal monitoring and WebSocket broadcasting +func StartEventMonitoring() error { + // Initialize broadcaster + wifiBroadcaster = NewWifiBroadcaster() + + // Create signal monitor + eventMonitor = iwd.NewSignalMonitor(dbusConn, station) + + // Register callbacks + eventMonitor.OnStateChange(handleStateChange) + eventMonitor.OnScanComplete(handleScanComplete) + + // Start monitoring + return eventMonitor.Start() +} + +// StopEventMonitoring stops D-Bus signal monitoring +func StopEventMonitoring() { + if eventMonitor != nil { + eventMonitor.Stop() + } +} + +// RegisterWebSocketClient registers a new WebSocket client for WiFi events +func RegisterWebSocketClient(conn *websocket.Conn) { + if wifiBroadcaster != nil { + wifiBroadcaster.RegisterClient(conn) + } +} + +// UnregisterWebSocketClient removes a WebSocket client +func UnregisterWebSocketClient(conn *websocket.Conn) { + if wifiBroadcaster != nil { + wifiBroadcaster.UnregisterClient(conn) + } +} + +// handleStateChange is called when WiFi connection state changes +func handleStateChange(newState iwd.StationState, connectedSSID string) { + if wifiBroadcaster != nil { + wifiBroadcaster.BroadcastStateChange(string(newState), connectedSSID) + } +} + +// handleScanComplete is called when a WiFi scan completes +func handleScanComplete() { + // Get updated network list + networks, err := GetCachedNetworks() + if err == nil && wifiBroadcaster != nil { + wifiBroadcaster.BroadcastScanUpdate(networks) + } +} + // mapSecurityType maps iwd security types to display format func mapSecurityType(iwdType string) string { switch iwdType { diff --git a/openapi.yaml b/openapi.yaml index bdcf204..c357310 100644 --- a/openapi.yaml +++ b/openapi.yaml @@ -28,6 +28,42 @@ tags: description: System logs and real-time monitoring paths: + /api/wifi/networks: + get: + tags: + - WiFi + summary: Get discovered WiFi networks + description: | + Returns the list of WiFi networks from the last scan without triggering a new scan. + Returns an empty list if no scan has been performed yet. + operationId: getWiFiNetworks + responses: + '200': + description: List of discovered networks + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/WiFiNetwork' + example: + - ssid: "Hotel-Guest" + signal: 5 + security: "WPA2" + channel: 6 + bssid: "aa:bb:cc:dd:ee:ff" + - ssid: "Public-WiFi" + signal: 3 + security: "Open" + channel: 11 + bssid: "11:22:33:44:55:66" + '500': + description: Error retrieving networks + content: + application/json: + schema: + $ref: '#/components/schemas/Error' + /api/wifi/scan: get: tags: