repeater/internal/station/arp/backend.go

177 lines
3.9 KiB
Go

package arp
import (
"sync"
"time"
"github.com/nemunaire/repeater/internal/station/backend"
)
// Backend implements StationBackend using ARP table discovery
type Backend struct {
arpTablePath string
lastStations map[string]backend.Station // Key: MAC address
callbacks backend.EventCallbacks
stopChan chan struct{}
mu sync.RWMutex
running bool
}
// NewBackend creates a new ARP backend
func NewBackend() *Backend {
return &Backend{
lastStations: make(map[string]backend.Station),
stopChan: make(chan struct{}),
}
}
// Initialize initializes the ARP backend
func (b *Backend) Initialize(config backend.BackendConfig) error {
b.mu.Lock()
defer b.mu.Unlock()
b.arpTablePath = config.ARPTablePath
if b.arpTablePath == "" {
b.arpTablePath = "/proc/net/arp"
}
return nil
}
// Close cleans up backend resources
func (b *Backend) Close() error {
b.StopEventMonitoring()
return nil
}
// GetStations returns all connected stations from ARP table
func (b *Backend) GetStations() ([]backend.Station, error) {
b.mu.RLock()
arpTablePath := b.arpTablePath
b.mu.RUnlock()
arpEntries, err := parseARPTable(arpTablePath)
if err != nil {
return nil, err
}
var stations []backend.Station
for _, entry := range arpEntries {
// Only include entries with valid flags (2 = COMPLETE, 6 = COMPLETE|PERM)
if entry.Flags == 2 || entry.Flags == 6 {
st := backend.Station{
MAC: entry.HWAddress.String(),
IP: entry.IP.String(),
Hostname: "", // No hostname available from ARP
Type: backend.GuessDeviceType("", entry.HWAddress.String()),
Signal: 0, // Not available from ARP
RxBytes: 0, // Not available from ARP
TxBytes: 0, // Not available from ARP
ConnectedAt: time.Now(),
}
stations = append(stations, st)
}
}
return stations, nil
}
// StartEventMonitoring starts monitoring for station events via polling
func (b *Backend) StartEventMonitoring(callbacks backend.EventCallbacks) error {
b.mu.Lock()
defer b.mu.Unlock()
if b.running {
return nil
}
b.callbacks = callbacks
b.running = true
// Start polling goroutine
go b.pollLoop()
return nil
}
// StopEventMonitoring stops event monitoring
func (b *Backend) StopEventMonitoring() {
b.mu.Lock()
if !b.running {
b.mu.Unlock()
return
}
b.running = false
b.mu.Unlock()
close(b.stopChan)
}
// SupportsRealTimeEvents returns false (ARP is polling-based)
func (b *Backend) SupportsRealTimeEvents() bool {
return false
}
// pollLoop polls the ARP table and simulates events
func (b *Backend) pollLoop() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <-ticker.C:
b.checkForChanges()
case <-b.stopChan:
return
}
}
}
// checkForChanges compares current state with last state and triggers callbacks
func (b *Backend) checkForChanges() {
// Get current stations
current, err := b.GetStations()
if err != nil {
return
}
// Build map of current stations
currentMap := make(map[string]backend.Station)
for _, station := range current {
currentMap[station.MAC] = station
}
b.mu.Lock()
defer b.mu.Unlock()
// Check for new stations (connected)
for mac, station := range currentMap {
if _, exists := b.lastStations[mac]; !exists {
// New station connected
if b.callbacks.OnStationConnected != nil {
go b.callbacks.OnStationConnected(station)
}
} else {
// Check for updates (IP change, etc.)
oldStation := b.lastStations[mac]
if oldStation.IP != station.IP || oldStation.Hostname != station.Hostname {
if b.callbacks.OnStationUpdated != nil {
go b.callbacks.OnStationUpdated(station)
}
}
}
}
// Check for disconnected stations
for mac := range b.lastStations {
if _, exists := currentMap[mac]; !exists {
// Station disconnected
if b.callbacks.OnStationDisconnected != nil {
go b.callbacks.OnStationDisconnected(mac)
}
}
}
// Update last state
b.lastStations = currentMap
}