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 }