Implementation with iwd
This commit is contained in:
parent
2b3a5b89f8
commit
17d665e21a
10 changed files with 755 additions and 171 deletions
|
|
@ -5,7 +5,11 @@ const appState = {
|
|||
autoScrollLogs: true,
|
||||
ws: null,
|
||||
reconnectAttempts: 0,
|
||||
maxReconnectAttempts: 5
|
||||
maxReconnectAttempts: 5,
|
||||
connectedSSID: null,
|
||||
networks: [],
|
||||
uptime: 0,
|
||||
uptimeInterval: null
|
||||
};
|
||||
|
||||
// Initialize the application
|
||||
|
|
@ -30,7 +34,8 @@ async function initializeApp() {
|
|||
// Start periodic updates
|
||||
startPeriodicUpdates();
|
||||
|
||||
showToast('success', 'Connecté', 'Interface web chargée avec succès');
|
||||
// Start uptime counter
|
||||
startUptimeCounter();
|
||||
}
|
||||
|
||||
// ===== API Functions =====
|
||||
|
|
@ -62,6 +67,7 @@ async function scanWifi() {
|
|||
const response = await fetch('/api/wifi/scan');
|
||||
const networks = await response.json();
|
||||
|
||||
appState.networks = networks;
|
||||
displayWifiNetworks(networks);
|
||||
showToast('success', 'Scan WiFi', `${networks.length} réseau(x) trouvé(s)`);
|
||||
} catch (error) {
|
||||
|
|
@ -128,6 +134,10 @@ async function disconnectWifi() {
|
|||
if (response.ok) {
|
||||
showToast('success', 'Déconnecté', 'Déconnexion WiFi réussie');
|
||||
await loadStatus();
|
||||
// Force refresh WiFi list to remove green highlighting
|
||||
if (appState.networks.length > 0) {
|
||||
displayWifiNetworks(appState.networks);
|
||||
}
|
||||
} else {
|
||||
throw new Error('Disconnection failed');
|
||||
}
|
||||
|
|
@ -288,10 +298,22 @@ function updateStatusDisplay(status) {
|
|||
|
||||
appState.hotspotEnabled = status.hotspotEnabled;
|
||||
|
||||
// Check if connectedSSID changed and refresh WiFi list if needed
|
||||
const connectedSSIDChanged = appState.connectedSSID !== status.connectedSSID;
|
||||
appState.connectedSSID = status.connectedSSID;
|
||||
|
||||
if (connectedSSIDChanged && appState.networks.length > 0) {
|
||||
displayWifiNetworks(appState.networks);
|
||||
}
|
||||
|
||||
// Update stats
|
||||
document.getElementById('connectedDevices').textContent = status.connectedCount;
|
||||
document.getElementById('dataUsage').textContent = `${status.dataUsage.toFixed(1)} MB`;
|
||||
|
||||
// Update uptime in state (will be incremented by the interval)
|
||||
appState.uptime = status.uptime;
|
||||
document.getElementById('uptime').textContent = formatUptime(status.uptime);
|
||||
|
||||
document.getElementById('currentNetwork').textContent = status.connectedSSID || '-';
|
||||
}
|
||||
|
||||
|
|
@ -307,15 +329,21 @@ function displayWifiNetworks(networks) {
|
|||
networks.forEach(network => {
|
||||
const wifiItem = document.createElement('div');
|
||||
wifiItem.className = 'wifi-item';
|
||||
|
||||
// Mark the currently connected network
|
||||
if (appState.connectedSSID && network.ssid === appState.connectedSSID) {
|
||||
wifiItem.classList.add('connected');
|
||||
}
|
||||
|
||||
wifiItem.onclick = () => selectWifi(network, wifiItem);
|
||||
|
||||
wifiItem.innerHTML = `
|
||||
<div class="wifi-info">
|
||||
<div class="wifi-ssid">${escapeHtml(network.ssid)}</div>
|
||||
<div class="wifi-details">
|
||||
<span>${network.security}</span>
|
||||
<span>Canal ${network.channel}</span>
|
||||
<span>${network.bssid}</span>
|
||||
<span>${escapeHtml(network.security)}</span>
|
||||
<span>Canal ${escapeHtml(String(network.channel))}</span>
|
||||
<span>${escapeHtml(network.bssid)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wifi-signal">
|
||||
|
|
@ -533,3 +561,16 @@ function startPeriodicUpdates() {
|
|||
loadDevices();
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
function startUptimeCounter() {
|
||||
// Clear any existing interval
|
||||
if (appState.uptimeInterval) {
|
||||
clearInterval(appState.uptimeInterval);
|
||||
}
|
||||
|
||||
// Increment uptime every second
|
||||
appState.uptimeInterval = setInterval(() => {
|
||||
appState.uptime++;
|
||||
document.getElementById('uptime').textContent = formatUptime(appState.uptime);
|
||||
}, 1000);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -130,6 +130,7 @@ body {
|
|||
gap: 1rem;
|
||||
align-items: center;
|
||||
transition: all 0.3s ease;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.stat-card:hover {
|
||||
|
|
@ -154,6 +155,7 @@ body {
|
|||
|
||||
.stat-content {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
|
|
@ -162,6 +164,9 @@ body {
|
|||
color: var(--text-primary);
|
||||
line-height: 1;
|
||||
margin-bottom: 0.25rem;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
|
|
@ -413,6 +418,16 @@ body {
|
|||
border-left: 4px solid #667eea;
|
||||
}
|
||||
|
||||
.wifi-item.connected {
|
||||
background: #d1fae5;
|
||||
border-left-color: var(--success-color) !important;
|
||||
}
|
||||
|
||||
.wifi-item.connected .wifi-ssid {
|
||||
color: var(--success-color);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wifi-item.loading {
|
||||
justify-content: center;
|
||||
color: var(--text-secondary);
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ package app
|
|||
import (
|
||||
"embed"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
|
|
@ -71,6 +74,52 @@ func (a *App) Shutdown() {
|
|||
logging.AddLog("Système", "Application arrêtée")
|
||||
}
|
||||
|
||||
// getSystemUptime reads system uptime from /proc/uptime
|
||||
func getSystemUptime() int64 {
|
||||
data, err := os.ReadFile("/proc/uptime")
|
||||
if err != nil {
|
||||
log.Printf("Error reading /proc/uptime: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
fields := strings.Fields(string(data))
|
||||
if len(fields) == 0 {
|
||||
return 0
|
||||
}
|
||||
|
||||
uptime, err := strconv.ParseFloat(fields[0], 64)
|
||||
if err != nil {
|
||||
log.Printf("Error parsing uptime: %v", err)
|
||||
return 0
|
||||
}
|
||||
|
||||
return int64(uptime)
|
||||
}
|
||||
|
||||
// getInterfaceBytes reads rx and tx bytes for a network interface
|
||||
func getInterfaceBytes(interfaceName string) (rxBytes, txBytes int64) {
|
||||
rxPath := "/sys/class/net/" + interfaceName + "/statistics/rx_bytes"
|
||||
txPath := "/sys/class/net/" + interfaceName + "/statistics/tx_bytes"
|
||||
|
||||
// Read RX bytes
|
||||
rxData, err := os.ReadFile(rxPath)
|
||||
if err != nil {
|
||||
log.Printf("Error reading rx_bytes for %s: %v", interfaceName, err)
|
||||
} else {
|
||||
rxBytes, _ = strconv.ParseInt(strings.TrimSpace(string(rxData)), 10, 64)
|
||||
}
|
||||
|
||||
// Read TX bytes
|
||||
txData, err := os.ReadFile(txPath)
|
||||
if err != nil {
|
||||
log.Printf("Error reading tx_bytes for %s: %v", interfaceName, err)
|
||||
} else {
|
||||
txBytes, _ = strconv.ParseInt(strings.TrimSpace(string(txData)), 10, 64)
|
||||
}
|
||||
|
||||
return rxBytes, txBytes
|
||||
}
|
||||
|
||||
// periodicStatusUpdate updates WiFi connection status periodically
|
||||
func (a *App) periodicStatusUpdate() {
|
||||
ticker := time.NewTicker(5 * time.Second)
|
||||
|
|
@ -79,10 +128,16 @@ func (a *App) periodicStatusUpdate() {
|
|||
for range ticker.C {
|
||||
a.StatusMutex.Lock()
|
||||
a.Status.Connected = wifi.IsConnected()
|
||||
if !a.Status.Connected {
|
||||
a.Status.ConnectedSSID = ""
|
||||
a.Status.ConnectedSSID = wifi.GetConnectedSSID()
|
||||
a.Status.Uptime = getSystemUptime()
|
||||
|
||||
// Get network data usage for WiFi interface
|
||||
if a.Config != nil {
|
||||
rxBytes, txBytes := getInterfaceBytes(a.Config.WifiInterface)
|
||||
// Convert to MB and sum rx + tx
|
||||
a.Status.DataUsage = float64(rxBytes+txBytes) / (1024 * 1024)
|
||||
}
|
||||
a.Status.Uptime = int64(time.Since(a.StartTime).Seconds())
|
||||
|
||||
a.StatusMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
170
internal/wifi/iwd/agent.go
Normal file
170
internal/wifi/iwd/agent.go
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
package iwd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/godbus/dbus/v5/introspect"
|
||||
)
|
||||
|
||||
const agentIntrospectXML = `
|
||||
<node>
|
||||
<interface name="net.connman.iwd.Agent">
|
||||
<method name="Release">
|
||||
</method>
|
||||
<method name="RequestPassphrase">
|
||||
<arg name="network" type="o" direction="in"/>
|
||||
<arg name="passphrase" type="s" direction="out"/>
|
||||
</method>
|
||||
<method name="RequestPrivateKeyPassphrase">
|
||||
<arg name="network" type="o" direction="in"/>
|
||||
<arg name="passphrase" type="s" direction="out"/>
|
||||
</method>
|
||||
<method name="RequestUserNameAndPassword">
|
||||
<arg name="network" type="o" direction="in"/>
|
||||
<arg name="username" type="s" direction="out"/>
|
||||
<arg name="password" type="s" direction="out"/>
|
||||
</method>
|
||||
<method name="RequestUserPassword">
|
||||
<arg name="network" type="o" direction="in"/>
|
||||
<arg name="user" type="s" direction="in"/>
|
||||
<arg name="password" type="s" direction="out"/>
|
||||
</method>
|
||||
<method name="Cancel">
|
||||
<arg name="reason" type="s" direction="in"/>
|
||||
</method>
|
||||
</interface>
|
||||
<interface name="org.freedesktop.DBus.Introspectable">
|
||||
<method name="Introspect">
|
||||
<arg name="xml" type="s" direction="out"/>
|
||||
</method>
|
||||
</interface>
|
||||
</node>`
|
||||
|
||||
// Agent implements the net.connman.iwd.Agent interface for credential callbacks
|
||||
type Agent struct {
|
||||
conn *dbus.Conn
|
||||
path dbus.ObjectPath
|
||||
passphraseStore map[string]string
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
// NewAgent creates a new Agent instance
|
||||
func NewAgent(conn *dbus.Conn, path dbus.ObjectPath) *Agent {
|
||||
return &Agent{
|
||||
conn: conn,
|
||||
path: path,
|
||||
passphraseStore: make(map[string]string),
|
||||
}
|
||||
}
|
||||
|
||||
// SetPassphrase stores a passphrase for a given network SSID
|
||||
func (a *Agent) SetPassphrase(ssid, passphrase string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
a.passphraseStore[ssid] = passphrase
|
||||
}
|
||||
|
||||
// ClearPassphrase removes the stored passphrase for a network
|
||||
func (a *Agent) ClearPassphrase(ssid string) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
delete(a.passphraseStore, ssid)
|
||||
}
|
||||
|
||||
// Export registers the agent object on D-Bus
|
||||
func (a *Agent) Export() error {
|
||||
err := a.conn.Export(a, a.path, AgentInterface)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to export agent: %v", err)
|
||||
}
|
||||
|
||||
err = a.conn.Export(introspect.Introspectable(agentIntrospectXML), a.path, "org.freedesktop.DBus.Introspectable")
|
||||
if err != nil {
|
||||
a.conn.Export(nil, a.path, AgentInterface)
|
||||
return fmt.Errorf("failed to export introspection: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Unexport unregisters the agent from D-Bus
|
||||
func (a *Agent) Unexport() {
|
||||
a.conn.Export(nil, a.path, AgentInterface)
|
||||
a.conn.Export(nil, a.path, "org.freedesktop.DBus.Introspectable")
|
||||
}
|
||||
|
||||
// getNetworkSSID queries the network object to get its SSID (Name property)
|
||||
func (a *Agent) getNetworkSSID(networkPath dbus.ObjectPath) (string, error) {
|
||||
obj := a.conn.Object(Service, networkPath)
|
||||
variant, err := obj.GetProperty(NetworkInterface + ".Name")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get network name: %v", err)
|
||||
}
|
||||
|
||||
name, ok := variant.Value().(string)
|
||||
if !ok {
|
||||
return "", fmt.Errorf("network name is not a string")
|
||||
}
|
||||
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// RequestPassphrase is called by iwd when connecting to PSK networks
|
||||
func (a *Agent) RequestPassphrase(network dbus.ObjectPath) (string, *dbus.Error) {
|
||||
fmt.Printf("[Agent] RequestPassphrase called for network: %s\n", network)
|
||||
|
||||
ssid, err := a.getNetworkSSID(network)
|
||||
if err != nil {
|
||||
fmt.Printf("[Agent] Failed to get SSID: %v\n", err)
|
||||
return "", dbus.MakeFailedError(fmt.Errorf("failed to get network SSID: %v", err))
|
||||
}
|
||||
|
||||
fmt.Printf("[Agent] Network SSID: %s\n", ssid)
|
||||
|
||||
a.mu.RLock()
|
||||
passphrase, ok := a.passphraseStore[ssid]
|
||||
a.mu.RUnlock()
|
||||
|
||||
if !ok {
|
||||
fmt.Printf("[Agent] No passphrase stored for SSID: %s\n", ssid)
|
||||
return "", dbus.MakeFailedError(fmt.Errorf("no passphrase stored for network '%s'", ssid))
|
||||
}
|
||||
|
||||
fmt.Printf("[Agent] Returning passphrase for SSID: %s\n", ssid)
|
||||
return passphrase, nil
|
||||
}
|
||||
|
||||
// RequestPrivateKeyPassphrase is called for encrypted private keys
|
||||
func (a *Agent) RequestPrivateKeyPassphrase(network dbus.ObjectPath) (string, *dbus.Error) {
|
||||
// Not implemented for now
|
||||
return "", dbus.MakeFailedError(fmt.Errorf("RequestPrivateKeyPassphrase not implemented"))
|
||||
}
|
||||
|
||||
// RequestUserNameAndPassword is called for enterprise networks
|
||||
func (a *Agent) RequestUserNameAndPassword(network dbus.ObjectPath) (string, string, *dbus.Error) {
|
||||
// Not implemented for now
|
||||
return "", "", dbus.MakeFailedError(fmt.Errorf("RequestUserNameAndPassword not implemented"))
|
||||
}
|
||||
|
||||
// RequestUserPassword is called for enterprise networks with known username
|
||||
func (a *Agent) RequestUserPassword(network dbus.ObjectPath, user string) (string, *dbus.Error) {
|
||||
// Not implemented for now
|
||||
return "", dbus.MakeFailedError(fmt.Errorf("RequestUserPassword not implemented"))
|
||||
}
|
||||
|
||||
// Cancel is called when a request is canceled
|
||||
func (a *Agent) Cancel(reason string) *dbus.Error {
|
||||
// Nothing to do, just acknowledge
|
||||
return nil
|
||||
}
|
||||
|
||||
// Release is called when the agent is unregistered
|
||||
func (a *Agent) Release() *dbus.Error {
|
||||
// Cleanup if needed
|
||||
a.mu.Lock()
|
||||
a.passphraseStore = make(map[string]string)
|
||||
a.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
39
internal/wifi/iwd/agentmanager.go
Normal file
39
internal/wifi/iwd/agentmanager.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
package iwd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// AgentManager handles agent registration with iwd
|
||||
type AgentManager struct {
|
||||
conn *dbus.Conn
|
||||
obj dbus.BusObject
|
||||
}
|
||||
|
||||
// NewAgentManager creates a new AgentManager instance
|
||||
func NewAgentManager(conn *dbus.Conn) *AgentManager {
|
||||
return &AgentManager{
|
||||
conn: conn,
|
||||
obj: conn.Object(Service, "/net/connman/iwd"),
|
||||
}
|
||||
}
|
||||
|
||||
// RegisterAgent registers an agent with iwd
|
||||
func (am *AgentManager) RegisterAgent(agentPath dbus.ObjectPath) error {
|
||||
err := am.obj.Call(AgentManagerInterface+".RegisterAgent", 0, agentPath).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to register agent: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// UnregisterAgent unregisters an agent from iwd
|
||||
func (am *AgentManager) UnregisterAgent(agentPath dbus.ObjectPath) error {
|
||||
err := am.obj.Call(AgentManagerInterface+".UnregisterAgent", 0, agentPath).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to unregister agent: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
71
internal/wifi/iwd/manager.go
Normal file
71
internal/wifi/iwd/manager.go
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
package iwd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// Manager handles iwd object discovery via ObjectManager
|
||||
type Manager struct {
|
||||
conn *dbus.Conn
|
||||
obj dbus.BusObject
|
||||
}
|
||||
|
||||
// NewManager creates a new Manager instance
|
||||
func NewManager(conn *dbus.Conn) *Manager {
|
||||
return &Manager{
|
||||
conn: conn,
|
||||
obj: conn.Object(Service, dbus.ObjectPath(ManagerPath)),
|
||||
}
|
||||
}
|
||||
|
||||
// GetManagedObjects returns all iwd managed objects
|
||||
func (m *Manager) GetManagedObjects() (map[dbus.ObjectPath]map[string]map[string]dbus.Variant, error) {
|
||||
var objects map[dbus.ObjectPath]map[string]map[string]dbus.Variant
|
||||
err := m.obj.Call("org.freedesktop.DBus.ObjectManager.GetManagedObjects", 0).Store(&objects)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get managed objects: %v", err)
|
||||
}
|
||||
return objects, nil
|
||||
}
|
||||
|
||||
// FindStation finds the Station object for the given interface name
|
||||
func (m *Manager) FindStation(interfaceName string) (*Station, error) {
|
||||
objects, err := m.GetManagedObjects()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// First, find the device with matching interface name
|
||||
var devicePath dbus.ObjectPath
|
||||
for path, interfaces := range objects {
|
||||
if deviceProps, ok := interfaces[DeviceInterface]; ok {
|
||||
if nameVariant, ok := deviceProps["Name"]; ok {
|
||||
if name, ok := nameVariant.Value().(string); ok && name == interfaceName {
|
||||
devicePath = path
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if devicePath == "" {
|
||||
return nil, fmt.Errorf("device with interface '%s' not found", interfaceName)
|
||||
}
|
||||
|
||||
// Now find the station object under this device
|
||||
// Station path is typically the same as device path or a child of it
|
||||
for path, interfaces := range objects {
|
||||
if _, ok := interfaces[StationInterface]; ok {
|
||||
// Check if this station belongs to our device
|
||||
// Station path should be the device path or start with it
|
||||
if path == devicePath || strings.HasPrefix(string(path), string(devicePath)+"/") {
|
||||
return NewStation(m.conn, path), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("station for device '%s' not found", interfaceName)
|
||||
}
|
||||
64
internal/wifi/iwd/network.go
Normal file
64
internal/wifi/iwd/network.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
package iwd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// Network represents an iwd Network interface
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
// GetProperties retrieves all network properties
|
||||
func (n *Network) GetProperties() (*NetworkProperties, error) {
|
||||
var props map[string]dbus.Variant
|
||||
err := n.obj.Call("org.freedesktop.DBus.Properties.GetAll", 0, NetworkInterface).Store(&props)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get network properties: %v", err)
|
||||
}
|
||||
|
||||
netProps := &NetworkProperties{}
|
||||
|
||||
if nameVariant, ok := props["Name"]; ok {
|
||||
if name, ok := nameVariant.Value().(string); ok {
|
||||
netProps.Name = name
|
||||
}
|
||||
}
|
||||
|
||||
if typeVariant, ok := props["Type"]; ok {
|
||||
if netType, ok := typeVariant.Value().(string); ok {
|
||||
netProps.Type = netType
|
||||
}
|
||||
}
|
||||
|
||||
if connectedVariant, ok := props["Connected"]; ok {
|
||||
if connected, ok := connectedVariant.Value().(bool); ok {
|
||||
netProps.Connected = connected
|
||||
}
|
||||
}
|
||||
|
||||
return netProps, nil
|
||||
}
|
||||
|
||||
// Connect initiates a connection to this network
|
||||
// Credentials are provided via the registered agent's RequestPassphrase callback
|
||||
func (n *Network) Connect() error {
|
||||
err := n.obj.Call(NetworkInterface+".Connect", 0).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
137
internal/wifi/iwd/station.go
Normal file
137
internal/wifi/iwd/station.go
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
package iwd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
)
|
||||
|
||||
// Station represents an iwd Station interface
|
||||
type Station struct {
|
||||
path dbus.ObjectPath
|
||||
conn *dbus.Conn
|
||||
obj dbus.BusObject
|
||||
}
|
||||
|
||||
// NewStation creates a new Station instance
|
||||
func NewStation(conn *dbus.Conn, path dbus.ObjectPath) *Station {
|
||||
return &Station{
|
||||
path: path,
|
||||
conn: conn,
|
||||
obj: conn.Object(Service, path),
|
||||
}
|
||||
}
|
||||
|
||||
// Scan triggers a network scan
|
||||
func (s *Station) Scan() error {
|
||||
err := s.obj.Call(StationInterface+".Scan", 0).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("scan failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsScanning checks if a scan is currently in progress
|
||||
func (s *Station) IsScanning() (bool, error) {
|
||||
prop, err := s.obj.GetProperty(StationInterface + ".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
|
||||
}
|
||||
|
||||
// GetOrderedNetworks returns networks sorted by signal strength
|
||||
func (s *Station) GetOrderedNetworks() ([]NetworkInfo, error) {
|
||||
var result []struct {
|
||||
Path dbus.ObjectPath
|
||||
Signal int16
|
||||
}
|
||||
|
||||
err := s.obj.Call(StationInterface+".GetOrderedNetworks", 0).Store(&result)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ordered networks: %v", err)
|
||||
}
|
||||
|
||||
networks := make([]NetworkInfo, len(result))
|
||||
for i, r := range result {
|
||||
networks[i] = NetworkInfo{
|
||||
Path: r.Path,
|
||||
Signal: r.Signal,
|
||||
}
|
||||
}
|
||||
|
||||
return networks, nil
|
||||
}
|
||||
|
||||
// GetState returns the current connection state
|
||||
func (s *Station) GetState() (StationState, error) {
|
||||
prop, err := s.obj.GetProperty(StationInterface + ".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 StationState(state), nil
|
||||
}
|
||||
|
||||
// Disconnect disconnects from the current network
|
||||
func (s *Station) Disconnect() error {
|
||||
err := s.obj.Call(StationInterface+".Disconnect", 0).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("disconnect failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetNetwork finds and returns a Network object by SSID
|
||||
func (s *Station) GetNetwork(ssid string) (*Network, error) {
|
||||
networks, err := s.GetOrderedNetworks()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Find the network with matching SSID
|
||||
for _, netInfo := range networks {
|
||||
network := NewNetwork(s.conn, netInfo.Path)
|
||||
props, err := network.GetProperties()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if props.Name == ssid {
|
||||
return network, nil
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("network '%s' not found", ssid)
|
||||
}
|
||||
|
||||
// GetConnectedNetwork returns the currently connected network
|
||||
func (s *Station) GetConnectedNetwork() (*Network, error) {
|
||||
prop, err := s.obj.GetProperty(StationInterface + ".ConnectedNetwork")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ConnectedNetwork property: %v", err)
|
||||
}
|
||||
|
||||
path, ok := prop.Value().(dbus.ObjectPath)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("ConnectedNetwork property is not an ObjectPath")
|
||||
}
|
||||
|
||||
// Check if path is empty (not connected)
|
||||
if path == "/" || path == "" {
|
||||
return nil, fmt.Errorf("not connected to any network")
|
||||
}
|
||||
|
||||
return NewNetwork(s.conn, path), nil
|
||||
}
|
||||
38
internal/wifi/iwd/types.go
Normal file
38
internal/wifi/iwd/types.go
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
package iwd
|
||||
|
||||
import "github.com/godbus/dbus/v5"
|
||||
|
||||
const (
|
||||
// D-Bus service and interfaces
|
||||
Service = "net.connman.iwd"
|
||||
ManagerPath = "/"
|
||||
DeviceInterface = "net.connman.iwd.Device"
|
||||
StationInterface = "net.connman.iwd.Station"
|
||||
NetworkInterface = "net.connman.iwd.Network"
|
||||
AgentInterface = "net.connman.iwd.Agent"
|
||||
AgentManagerInterface = "net.connman.iwd.AgentManager"
|
||||
)
|
||||
|
||||
// NetworkInfo represents a network with its signal strength
|
||||
type NetworkInfo struct {
|
||||
Path dbus.ObjectPath
|
||||
Signal int16 // 100 * dBm (0 to -10000)
|
||||
}
|
||||
|
||||
// NetworkProperties holds network properties
|
||||
type NetworkProperties struct {
|
||||
Name string // SSID
|
||||
Type string // "open", "wep", "psk", "8021x"
|
||||
Connected bool
|
||||
}
|
||||
|
||||
// StationState represents the connection state
|
||||
type StationState string
|
||||
|
||||
const (
|
||||
StateConnected StationState = "connected"
|
||||
StateDisconnected StationState = "disconnected"
|
||||
StateConnecting StationState = "connecting"
|
||||
StateDisconnecting StationState = "disconnecting"
|
||||
StateRoaming StationState = "roaming"
|
||||
)
|
||||
|
|
@ -2,48 +2,67 @@ package wifi
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/godbus/dbus/v5"
|
||||
"github.com/nemunaire/repeater/internal/models"
|
||||
"github.com/nemunaire/repeater/internal/wifi/iwd"
|
||||
)
|
||||
|
||||
const (
|
||||
WPA_CONF = "/etc/wpa_supplicant/wpa_supplicant.conf"
|
||||
|
||||
// D-Bus constants for wpa_supplicant
|
||||
WPA_SUPPLICANT_SERVICE = "fi.w1.wpa_supplicant1"
|
||||
WPA_SUPPLICANT_PATH = "/fi/w1/wpa_supplicant1"
|
||||
WPA_SUPPLICANT_IFACE = "fi.w1.wpa_supplicant1"
|
||||
WPA_INTERFACE_IFACE = "fi.w1.wpa_supplicant1.Interface"
|
||||
WPA_BSS_IFACE = "fi.w1.wpa_supplicant1.BSS"
|
||||
WPA_NETWORK_IFACE = "fi.w1.wpa_supplicant1.Network"
|
||||
AGENT_PATH = "/com/github/nemunaire/repeater/agent"
|
||||
)
|
||||
|
||||
var (
|
||||
wlanInterface string
|
||||
dbusConn *dbus.Conn
|
||||
wpaSupplicant dbus.BusObject
|
||||
iwdManager *iwd.Manager
|
||||
station *iwd.Station
|
||||
agent *iwd.Agent
|
||||
agentManager *iwd.AgentManager
|
||||
)
|
||||
|
||||
// Initialize initializes the WiFi service with D-Bus connection
|
||||
// Initialize initializes the WiFi service with iwd D-Bus connection
|
||||
func Initialize(interfaceName string) error {
|
||||
wlanInterface = interfaceName
|
||||
var err error
|
||||
|
||||
// Connect to D-Bus
|
||||
dbusConn, err = dbus.SystemBus()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to D-Bus: %v", err)
|
||||
return fmt.Errorf("échec de connexion à D-Bus: %v", err)
|
||||
}
|
||||
|
||||
// Find station for interface
|
||||
iwdManager = iwd.NewManager(dbusConn)
|
||||
station, err = iwdManager.FindStation(interfaceName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("impossible de trouver la station pour %s: %v", interfaceName, err)
|
||||
}
|
||||
|
||||
// Create and register agent for credential callbacks
|
||||
agent = iwd.NewAgent(dbusConn, dbus.ObjectPath(AGENT_PATH))
|
||||
if err := agent.Export(); err != nil {
|
||||
return fmt.Errorf("échec de l'export de l'agent: %v", err)
|
||||
}
|
||||
|
||||
agentManager = iwd.NewAgentManager(dbusConn)
|
||||
if err := agentManager.RegisterAgent(dbus.ObjectPath(AGENT_PATH)); err != nil {
|
||||
agent.Unexport()
|
||||
return fmt.Errorf("échec de l'enregistrement de l'agent: %v", err)
|
||||
}
|
||||
|
||||
wpaSupplicant = dbusConn.Object(WPA_SUPPLICANT_SERVICE, dbus.ObjectPath(WPA_SUPPLICANT_PATH))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close closes the D-Bus connection
|
||||
// Close closes the D-Bus connection and unregisters the agent
|
||||
func Close() {
|
||||
if agentManager != nil && agent != nil {
|
||||
agentManager.UnregisterAgent(dbus.ObjectPath(AGENT_PATH))
|
||||
agent.Unexport()
|
||||
}
|
||||
if dbusConn != nil {
|
||||
dbusConn.Close()
|
||||
}
|
||||
|
|
@ -51,100 +70,52 @@ func Close() {
|
|||
|
||||
// ScanNetworks scans for available WiFi networks
|
||||
func ScanNetworks() ([]models.WiFiNetwork, error) {
|
||||
interfacePath, err := getWiFiInterfacePath()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
|
||||
}
|
||||
|
||||
wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
|
||||
|
||||
// Check current scanning state
|
||||
scanning, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".Scanning")
|
||||
if err == nil && scanning.Value().(bool) {
|
||||
// Scan already in progress, wait for it to complete
|
||||
// Check if already scanning
|
||||
scanning, err := station.IsScanning()
|
||||
if err == nil && scanning {
|
||||
time.Sleep(3 * time.Second)
|
||||
} else {
|
||||
// Trigger a scan
|
||||
call := wifiInterface.Call(WPA_INTERFACE_IFACE+".Scan", 0, map[string]dbus.Variant{"Type": dbus.MakeVariant("active")})
|
||||
if call.Err != nil {
|
||||
// If scan is rejected, it might be too soon after a previous scan
|
||||
// Try to use cached results instead
|
||||
if strings.Contains(call.Err.Error(), "rejected") {
|
||||
// Continue to retrieve existing BSS list
|
||||
} else {
|
||||
return nil, fmt.Errorf("erreur lors du scan: %v", call.Err)
|
||||
}
|
||||
} else {
|
||||
// Wait for scan to complete
|
||||
time.Sleep(2 * time.Second)
|
||||
// Trigger scan
|
||||
err := station.Scan()
|
||||
if err != nil && !strings.Contains(err.Error(), "rejected") {
|
||||
return nil, fmt.Errorf("erreur lors du scan: %v", err)
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
// Retrieve BSS list
|
||||
bssePaths, err := wifiInterface.GetProperty(WPA_INTERFACE_IFACE + ".BSSs")
|
||||
// Get ordered networks
|
||||
networkInfos, err := station.GetOrderedNetworks()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("erreur lors de la récupération des BSS: %v", err)
|
||||
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 _, bssPath := range bssePaths.Value().([]dbus.ObjectPath) {
|
||||
bss := dbusConn.Object(WPA_SUPPLICANT_SERVICE, bssPath)
|
||||
|
||||
// Get BSS properties
|
||||
var props map[string]dbus.Variant
|
||||
err = bss.Call("org.freedesktop.DBus.Properties.GetAll", 0, WPA_BSS_IFACE).Store(&props)
|
||||
for _, netInfo := range networkInfos {
|
||||
network := iwd.NewNetwork(dbusConn, netInfo.Path)
|
||||
props, err := network.GetProperties()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
network := models.WiFiNetwork{}
|
||||
|
||||
// Extract SSID
|
||||
if ssidBytes, ok := props["SSID"].Value().([]byte); ok {
|
||||
network.SSID = string(ssidBytes)
|
||||
}
|
||||
|
||||
// Skip duplicates and empty SSIDs
|
||||
if network.SSID == "" || seenSSIDs[network.SSID] {
|
||||
if props.Name == "" || seenSSIDs[props.Name] {
|
||||
continue
|
||||
}
|
||||
seenSSIDs[network.SSID] = true
|
||||
seenSSIDs[props.Name] = true
|
||||
|
||||
// Extract BSSID
|
||||
if bssidBytes, ok := props["BSSID"].Value().([]byte); ok {
|
||||
network.BSSID = fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x",
|
||||
bssidBytes[0], bssidBytes[1], bssidBytes[2], bssidBytes[3], bssidBytes[4], bssidBytes[5])
|
||||
wifiNet := models.WiFiNetwork{
|
||||
SSID: props.Name,
|
||||
Signal: signalToStrength(int(netInfo.Signal) / 100),
|
||||
Security: mapSecurityType(props.Type),
|
||||
BSSID: generateSyntheticBSSID(props.Name),
|
||||
Channel: 0,
|
||||
}
|
||||
|
||||
// Extract signal strength
|
||||
if signal, ok := props["Signal"].Value().(int16); ok {
|
||||
network.Signal = signalToStrength(int(signal))
|
||||
}
|
||||
|
||||
// Extract frequency and calculate channel
|
||||
if frequency, ok := props["Frequency"].Value().(uint16); ok {
|
||||
network.Channel = frequencyToChannel(int(frequency))
|
||||
}
|
||||
|
||||
// Determine security
|
||||
if privacyVal, ok := props["Privacy"].Value().(bool); ok && privacyVal {
|
||||
if wpaProps, ok := props["WPA"].Value().(map[string]dbus.Variant); ok && len(wpaProps) > 0 {
|
||||
network.Security = "WPA"
|
||||
} else if rsnProps, ok := props["RSN"].Value().(map[string]dbus.Variant); ok && len(rsnProps) > 0 {
|
||||
network.Security = "WPA2"
|
||||
} else {
|
||||
network.Security = "WEP"
|
||||
}
|
||||
} else {
|
||||
network.Security = "Open"
|
||||
}
|
||||
|
||||
networks = append(networks, network)
|
||||
networks = append(networks, wifiNet)
|
||||
}
|
||||
|
||||
// Sort by signal strength
|
||||
// Sort by signal strength (descending)
|
||||
sort.Slice(networks, func(i, j int) bool {
|
||||
return networks[i].Signal > networks[j].Signal
|
||||
})
|
||||
|
|
@ -152,37 +123,32 @@ func ScanNetworks() ([]models.WiFiNetwork, error) {
|
|||
return networks, nil
|
||||
}
|
||||
|
||||
// Connect connects to a WiFi network using D-Bus
|
||||
// Connect connects to a WiFi network using iwd agent callback
|
||||
func Connect(ssid, password string) error {
|
||||
interfacePath, err := getWiFiInterfacePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
|
||||
}
|
||||
|
||||
wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
|
||||
|
||||
// Create a new network
|
||||
networkConfig := map[string]dbus.Variant{
|
||||
"ssid": dbus.MakeVariant(ssid),
|
||||
}
|
||||
|
||||
// Store passphrase in agent for callback
|
||||
if password != "" {
|
||||
networkConfig["psk"] = dbus.MakeVariant(password)
|
||||
agent.SetPassphrase(ssid, password)
|
||||
}
|
||||
|
||||
var networkPath dbus.ObjectPath
|
||||
err = wifiInterface.Call(WPA_INTERFACE_IFACE+".AddNetwork", 0, networkConfig).Store(&networkPath)
|
||||
// Ensure passphrase is cleared after connection attempt
|
||||
defer func() {
|
||||
if password != "" {
|
||||
agent.ClearPassphrase(ssid)
|
||||
}
|
||||
}()
|
||||
|
||||
// Get network object
|
||||
network, err := station.GetNetwork(ssid)
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de l'ajout du réseau: %v", err)
|
||||
return fmt.Errorf("réseau '%s' non trouvé: %v", ssid, err)
|
||||
}
|
||||
|
||||
// Select the network
|
||||
err = wifiInterface.Call(WPA_INTERFACE_IFACE+".SelectNetwork", 0, networkPath).Err
|
||||
if err != nil {
|
||||
return fmt.Errorf("erreur lors de la sélection du réseau: %v", err)
|
||||
// Connect - iwd will call agent.RequestPassphrase() if needed
|
||||
if err := network.Connect(); err != nil {
|
||||
return fmt.Errorf("erreur lors de la connexion: %v", err)
|
||||
}
|
||||
|
||||
// Wait for connection
|
||||
// Poll for connection
|
||||
for i := 0; i < 20; i++ {
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if IsConnected() {
|
||||
|
|
@ -195,81 +161,69 @@ func Connect(ssid, password string) error {
|
|||
|
||||
// Disconnect disconnects from the current WiFi network
|
||||
func Disconnect() error {
|
||||
interfacePath, err := getWiFiInterfacePath()
|
||||
if err != nil {
|
||||
return fmt.Errorf("impossible d'obtenir l'interface WiFi: %v", err)
|
||||
}
|
||||
|
||||
wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
|
||||
|
||||
// Disconnect
|
||||
err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Disconnect", 0).Err
|
||||
if err != nil {
|
||||
if err := station.Disconnect(); err != nil {
|
||||
return fmt.Errorf("erreur lors de la déconnexion: %v", err)
|
||||
}
|
||||
|
||||
// Remove all networks
|
||||
var networks []dbus.ObjectPath
|
||||
err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "Networks").Store(&networks)
|
||||
if err == nil {
|
||||
for _, networkPath := range networks {
|
||||
wifiInterface.Call(WPA_INTERFACE_IFACE+".RemoveNetwork", 0, networkPath)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// IsConnected checks if WiFi is connected using D-Bus
|
||||
// IsConnected checks if WiFi is connected using iwd
|
||||
func IsConnected() bool {
|
||||
interfacePath, err := getWiFiInterfacePath()
|
||||
state, err := station.GetState()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
wifiInterface := dbusConn.Object(WPA_SUPPLICANT_SERVICE, interfacePath)
|
||||
var state string
|
||||
err = wifiInterface.Call(WPA_INTERFACE_IFACE+".Get", 0, WPA_INTERFACE_IFACE, "State").Store(&state)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return state == "completed"
|
||||
return state == iwd.StateConnected
|
||||
}
|
||||
|
||||
// IsConnectedLegacy checks if WiFi is connected using iwconfig (fallback)
|
||||
func IsConnectedLegacy() bool {
|
||||
cmd := exec.Command("iwconfig", wlanInterface)
|
||||
output, err := cmd.Output()
|
||||
// GetConnectedSSID returns the SSID of the currently connected network
|
||||
func GetConnectedSSID() string {
|
||||
network, err := station.GetConnectedNetwork()
|
||||
if err != nil {
|
||||
return false
|
||||
return ""
|
||||
}
|
||||
|
||||
return strings.Contains(string(output), "Access Point:")
|
||||
props, err := network.GetProperties()
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return props.Name
|
||||
}
|
||||
|
||||
// getWiFiInterfacePath retrieves the D-Bus path for the WiFi interface
|
||||
func getWiFiInterfacePath() (dbus.ObjectPath, error) {
|
||||
var interfacePath dbus.ObjectPath
|
||||
err := wpaSupplicant.Call(WPA_SUPPLICANT_IFACE+".GetInterface", 0, wlanInterface).Store(&interfacePath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("erreur lors de la récupération des interfaces: %v", err)
|
||||
// mapSecurityType maps iwd security types to display format
|
||||
func mapSecurityType(iwdType string) string {
|
||||
switch iwdType {
|
||||
case "open":
|
||||
return "Open"
|
||||
case "wep":
|
||||
return "WEP"
|
||||
case "psk":
|
||||
return "WPA2"
|
||||
case "8021x":
|
||||
return "WPA2"
|
||||
default:
|
||||
return "WPA2"
|
||||
}
|
||||
|
||||
return interfacePath, nil
|
||||
}
|
||||
|
||||
// frequencyToChannel converts WiFi frequency to channel number
|
||||
func frequencyToChannel(frequency int) int {
|
||||
if frequency >= 2412 && frequency <= 2484 {
|
||||
if frequency == 2484 {
|
||||
return 14
|
||||
}
|
||||
return (frequency-2412)/5 + 1
|
||||
} else if frequency >= 5170 && frequency <= 5825 {
|
||||
return (frequency - 5000) / 5
|
||||
// generateSyntheticBSSID generates a consistent fake BSSID from SSID
|
||||
func generateSyntheticBSSID(ssid string) string {
|
||||
// Use a simple hash approach - consistent per SSID
|
||||
hash := 0
|
||||
for _, c := range ssid {
|
||||
hash = ((hash << 5) - hash) + int(c)
|
||||
}
|
||||
return 0
|
||||
|
||||
// Generate 6 bytes for MAC address
|
||||
b1 := byte((hash >> 0) & 0xff)
|
||||
b2 := byte((hash >> 8) & 0xff)
|
||||
b3 := byte((hash >> 16) & 0xff)
|
||||
b4 := byte((hash >> 24) & 0xff)
|
||||
b5 := byte(len(ssid) & 0xff)
|
||||
b6 := byte((len(ssid) >> 8) & 0xff)
|
||||
|
||||
return fmt.Sprintf("%02x:%02x:%02x:%02x:%02x:%02x", b1, b2, b3, b4, b5, b6)
|
||||
}
|
||||
|
||||
// signalToStrength converts signal level (dBm) to strength (1-5)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue