package wpasupplicant import ( "log" "sync" "github.com/godbus/dbus/v5" "github.com/nemunaire/repeater/internal/wifi/backend" ) // SignalMonitor monitors D-Bus signals from wpa_supplicant type SignalMonitor struct { conn *dbus.Conn iface *WPAInterface callbacks backend.EventCallbacks // Signal channel signalChan chan *dbus.Signal // Control stopChan chan struct{} mu sync.RWMutex running bool // State tracking lastState WPAState } // NewSignalMonitor creates a new signal monitor func NewSignalMonitor(conn *dbus.Conn, iface *WPAInterface) *SignalMonitor { return &SignalMonitor{ conn: conn, iface: iface, signalChan: make(chan *dbus.Signal, 100), stopChan: make(chan struct{}), } } // Start begins monitoring D-Bus signals func (sm *SignalMonitor) Start(callbacks backend.EventCallbacks) error { sm.mu.Lock() if sm.running { sm.mu.Unlock() return nil } sm.running = true sm.callbacks = callbacks sm.mu.Unlock() interfacePath := sm.iface.GetPath() // Add signal match for PropertiesChanged on Interface matchOptions := []dbus.MatchOption{ dbus.WithMatchObjectPath(interfacePath), 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 } // Add signal match for ScanDone scanDoneOptions := []dbus.MatchOption{ dbus.WithMatchObjectPath(interfacePath), dbus.WithMatchInterface(InterfaceInterface), dbus.WithMatchMember("ScanDone"), } if err := sm.conn.AddMatchSignal(scanDoneOptions...); err != nil { sm.mu.Lock() sm.running = false sm.mu.Unlock() return err } // Register signal channel sm.conn.Signal(sm.signalChan) // Get initial state state, err := sm.iface.GetState() if err == nil { sm.lastState = state } // Start monitoring goroutine go sm.monitor() log.Printf("D-Bus signal monitoring started for wpa_supplicant interface %s", interfacePath) 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 for wpa_supplicant") } // 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) { // Handle ScanDone signal if sig.Name == InterfaceInterface+".ScanDone" { sm.handleScanDone(sig) return } // Handle PropertiesChanged signals if sig.Name != "org.freedesktop.DBus.Properties.PropertiesChanged" { return } // Verify signal is from Interface if len(sig.Body) < 2 { return } interfaceName, ok := sig.Body[0].(string) if !ok || interfaceName != InterfaceInterface { 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(WPAState(state)) } } // Check for CurrentBSS property change (connection status) if _, ok := changedProps["CurrentBSS"]; ok { // BSS changed, trigger state update sm.handleConnectionChange() } } // handleStateChange processes a state change func (sm *SignalMonitor) handleStateChange(state WPAState) { sm.lastState = state sm.mu.RLock() callback := sm.callbacks.OnStateChange sm.mu.RUnlock() if callback == nil { return } // Map wpa_supplicant state to backend state backendState := mapWPAState(state) // Get connected SSID if connected ssid := "" if backendState == backend.StateConnected { if bssPath, err := sm.iface.GetCurrentBSS(); err == nil && bssPath != "/" { bss := NewBSS(sm.conn, bssPath) if ssidStr, err := bss.GetSSIDString(); err == nil { ssid = ssidStr } } } callback(backendState, ssid) } // handleConnectionChange processes connection changes func (sm *SignalMonitor) handleConnectionChange() { // Get current state and trigger state change callback state, err := sm.iface.GetState() if err != nil { return } sm.handleStateChange(state) } // handleScanDone processes scan completion func (sm *SignalMonitor) handleScanDone(sig *dbus.Signal) { sm.mu.RLock() callback := sm.callbacks.OnScanComplete sm.mu.RUnlock() if callback != nil { callback() } } // mapWPAState maps wpa_supplicant states to backend-agnostic states func mapWPAState(state WPAState) backend.ConnectionState { switch state { case StateCompleted: return backend.StateConnected case StateAuthenticating, StateAssociating, StateAssociated, State4WayHandshake, StateGroupHandshake: return backend.StateConnecting case StateDisconnected, StateInactive, StateInterfaceDisabled: return backend.StateDisconnected case StateScanning: // Keep as disconnected if just scanning return backend.StateDisconnected default: return backend.StateDisconnected } }