Hostapd already parsed signal strength and rx/tx counters but the station -> ConnectedDevice conversion threw them away. Add signalDbm, rxBytes, txBytes and connectedAt to the OpenAPI schema and the ConnectedDevice model, and centralise the conversion in station.ToConnectedDevice so handlers, the periodic refresh and the event callbacks all serialise the same shape. Two follow-on bugs surfaced while wiring this up: - The hostapd backend only stored station entries on first contact. Subsequent polls were dropped, so signal and byte counters never refreshed. Reconcile updates in checkStationChanges. - ConnectedAt was reset to time.Now() on every conversion. Track FirstSeen on HostapdStation when the station joins, and preserve the timestamp across periodic refreshes in app.go so the UI's "connected since" badge is stable. Frontend gains a metrics row on each device card with signal bars, total traffic and a live duration. Falls back gracefully when a backend (DHCP, ARP) doesn't expose these fields.
126 lines
2.6 KiB
Go
126 lines
2.6 KiB
Go
package station
|
|
|
|
import (
|
|
"sync"
|
|
|
|
"github.com/nemunaire/repeater/internal/models"
|
|
"github.com/nemunaire/repeater/internal/station/backend"
|
|
)
|
|
|
|
var (
|
|
currentBackend backend.StationBackend
|
|
mu sync.RWMutex
|
|
)
|
|
|
|
// Initialize initializes the station discovery backend
|
|
func Initialize(backendName string, config backend.BackendConfig) error {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
// Close existing backend if any
|
|
if currentBackend != nil {
|
|
currentBackend.Close()
|
|
}
|
|
|
|
// Create new backend
|
|
b, err := createBackend(backendName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Initialize the backend
|
|
if err := b.Initialize(config); err != nil {
|
|
return err
|
|
}
|
|
|
|
currentBackend = b
|
|
return nil
|
|
}
|
|
|
|
// GetStations returns all connected stations as ConnectedDevice models
|
|
func GetStations() ([]models.ConnectedDevice, error) {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
if currentBackend == nil {
|
|
return nil, nil
|
|
}
|
|
|
|
stations, err := currentBackend.GetStations()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
devices := make([]models.ConnectedDevice, len(stations))
|
|
for i, s := range stations {
|
|
devices[i] = toConnectedDevice(s)
|
|
}
|
|
|
|
return devices, nil
|
|
}
|
|
|
|
// toConnectedDevice converts a backend Station to the API model. Centralised
|
|
// so handlers, the periodic refresh and the event callbacks all serialise the
|
|
// same shape.
|
|
func toConnectedDevice(s backend.Station) models.ConnectedDevice {
|
|
return models.ConnectedDevice{
|
|
Name: s.Hostname,
|
|
Type: s.Type,
|
|
MAC: s.MAC,
|
|
IP: s.IP,
|
|
SignalDbm: s.Signal,
|
|
RxBytes: s.RxBytes,
|
|
TxBytes: s.TxBytes,
|
|
ConnectedAt: s.ConnectedAt,
|
|
}
|
|
}
|
|
|
|
// ToConnectedDevice exposes the conversion for callers in other packages.
|
|
func ToConnectedDevice(s backend.Station) models.ConnectedDevice {
|
|
return toConnectedDevice(s)
|
|
}
|
|
|
|
// StartEventMonitoring starts monitoring for station events
|
|
func StartEventMonitoring(callbacks backend.EventCallbacks) error {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
if currentBackend == nil {
|
|
return nil
|
|
}
|
|
|
|
return currentBackend.StartEventMonitoring(callbacks)
|
|
}
|
|
|
|
// StopEventMonitoring stops monitoring for station events
|
|
func StopEventMonitoring() {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
if currentBackend != nil {
|
|
currentBackend.StopEventMonitoring()
|
|
}
|
|
}
|
|
|
|
// Close closes the current backend
|
|
func Close() {
|
|
mu.Lock()
|
|
defer mu.Unlock()
|
|
|
|
if currentBackend != nil {
|
|
currentBackend.Close()
|
|
currentBackend = nil
|
|
}
|
|
}
|
|
|
|
// SupportsRealTimeEvents returns true if the current backend supports real-time events
|
|
func SupportsRealTimeEvents() bool {
|
|
mu.RLock()
|
|
defer mu.RUnlock()
|
|
|
|
if currentBackend == nil {
|
|
return false
|
|
}
|
|
|
|
return currentBackend.SupportsRealTimeEvents()
|
|
}
|