- RFMonitorStats struct for per-interface RF data (noise floor, channel width, TX power) - CollectRFMonitor with v6/v7 RouterOS version routing - WIRELESS_REGISTRATIONS NATS stream with 30-day retention (separate from DEVICE_EVENTS) - WirelessRegistrationEvent type and PublishWirelessRegistrations method - Poll cycle collects per-client registrations and RF stats, publishes combined event Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
108 lines
2.9 KiB
Go
108 lines
2.9 KiB
Go
package device
|
|
|
|
import (
|
|
"log/slog"
|
|
"strconv"
|
|
|
|
routeros "github.com/go-routeros/routeros/v3"
|
|
)
|
|
|
|
// RFMonitorStats holds per-interface RF monitoring data collected from
|
|
// the RouterOS monitor command. These stats describe the RF environment
|
|
// rather than individual clients.
|
|
type RFMonitorStats struct {
|
|
Interface string `json:"interface"`
|
|
NoiseFloor int `json:"noise_floor"` // dBm, e.g. -105
|
|
ChannelWidth string `json:"channel_width"` // e.g. "20MHz", "40MHz"
|
|
TxPower int `json:"tx_power"` // dBm, e.g. 24
|
|
RegisteredClients int `json:"registered_clients"` // count from monitor
|
|
}
|
|
|
|
// CollectRFMonitor queries the RouterOS device for per-interface RF statistics
|
|
// using the monitor command.
|
|
//
|
|
// Version routing:
|
|
// - majorVersion >= 7: runs /interface/wifi/print to list interfaces, then
|
|
// /interface/wifi/monitor for each interface.
|
|
// - majorVersion < 7: runs /interface/wireless/print to list interfaces,
|
|
// then /interface/wireless/monitor for each interface.
|
|
//
|
|
// Returns nil, nil when the device has no wireless interfaces.
|
|
func CollectRFMonitor(client *routeros.Client, majorVersion int) ([]RFMonitorStats, error) {
|
|
var printCmd, monitorCmd string
|
|
|
|
if majorVersion >= 7 {
|
|
printCmd = "/interface/wifi/print"
|
|
monitorCmd = "/interface/wifi/monitor"
|
|
} else {
|
|
printCmd = "/interface/wireless/print"
|
|
monitorCmd = "/interface/wireless/monitor"
|
|
}
|
|
|
|
// List wireless interface names.
|
|
listReply, err := client.Run(printCmd, "=.proplist=name")
|
|
if err != nil {
|
|
slog.Debug("device has no wireless interfaces for RF monitor", "error", err)
|
|
return nil, nil
|
|
}
|
|
|
|
if len(listReply.Re) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
stats := make([]RFMonitorStats, 0, len(listReply.Re))
|
|
for _, s := range listReply.Re {
|
|
ifaceName := s.Map["name"]
|
|
if ifaceName == "" {
|
|
continue
|
|
}
|
|
|
|
// Run monitor command for this interface.
|
|
monReply, monErr := client.Run(monitorCmd, "=numbers="+ifaceName, "=once=")
|
|
if monErr != nil {
|
|
slog.Debug("RF monitor command failed for interface",
|
|
"interface", ifaceName, "command", monitorCmd, "error", monErr)
|
|
continue
|
|
}
|
|
|
|
if len(monReply.Re) == 0 {
|
|
continue
|
|
}
|
|
|
|
m := monReply.Re[0].Map
|
|
entry := RFMonitorStats{
|
|
Interface: ifaceName,
|
|
}
|
|
|
|
// Noise floor.
|
|
if nf, parseErr := strconv.Atoi(m["noise-floor"]); parseErr == nil {
|
|
entry.NoiseFloor = nf
|
|
}
|
|
|
|
// Channel width: v6 uses "channel-width", v7 may use "channel" with width embedded.
|
|
if majorVersion >= 7 {
|
|
entry.ChannelWidth = m["channel"]
|
|
} else {
|
|
entry.ChannelWidth = m["channel-width"]
|
|
}
|
|
|
|
// TX power.
|
|
if txp, parseErr := strconv.Atoi(m["tx-power"]); parseErr == nil {
|
|
entry.TxPower = txp
|
|
}
|
|
|
|
// Registered clients count.
|
|
if rc, parseErr := strconv.Atoi(m["registered-clients"]); parseErr == nil {
|
|
entry.RegisteredClients = rc
|
|
}
|
|
|
|
stats = append(stats, entry)
|
|
}
|
|
|
|
if len(stats) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
return stats, nil
|
|
}
|