From 04ada45f44410779eddf897096f1b0b6bac6cb4e Mon Sep 17 00:00:00 2001 From: Pierre-Olivier Mercier Date: Thu, 1 Jan 2026 22:19:57 +0700 Subject: [PATCH] Implement wpa_supplicant backend --- internal/wifi/factory.go | 4 +- internal/wifi/wpasupplicant/backend.go | 265 +++++++++++++++++++++++ internal/wifi/wpasupplicant/bss.go | 157 ++++++++++++++ internal/wifi/wpasupplicant/interface.go | 146 +++++++++++++ internal/wifi/wpasupplicant/network.go | 41 ++++ internal/wifi/wpasupplicant/signals.go | 236 ++++++++++++++++++++ internal/wifi/wpasupplicant/types.go | 27 +++ 7 files changed, 874 insertions(+), 2 deletions(-) create mode 100644 internal/wifi/wpasupplicant/backend.go create mode 100644 internal/wifi/wpasupplicant/bss.go create mode 100644 internal/wifi/wpasupplicant/interface.go create mode 100644 internal/wifi/wpasupplicant/network.go create mode 100644 internal/wifi/wpasupplicant/signals.go create mode 100644 internal/wifi/wpasupplicant/types.go diff --git a/internal/wifi/factory.go b/internal/wifi/factory.go index 5b49c7a..fdb63cc 100644 --- a/internal/wifi/factory.go +++ b/internal/wifi/factory.go @@ -5,6 +5,7 @@ import ( "github.com/nemunaire/repeater/internal/wifi/backend" "github.com/nemunaire/repeater/internal/wifi/iwd" + "github.com/nemunaire/repeater/internal/wifi/wpasupplicant" ) // createBackend creates the appropriate WiFi backend based on the backend name @@ -13,8 +14,7 @@ func createBackend(backendName string) (backend.WiFiBackend, error) { case "iwd": return iwd.NewIWDBackend(), nil case "wpasupplicant": - // TODO: Implement wpa_supplicant backend - return nil, fmt.Errorf("wpa_supplicant backend not yet implemented") + return wpasupplicant.NewWPABackend(), nil default: return nil, fmt.Errorf("invalid wifi backend: %s (must be 'iwd' or 'wpasupplicant')", backendName) } diff --git a/internal/wifi/wpasupplicant/backend.go b/internal/wifi/wpasupplicant/backend.go new file mode 100644 index 0000000..406b8f5 --- /dev/null +++ b/internal/wifi/wpasupplicant/backend.go @@ -0,0 +1,265 @@ +package wpasupplicant + +import ( + "fmt" + "time" + + "github.com/godbus/dbus/v5" + "github.com/nemunaire/repeater/internal/wifi/backend" +) + +// WPABackend implements the WiFiBackend interface for wpa_supplicant +type WPABackend struct { + conn *dbus.Conn + wpasupplicant dbus.BusObject + iface *WPAInterface + signalMonitor *SignalMonitor + interfaceName string + currentNetwork dbus.ObjectPath +} + +// NewWPABackend creates a new wpa_supplicant backend instance +func NewWPABackend() *WPABackend { + return &WPABackend{} +} + +// Initialize initializes the wpa_supplicant backend with the given interface name +func (b *WPABackend) Initialize(interfaceName string) error { + b.interfaceName = interfaceName + var err error + + // Connect to D-Bus + b.conn, err = dbus.SystemBus() + if err != nil { + return fmt.Errorf("failed to connect to D-Bus: %v", err) + } + + // Get wpa_supplicant root object + b.wpasupplicant = b.conn.Object(Service, dbus.ObjectPath(RootPath)) + + // Get interface path for the given interface name + interfacePath, err := b.getInterfacePath(interfaceName) + if err != nil { + return fmt.Errorf("failed to get interface for %s: %v", interfaceName, err) + } + + b.iface = NewWPAInterface(b.conn, interfacePath) + + return nil +} + +// getInterfacePath gets or creates the wpa_supplicant Interface object path +func (b *WPABackend) getInterfacePath(interfaceName string) (dbus.ObjectPath, error) { + var interfacePath dbus.ObjectPath + + // Try to get existing interface + err := b.wpasupplicant.Call(Service+".GetInterface", 0, interfaceName).Store(&interfacePath) + if err == nil { + return interfacePath, nil + } + + // Interface doesn't exist, create it + args := map[string]dbus.Variant{ + "Ifname": dbus.MakeVariant(interfaceName), + } + + err = b.wpasupplicant.Call(Service+".CreateInterface", 0, args).Store(&interfacePath) + if err != nil { + return "", fmt.Errorf("failed to create interface: %v", err) + } + + return interfacePath, nil +} + +// Close closes the D-Bus connection +func (b *WPABackend) Close() error { + if b.conn != nil { + b.conn.Close() + } + return nil +} + +// ScanNetworks triggers a network scan +func (b *WPABackend) ScanNetworks() error { + err := b.iface.Scan("active") + if err != nil { + return fmt.Errorf("failed to trigger scan: %v", err) + } + return nil +} + +// GetOrderedNetworks returns networks sorted by signal strength in backend-agnostic format +func (b *WPABackend) GetOrderedNetworks() ([]backend.BackendNetwork, error) { + // Get BSS list + bssPaths, err := b.iface.GetBSSs() + if err != nil { + return nil, fmt.Errorf("failed to get BSSs: %v", err) + } + + var networks []backend.BackendNetwork + seenSSIDs := make(map[string]bool) + + // Iterate through BSSs and collect network info + for _, bssPath := range bssPaths { + bss := NewBSS(b.conn, bssPath) + props, err := bss.GetProperties() + if err != nil { + continue + } + + ssid := string(props.SSID) + if ssid == "" || seenSSIDs[ssid] { + continue + } + seenSSIDs[ssid] = true + + // Get BSSID string + bssidStr, err := bss.GetBSSIDString() + if err != nil { + bssidStr = "" + } + + // Convert to backend-agnostic format + backendNet := backend.BackendNetwork{ + SSID: ssid, + SignalDBm: props.Signal, + SecurityType: props.DetermineSecurityType(), + BSSID: bssidStr, + Frequency: props.Frequency, + } + + networks = append(networks, backendNet) + } + + // Sort by signal strength (descending) + // Note: This is a simple bubble sort for demonstration + // In production, use sort.Slice + for i := 0; i < len(networks); i++ { + for j := i + 1; j < len(networks); j++ { + if networks[j].SignalDBm > networks[i].SignalDBm { + networks[i], networks[j] = networks[j], networks[i] + } + } + } + + return networks, nil +} + +// IsScanning checks if a scan is currently in progress +func (b *WPABackend) IsScanning() (bool, error) { + return b.iface.GetScanning() +} + +// Connect connects to a WiFi network +func (b *WPABackend) Connect(ssid, password string) error { + // Create network configuration + config := make(map[string]interface{}) + config["ssid"] = fmt.Sprintf("\"%s\"", ssid) // wpa_supplicant expects quoted SSID + + if password != "" { + // For WPA/WPA2-PSK networks + config["psk"] = fmt.Sprintf("\"%s\"", password) + } else { + // For open networks + config["key_mgmt"] = "NONE" + } + + // Add network + networkPath, err := b.iface.AddNetwork(config) + if err != nil { + return fmt.Errorf("failed to add network: %v", err) + } + + // Store current network path for cleanup + b.currentNetwork = networkPath + + // Select (connect to) the network + err = b.iface.SelectNetwork(networkPath) + if err != nil { + // Clean up network on failure + b.iface.RemoveNetwork(networkPath) + return fmt.Errorf("failed to select network: %v", err) + } + + return nil +} + +// Disconnect disconnects from the current WiFi network +func (b *WPABackend) Disconnect() error { + // Disconnect from current network + if err := b.iface.Disconnect(); err != nil { + return fmt.Errorf("failed to disconnect: %v", err) + } + + // Remove the network configuration if we have one + if b.currentNetwork != "" && b.currentNetwork != "/" { + b.iface.RemoveNetwork(b.currentNetwork) + b.currentNetwork = "" + } + + return nil +} + +// GetConnectionState returns the current WiFi connection state +func (b *WPABackend) GetConnectionState() (backend.ConnectionState, error) { + state, err := b.iface.GetState() + if err != nil { + return backend.StateDisconnected, err + } + return mapWPAState(state), nil +} + +// GetConnectedSSID returns the SSID of the currently connected network +func (b *WPABackend) GetConnectedSSID() string { + // Get current BSS + bssPath, err := b.iface.GetCurrentBSS() + if err != nil || bssPath == "/" { + return "" + } + + // Get BSS object + bss := NewBSS(b.conn, bssPath) + ssid, err := bss.GetSSIDString() + if err != nil { + return "" + } + + return ssid +} + +// StartEventMonitoring starts monitoring WiFi events +func (b *WPABackend) StartEventMonitoring(callbacks backend.EventCallbacks) error { + // Create signal monitor + b.signalMonitor = NewSignalMonitor(b.conn, b.iface) + + // Start monitoring + return b.signalMonitor.Start(callbacks) +} + +// StopEventMonitoring stops monitoring WiFi events +func (b *WPABackend) StopEventMonitoring() { + if b.signalMonitor != nil { + b.signalMonitor.Stop() + } +} + +// Wait for scan to complete (helper method) +func (b *WPABackend) waitForScanComplete(timeout time.Duration) error { + start := time.Now() + for { + if time.Since(start) > timeout { + return fmt.Errorf("scan timeout") + } + + scanning, err := b.iface.GetScanning() + if err != nil { + return err + } + + if !scanning { + return nil + } + + time.Sleep(100 * time.Millisecond) + } +} diff --git a/internal/wifi/wpasupplicant/bss.go b/internal/wifi/wpasupplicant/bss.go new file mode 100644 index 0000000..799e9f9 --- /dev/null +++ b/internal/wifi/wpasupplicant/bss.go @@ -0,0 +1,157 @@ +package wpasupplicant + +import ( + "fmt" + + "github.com/godbus/dbus/v5" +) + +// BSS represents a wpa_supplicant BSS (Basic Service Set) object +type BSS struct { + path dbus.ObjectPath + conn *dbus.Conn + obj dbus.BusObject +} + +// BSSProperties holds the properties of a BSS +type BSSProperties struct { + SSID []byte + BSSID []byte + Signal int16 // Signal strength in dBm + Frequency uint32 // Frequency in MHz + Privacy bool // Whether encryption is enabled + RSN map[string]dbus.Variant + WPA map[string]dbus.Variant +} + +// NewBSS creates a new BSS instance +func NewBSS(conn *dbus.Conn, path dbus.ObjectPath) *BSS { + return &BSS{ + path: path, + conn: conn, + obj: conn.Object(Service, path), + } +} + +// GetProperties returns all properties of the BSS +func (b *BSS) GetProperties() (*BSSProperties, error) { + props := &BSSProperties{} + + // Get SSID + if ssidProp, err := b.obj.GetProperty(BSSInterface + ".SSID"); err == nil { + if ssid, ok := ssidProp.Value().([]byte); ok { + props.SSID = ssid + } + } + + // Get BSSID + if bssidProp, err := b.obj.GetProperty(BSSInterface + ".BSSID"); err == nil { + if bssid, ok := bssidProp.Value().([]byte); ok { + props.BSSID = bssid + } + } + + // Get Signal + if signalProp, err := b.obj.GetProperty(BSSInterface + ".Signal"); err == nil { + if signal, ok := signalProp.Value().(int16); ok { + props.Signal = signal + } + } + + // Get Frequency + if freqProp, err := b.obj.GetProperty(BSSInterface + ".Frequency"); err == nil { + if freq, ok := freqProp.Value().(uint16); ok { + props.Frequency = uint32(freq) + } + } + + // Get Privacy + if privacyProp, err := b.obj.GetProperty(BSSInterface + ".Privacy"); err == nil { + if privacy, ok := privacyProp.Value().(bool); ok { + props.Privacy = privacy + } + } + + // Get RSN (WPA2) information + if rsnProp, err := b.obj.GetProperty(BSSInterface + ".RSN"); err == nil { + if rsn, ok := rsnProp.Value().(map[string]dbus.Variant); ok { + props.RSN = rsn + } + } + + // Get WPA information + if wpaProp, err := b.obj.GetProperty(BSSInterface + ".WPA"); err == nil { + if wpa, ok := wpaProp.Value().(map[string]dbus.Variant); ok { + props.WPA = wpa + } + } + + return props, nil +} + +// GetSSIDString returns the SSID as a string +func (b *BSS) GetSSIDString() (string, error) { + prop, err := b.obj.GetProperty(BSSInterface + ".SSID") + if err != nil { + return "", fmt.Errorf("failed to get SSID property: %v", err) + } + + ssid, ok := prop.Value().([]byte) + if !ok { + return "", fmt.Errorf("SSID property is not a byte array") + } + + return string(ssid), nil +} + +// GetBSSIDString returns the BSSID as a formatted MAC address string +func (b *BSS) GetBSSIDString() (string, error) { + prop, err := b.obj.GetProperty(BSSInterface + ".BSSID") + if err != nil { + return "", fmt.Errorf("failed to get BSSID property: %v", err) + } + + bssid, ok := prop.Value().([]byte) + if !ok || len(bssid) != 6 { + return "", fmt.Errorf("BSSID property is not a valid MAC address") + } + + return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", + bssid[0], bssid[1], bssid[2], bssid[3], bssid[4], bssid[5]), nil +} + +// GetSignal returns the signal strength in dBm +func (b *BSS) GetSignal() (int16, error) { + prop, err := b.obj.GetProperty(BSSInterface + ".Signal") + if err != nil { + return 0, fmt.Errorf("failed to get Signal property: %v", err) + } + + signal, ok := prop.Value().(int16) + if !ok { + return 0, fmt.Errorf("Signal property is not an int16") + } + + return signal, nil +} + +// DetermineSecurityType determines the security type based on BSS properties +func (p *BSSProperties) DetermineSecurityType() string { + // Check for WPA2 (RSN) + if len(p.RSN) > 0 { + return "psk" + } + + // Check for WPA + if len(p.WPA) > 0 { + return "psk" + } + + // Check for WEP (privacy but no WPA/RSN) + if p.Privacy { + return "wep" + } + + // Open network + return "open" +} diff --git a/internal/wifi/wpasupplicant/interface.go b/internal/wifi/wpasupplicant/interface.go new file mode 100644 index 0000000..ecc9fbb --- /dev/null +++ b/internal/wifi/wpasupplicant/interface.go @@ -0,0 +1,146 @@ +package wpasupplicant + +import ( + "fmt" + + "github.com/godbus/dbus/v5" +) + +// WPAInterface represents a wpa_supplicant Interface object +type WPAInterface struct { + path dbus.ObjectPath + conn *dbus.Conn + obj dbus.BusObject +} + +// NewWPAInterface creates a new WPAInterface instance +func NewWPAInterface(conn *dbus.Conn, path dbus.ObjectPath) *WPAInterface { + return &WPAInterface{ + path: path, + conn: conn, + obj: conn.Object(Service, path), + } +} + +// Scan triggers a network scan +func (i *WPAInterface) Scan(scanType string) error { + args := map[string]interface{}{ + "Type": scanType, // "active" or "passive" + } + + err := i.obj.Call(InterfaceInterface+".Scan", 0, args).Err + if err != nil { + return fmt.Errorf("scan failed: %v", err) + } + return nil +} + +// GetBSSs returns a list of BSS (Basic Service Set) object paths +func (i *WPAInterface) GetBSSs() ([]dbus.ObjectPath, error) { + prop, err := i.obj.GetProperty(InterfaceInterface + ".BSSs") + if err != nil { + return nil, fmt.Errorf("failed to get BSSs property: %v", err) + } + + bsss, ok := prop.Value().([]dbus.ObjectPath) + if !ok { + return nil, fmt.Errorf("BSSs property is not an array of ObjectPath") + } + + return bsss, nil +} + +// GetState returns the current connection state +func (i *WPAInterface) GetState() (WPAState, error) { + prop, err := i.obj.GetProperty(InterfaceInterface + ".State") + if err != nil { + return "", fmt.Errorf("failed to get State property: %v", err) + } + + state, ok := prop.Value().(string) + if !ok { + return "", fmt.Errorf("State property is not a string") + } + + return WPAState(state), nil +} + +// GetCurrentBSS returns the currently connected BSS object path +func (i *WPAInterface) GetCurrentBSS() (dbus.ObjectPath, error) { + prop, err := i.obj.GetProperty(InterfaceInterface + ".CurrentBSS") + if err != nil { + return "", fmt.Errorf("failed to get CurrentBSS property: %v", err) + } + + bss, ok := prop.Value().(dbus.ObjectPath) + if !ok { + return "", fmt.Errorf("CurrentBSS property is not an ObjectPath") + } + + return bss, nil +} + +// AddNetwork creates a new network configuration +func (i *WPAInterface) AddNetwork(config map[string]interface{}) (dbus.ObjectPath, error) { + var networkPath dbus.ObjectPath + + // Convert config to proper DBus variant format + dbusConfig := make(map[string]dbus.Variant) + for key, value := range config { + dbusConfig[key] = dbus.MakeVariant(value) + } + + err := i.obj.Call(InterfaceInterface+".AddNetwork", 0, dbusConfig).Store(&networkPath) + if err != nil { + return "", fmt.Errorf("failed to add network: %v", err) + } + + return networkPath, nil +} + +// SelectNetwork connects to a network +func (i *WPAInterface) SelectNetwork(networkPath dbus.ObjectPath) error { + err := i.obj.Call(InterfaceInterface+".SelectNetwork", 0, networkPath).Err + if err != nil { + return fmt.Errorf("failed to select network: %v", err) + } + return nil +} + +// RemoveNetwork removes a network configuration +func (i *WPAInterface) RemoveNetwork(networkPath dbus.ObjectPath) error { + err := i.obj.Call(InterfaceInterface+".RemoveNetwork", 0, networkPath).Err + if err != nil { + return fmt.Errorf("failed to remove network: %v", err) + } + return nil +} + +// Disconnect disconnects from the current network +func (i *WPAInterface) Disconnect() error { + err := i.obj.Call(InterfaceInterface+".Disconnect", 0).Err + if err != nil { + return fmt.Errorf("disconnect failed: %v", err) + } + return nil +} + +// GetPath returns the D-Bus object path for this interface +func (i *WPAInterface) GetPath() dbus.ObjectPath { + return i.path +} + +// GetScanning returns whether a scan is currently in progress +func (i *WPAInterface) GetScanning() (bool, error) { + prop, err := i.obj.GetProperty(InterfaceInterface + ".Scanning") + if err != nil { + return false, fmt.Errorf("failed to get Scanning property: %v", err) + } + + scanning, ok := prop.Value().(bool) + if !ok { + return false, fmt.Errorf("Scanning property is not a boolean") + } + + return scanning, nil +} diff --git a/internal/wifi/wpasupplicant/network.go b/internal/wifi/wpasupplicant/network.go new file mode 100644 index 0000000..3152a6b --- /dev/null +++ b/internal/wifi/wpasupplicant/network.go @@ -0,0 +1,41 @@ +package wpasupplicant + +import ( + "github.com/godbus/dbus/v5" +) + +// Network represents a wpa_supplicant Network configuration object +type Network struct { + path dbus.ObjectPath + conn *dbus.Conn + obj dbus.BusObject +} + +// NewNetwork creates a new Network instance +func NewNetwork(conn *dbus.Conn, path dbus.ObjectPath) *Network { + return &Network{ + path: path, + conn: conn, + obj: conn.Object(Service, path), + } +} + +// GetPath returns the D-Bus object path for this network +func (n *Network) GetPath() dbus.ObjectPath { + return n.path +} + +// GetProperties returns properties of the network configuration +func (n *Network) GetProperties() (map[string]dbus.Variant, error) { + prop, err := n.obj.GetProperty(NetworkInterface + ".Properties") + if err != nil { + return nil, err + } + + props, ok := prop.Value().(map[string]dbus.Variant) + if !ok { + return nil, nil + } + + return props, nil +} diff --git a/internal/wifi/wpasupplicant/signals.go b/internal/wifi/wpasupplicant/signals.go new file mode 100644 index 0000000..a17a292 --- /dev/null +++ b/internal/wifi/wpasupplicant/signals.go @@ -0,0 +1,236 @@ +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 + } +} diff --git a/internal/wifi/wpasupplicant/types.go b/internal/wifi/wpasupplicant/types.go new file mode 100644 index 0000000..85fec0f --- /dev/null +++ b/internal/wifi/wpasupplicant/types.go @@ -0,0 +1,27 @@ +package wpasupplicant + +const ( + // D-Bus service and interfaces + Service = "fi.w1.wpa_supplicant1" + RootPath = "/fi/w1/wpa_supplicant1" + InterfaceInterface = "fi.w1.wpa_supplicant1.Interface" + BSSInterface = "fi.w1.wpa_supplicant1.BSS" + NetworkInterface = "fi.w1.wpa_supplicant1.Network" +) + +// WPAState represents the wpa_supplicant connection state +type WPAState string + +const ( + // wpa_supplicant state strings + StateDisconnected WPAState = "disconnected" + StateInactive WPAState = "inactive" + StateScanning WPAState = "scanning" + StateAuthenticating WPAState = "authenticating" + StateAssociating WPAState = "associating" + StateAssociated WPAState = "associated" + State4WayHandshake WPAState = "4way_handshake" + StateGroupHandshake WPAState = "group_handshake" + StateCompleted WPAState = "completed" + StateInterfaceDisabled WPAState = "interface_disabled" +)