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: