repeater/internal/station/station.go
Pierre-Olivier Mercier 70140bc289 station: Identify devices by MAC OUI vendor lookup
Embed the IEEE OUI registry (~1MB pre-processed text file) and resolve
the vendor for every station MAC. Locally administered MACs (U/L bit
set, used by iOS/Android private addresses and virtual interfaces) are
skipped so we don't return spurious matches against randomized prefixes.

The vendor name shows up in the device card as a secondary line, and
falls back to the title position when no DHCP hostname is available —
"Apple" with the IP and MAC is far more useful than "Sans nom".

The lookup table loads lazily (sync.Once) on the first call so the
~40k-entry parse only runs when the station discovery code is exercised.
2026-05-01 22:32:57 +08:00

127 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,
Vendor: s.Vendor,
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()
}