package radio import ( "math" "time" ) // Info descriptor. type Info struct { Name string `yaml:"name" json:"name"` // Name of the device Device string `yaml:"device" json:"device"` // Device type Manufacturer string `yaml:"manufacturer" json:"manufacturer"` // Device manufacturer FirmwareDate time.Time `yaml:"firmware_date" json:"firmware_date,omitempty"` // Firmware date FirmwareVersion string `yaml:"firmware_version" json:"firmware_version"` // Firmware version Antenna string `yaml:"antenna" json:"antenna"` // Antenna type Modulation string `yaml:"modulation" json:"modulation"` // Modulation (constant from protocol) Position *Position `yaml:"position" json:"position"` // Position Frequency float64 `yaml:"frequency" json:"frequency"` // Frequency (in MHz) RXFrequency float64 `yaml:"rx_frequency" json:"rx_frequency,omitempty"` // Used with split VFOs TXFrequency float64 `yaml:"tx_frequency" json:"tx_frequency,omitempty"` // Used with split VFOs HasRX bool `yaml:"has_rx" json:"has_rx"` // Is RX capable HasTX bool `yaml:"has_tx" json:"has_tx"` // Is TX capable Bandwidth float64 `yaml:"bandwidth" json:"bandwidth"` // Bandwidth (in kHz) Power float64 `yaml:"power" json:"power"` // Power (in dBm) Gain float64 `yaml:"gain" json:"gain"` // Gain (in dBm) LoRaSF uint8 `yaml:"lora_sf" json:"lora_sf,omitempty"` // LoRa spreading factor LoRaCR uint8 `yaml:"lora_cr" json:"lora_cr,omitempty"` // LoRa coding rate Extra map[string]any `yaml:"extra" json:"extra"` // Extra metadata } type Position struct { Latitude float64 `json:"latitude"` Longitude float64 `json:"longitude"` Altitude float64 `json:"altitude,omitempty"` } const earthRadiusKm = 6371.0088 // WGS84 mean Earth radius // RoundTo reduces the accuracy of the position by the provided radius. func (pos *Position) RoundTo(km float64) *Position { if km <= 0 { return pos } // Convert km to angular distance (radians) angular := km / earthRadiusKm // Convert to degrees degLatStep := angular * (180.0 / math.Pi) // Longitude step depends on latitude latRad := pos.Latitude * math.Pi / 180.0 cosLat := math.Cos(latRad) var degLonStep float64 if cosLat > 1e-12 { degLonStep = degLatStep / cosLat } else { // Near poles — longitude collapses degLonStep = 360.0 } roundedLat := math.Round(pos.Latitude/degLatStep) * degLatStep roundedLon := math.Round(pos.Longitude/degLonStep) * degLonStep // Normalize longitude to [-180, 180] roundedLon = math.Mod(roundedLon+180.0, 360.0) if roundedLon < 0 { roundedLon += 360.0 } roundedLon -= 180.0 return &Position{ Latitude: roundedLat, Longitude: roundedLon, Altitude: pos.Altitude, } } // Device is the minimum implementation for a radio device. type Device interface { Close() error Info() *Info } // DBmToW converts power in dBm to Watts. // // Formula: // // P(W) = 10^(dBm/10) / 1000 func DBmToW(dBm float64) float64 { return math.Pow(10, dBm/10.0) / 1000.0 } // WToDBm converts power in Watts to dBm. // // Behavior: // // watts > 0 → normal conversion // watts <= 0 → returns -Inf (represents zero power) // // Formula: // // dBm = 10 * log10(P(W) * 1000) func WToDBm(watts float64) float64 { if watts <= 0 { return math.Inf(-1) } return 10.0 * math.Log10(watts*1000.0) }