Add websocket wifi updates
This commit is contained in:
parent
c443fce24f
commit
1477d909b0
11 changed files with 755 additions and 10 deletions
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
|
||||
}
|
||||
|
||||
// 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"
|
||||
|
||||
"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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue