Add websocket wifi updates
This commit is contained in:
parent
c443fce24f
commit
1477d909b0
11 changed files with 755 additions and 10 deletions
|
|
@ -4,7 +4,9 @@ const appState = {
|
||||||
hotspotEnabled: true,
|
hotspotEnabled: true,
|
||||||
autoScrollLogs: true,
|
autoScrollLogs: true,
|
||||||
ws: null,
|
ws: null,
|
||||||
|
wifiWs: null,
|
||||||
reconnectAttempts: 0,
|
reconnectAttempts: 0,
|
||||||
|
wifiReconnectAttempts: 0,
|
||||||
maxReconnectAttempts: 5,
|
maxReconnectAttempts: 5,
|
||||||
connectedSSID: null,
|
connectedSSID: null,
|
||||||
networks: [],
|
networks: [],
|
||||||
|
|
@ -28,8 +30,9 @@ async function initializeApp() {
|
||||||
loadLogs()
|
loadLogs()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Set up WebSocket for real-time logs
|
// Set up WebSockets for real-time updates
|
||||||
connectWebSocket();
|
connectWebSocket();
|
||||||
|
connectWifiWebSocket();
|
||||||
|
|
||||||
// Start periodic updates
|
// Start periodic updates
|
||||||
startPeriodicUpdates();
|
startPeriodicUpdates();
|
||||||
|
|
@ -72,8 +75,20 @@ async function scanWifi() {
|
||||||
showToast('success', 'Scan WiFi', `${networks.length} réseau(x) trouvé(s)`);
|
showToast('success', 'Scan WiFi', `${networks.length} réseau(x) trouvé(s)`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error scanning WiFi:', error);
|
console.error('Error scanning WiFi:', error);
|
||||||
wifiList.innerHTML = '<div class="wifi-item loading"><span style="color: var(--danger-color);">Erreur lors du scan</span></div>';
|
|
||||||
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 = '<div class="wifi-item loading"><span style="color: var(--danger-color);">Erreur lors du scan</span></div>';
|
||||||
|
showToast('error', 'Erreur', 'Échec du scan WiFi');
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (scanBtn) {
|
if (scanBtn) {
|
||||||
scanBtn.disabled = false;
|
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 =====
|
// ===== Utility Functions =====
|
||||||
|
|
||||||
function generateSignalBars(strength) {
|
function generateSignalBars(strength) {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,18 @@ import (
|
||||||
"github.com/nemunaire/repeater/internal/wifi"
|
"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
|
// ScanWiFi handles WiFi network scanning
|
||||||
func ScanWiFi(c *gin.Context) {
|
func ScanWiFi(c *gin.Context) {
|
||||||
networks, err := wifi.ScanNetworks()
|
networks, err := wifi.ScanNetworks()
|
||||||
|
|
|
||||||
30
internal/api/handlers/websocket_wifi.go
Normal file
30
internal/api/handlers/websocket_wifi.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,6 +24,7 @@ func SetupRouter(status *models.SystemStatus, cfg *config.Config, assets embed.F
|
||||||
// WiFi endpoints
|
// WiFi endpoints
|
||||||
wifi := api.Group("/wifi")
|
wifi := api.Group("/wifi")
|
||||||
{
|
{
|
||||||
|
wifi.GET("/networks", handlers.GetWiFiNetworks)
|
||||||
wifi.GET("/scan", handlers.ScanWiFi)
|
wifi.GET("/scan", handlers.ScanWiFi)
|
||||||
wifi.POST("/connect", handlers.ConnectWiFi)
|
wifi.POST("/connect", handlers.ConnectWiFi)
|
||||||
wifi.POST("/disconnect", handlers.DisconnectWiFi)
|
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)
|
api.DELETE("/logs", handlers.ClearLogs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// WebSocket endpoint
|
// WebSocket endpoints
|
||||||
r.GET("/ws/logs", handlers.WebSocketLogs)
|
r.GET("/ws/logs", handlers.WebSocketLogs)
|
||||||
|
r.GET("/ws/wifi", handlers.WebSocketWifi)
|
||||||
|
|
||||||
// Serve static files
|
// Serve static files
|
||||||
sub, err := fs.Sub(assets, "static")
|
sub, err := fs.Sub(assets, "static")
|
||||||
|
|
|
||||||
|
|
@ -53,6 +53,12 @@ func (a *App) Initialize(cfg *config.Config) error {
|
||||||
return err
|
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
|
// Start periodic tasks
|
||||||
go a.periodicStatusUpdate()
|
go a.periodicStatusUpdate()
|
||||||
go a.periodicDeviceUpdate()
|
go a.periodicDeviceUpdate()
|
||||||
|
|
@ -71,6 +77,7 @@ func (a *App) Run(addr string) error {
|
||||||
|
|
||||||
// Shutdown gracefully shuts down the application
|
// Shutdown gracefully shuts down the application
|
||||||
func (a *App) Shutdown() {
|
func (a *App) Shutdown() {
|
||||||
|
wifi.StopEventMonitoring()
|
||||||
wifi.Close()
|
wifi.Close()
|
||||||
logging.AddLog("Système", "Application arrêtée")
|
logging.AddLog("Système", "Application arrêtée")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
140
internal/wifi/broadcaster.go
Normal file
140
internal/wifi/broadcaster.go
Normal file
|
|
@ -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
|
||||||
|
}
|
||||||
70
internal/wifi/events.go
Normal file
70
internal/wifi/events.go
Normal file
|
|
@ -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,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
223
internal/wifi/iwd/signals.go
Normal file
223
internal/wifi/iwd/signals.go
Normal file
|
|
@ -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)
|
||||||
|
}
|
||||||
|
|
@ -135,3 +135,8 @@ func (s *Station) GetConnectedNetwork() (*Network, error) {
|
||||||
|
|
||||||
return NewNetwork(s.conn, path), nil
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/godbus/dbus/v5"
|
"github.com/godbus/dbus/v5"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
"github.com/nemunaire/repeater/internal/models"
|
"github.com/nemunaire/repeater/internal/models"
|
||||||
"github.com/nemunaire/repeater/internal/wifi/iwd"
|
"github.com/nemunaire/repeater/internal/wifi/iwd"
|
||||||
)
|
)
|
||||||
|
|
@ -16,12 +17,14 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
wlanInterface string
|
wlanInterface string
|
||||||
dbusConn *dbus.Conn
|
dbusConn *dbus.Conn
|
||||||
iwdManager *iwd.Manager
|
iwdManager *iwd.Manager
|
||||||
station *iwd.Station
|
station *iwd.Station
|
||||||
agent *iwd.Agent
|
agent *iwd.Agent
|
||||||
agentManager *iwd.AgentManager
|
agentManager *iwd.AgentManager
|
||||||
|
eventMonitor *iwd.SignalMonitor
|
||||||
|
wifiBroadcaster *WifiBroadcaster
|
||||||
)
|
)
|
||||||
|
|
||||||
// Initialize initializes the WiFi service with iwd D-Bus connection
|
// 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
|
// ScanNetworks scans for available WiFi networks
|
||||||
func ScanNetworks() ([]models.WiFiNetwork, error) {
|
func ScanNetworks() ([]models.WiFiNetwork, error) {
|
||||||
// Check if already scanning
|
// Check if already scanning
|
||||||
|
|
@ -120,6 +165,11 @@ func ScanNetworks() ([]models.WiFiNetwork, error) {
|
||||||
return networks[i].Signal > networks[j].Signal
|
return networks[i].Signal > networks[j].Signal
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Broadcast to WebSocket clients if available
|
||||||
|
if wifiBroadcaster != nil {
|
||||||
|
wifiBroadcaster.BroadcastScanUpdate(networks)
|
||||||
|
}
|
||||||
|
|
||||||
return networks, nil
|
return networks, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -191,6 +241,59 @@ func GetConnectedSSID() string {
|
||||||
return props.Name
|
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
|
// mapSecurityType maps iwd security types to display format
|
||||||
func mapSecurityType(iwdType string) string {
|
func mapSecurityType(iwdType string) string {
|
||||||
switch iwdType {
|
switch iwdType {
|
||||||
|
|
|
||||||
36
openapi.yaml
36
openapi.yaml
|
|
@ -28,6 +28,42 @@ tags:
|
||||||
description: System logs and real-time monitoring
|
description: System logs and real-time monitoring
|
||||||
|
|
||||||
paths:
|
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:
|
/api/wifi/scan:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue