Refactored protocol and radio interfaces
This commit is contained in:
@@ -9,9 +9,10 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"git.maze.io/go/ham/protocol"
|
"git.maze.io/go/ham/protocol"
|
||||||
"git.maze.io/go/ham/protocol/meshcore/crypto"
|
"git.maze.io/go/ham/radio"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -24,21 +25,6 @@ type Node struct {
|
|||||||
driver nodeDriver
|
driver nodeDriver
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeInfo struct {
|
|
||||||
Manufacturer string `json:"manufacturer"`
|
|
||||||
FirmwareVersion string `json:"firmware_version"`
|
|
||||||
Type NodeType `json:"node_type"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Power uint8 `json:"power"`
|
|
||||||
MaxPower uint8 `json:"max_power"`
|
|
||||||
PublicKey *crypto.PublicKey `json:"public_key"`
|
|
||||||
Position *Position `json:"position"`
|
|
||||||
Frequency float64 `json:"frequency"` // in MHz
|
|
||||||
Bandwidth float64 `json:"bandwidth"` // in kHz
|
|
||||||
SpreadingFactor uint8 `json:"sf"`
|
|
||||||
CodingRate uint8 `json:"cr"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewCompanion connects to a companion device type (over serial, TCP or BLE).
|
// NewCompanion connects to a companion device type (over serial, TCP or BLE).
|
||||||
func NewCompanion(conn io.ReadWriteCloser) (*Node, error) {
|
func NewCompanion(conn io.ReadWriteCloser) (*Node, error) {
|
||||||
driver := newCompanionDriver(conn)
|
driver := newCompanionDriver(conn)
|
||||||
@@ -64,19 +50,17 @@ func (dev *Node) RawPackets() <-chan *protocol.Packet {
|
|||||||
return dev.driver.RawPackets()
|
return dev.driver.RawPackets()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dev *Node) Info() *NodeInfo {
|
func (dev *Node) Info() *radio.Info {
|
||||||
return dev.driver.Info()
|
return dev.driver.Info()
|
||||||
}
|
}
|
||||||
|
|
||||||
type nodeDriver interface {
|
type nodeDriver interface {
|
||||||
|
radio.Device
|
||||||
|
protocol.PacketReceiver
|
||||||
|
|
||||||
Setup() error
|
Setup() error
|
||||||
|
|
||||||
Close() error
|
|
||||||
|
|
||||||
Packets() <-chan *Packet
|
Packets() <-chan *Packet
|
||||||
RawPackets() <-chan *protocol.Packet
|
|
||||||
|
|
||||||
Info() *NodeInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type CompanionError struct {
|
type CompanionError struct {
|
||||||
@@ -107,7 +91,35 @@ type companionDriver struct {
|
|||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
packets chan *Packet
|
packets chan *Packet
|
||||||
rawPackets chan *protocol.Packet
|
rawPackets chan *protocol.Packet
|
||||||
info NodeInfo
|
info companionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
type companionInfo struct {
|
||||||
|
// Fields returns by CMD_APP_START.
|
||||||
|
Type NodeType
|
||||||
|
Power byte // in dBm
|
||||||
|
MaxPower byte // in dBm
|
||||||
|
PublicKey [32]byte
|
||||||
|
Latitude float64
|
||||||
|
Longitude float64
|
||||||
|
HasMultiACKs bool
|
||||||
|
AdvertLocationPolicy byte
|
||||||
|
TelemetryFlags byte
|
||||||
|
ManualAddContacts byte
|
||||||
|
Frequency float64 // in MHz
|
||||||
|
Bandwidth float64 // in kHz
|
||||||
|
SpreadingFactor byte
|
||||||
|
CodingRate byte
|
||||||
|
Name string
|
||||||
|
|
||||||
|
// Fields returns by CMD_DEVICE_QUERY.
|
||||||
|
FirmwareVersion string
|
||||||
|
FirmwareVersionCode byte
|
||||||
|
FirmwareBuildDate string
|
||||||
|
Manufacturer string
|
||||||
|
MaxContacts int
|
||||||
|
MaxGroupChannels int
|
||||||
|
BLEPIN [4]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver {
|
func newCompanionDriver(conn io.ReadWriteCloser) *companionDriver {
|
||||||
@@ -145,8 +157,25 @@ func (drv *companionDriver) RawPackets() <-chan *protocol.Packet {
|
|||||||
return drv.rawPackets
|
return drv.rawPackets
|
||||||
}
|
}
|
||||||
|
|
||||||
func (drv *companionDriver) Info() *NodeInfo {
|
func (drv *companionDriver) Info() *radio.Info {
|
||||||
return &drv.info
|
var pos *radio.Position
|
||||||
|
if drv.info.Latitude != 0 && drv.info.Longitude != 0 {
|
||||||
|
pos = &radio.Position{
|
||||||
|
Latitude: drv.info.Latitude,
|
||||||
|
Longitude: drv.info.Longitude,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &radio.Info{
|
||||||
|
Name: drv.info.Name,
|
||||||
|
Manufacturer: drv.info.Manufacturer,
|
||||||
|
Modulation: protocol.LoRa,
|
||||||
|
Position: pos,
|
||||||
|
Frequency: drv.info.Frequency,
|
||||||
|
Bandwidth: drv.info.Bandwidth,
|
||||||
|
Power: float64(drv.info.Power),
|
||||||
|
LoRaSF: drv.info.SpreadingFactor,
|
||||||
|
LoRaCR: drv.info.CodingRate,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (drv *companionDriver) readFrame() ([]byte, error) {
|
func (drv *companionDriver) readFrame() ([]byte, error) {
|
||||||
@@ -252,6 +281,7 @@ func (drv *companionDriver) handleRXData(b []byte) {
|
|||||||
return // not listening for packets, discard
|
return // not listening for packets, discard
|
||||||
}
|
}
|
||||||
|
|
||||||
|
now := time.Now().UTC()
|
||||||
packet := new(Packet)
|
packet := new(Packet)
|
||||||
if err := packet.UnmarshalBytes(b[2:]); err == nil {
|
if err := packet.UnmarshalBytes(b[2:]); err == nil {
|
||||||
packet.SNR = float64(b[0]) / 4
|
packet.SNR = float64(b[0]) / 4
|
||||||
@@ -264,6 +294,7 @@ func (drv *companionDriver) handleRXData(b []byte) {
|
|||||||
if drv.rawPackets != nil {
|
if drv.rawPackets != nil {
|
||||||
select {
|
select {
|
||||||
case drv.rawPackets <- &protocol.Packet{
|
case drv.rawPackets <- &protocol.Packet{
|
||||||
|
Time: now,
|
||||||
Protocol: "meshcore",
|
Protocol: "meshcore",
|
||||||
SNR: packet.SNR,
|
SNR: packet.SNR,
|
||||||
RSSI: packet.RSSI,
|
RSSI: packet.RSSI,
|
||||||
@@ -295,18 +326,18 @@ func (drv *companionDriver) sendAppStart() (err error) {
|
|||||||
drv.info.Type = NodeType(b[0])
|
drv.info.Type = NodeType(b[0])
|
||||||
drv.info.Power = b[1]
|
drv.info.Power = b[1]
|
||||||
drv.info.MaxPower = b[2]
|
drv.info.MaxPower = b[2]
|
||||||
drv.info.PublicKey, _ = crypto.NewPublicKey(b[3 : 3+crypto.PublicKeySize])
|
copy(drv.info.PublicKey[:], b[3:])
|
||||||
drv.info.Position = new(Position)
|
drv.info.Latitude, drv.info.Longitude = decodeLatLon(b[35:])
|
||||||
drv.info.Position.Unmarshal(b[35:])
|
drv.info.HasMultiACKs = b[43] != 0
|
||||||
//drv.info.HasMultiACKs = b[43] != 0
|
drv.info.AdvertLocationPolicy = b[44]
|
||||||
//drv.info.AdvertLocationPolicy = b[44]
|
drv.info.TelemetryFlags = b[45]
|
||||||
//drv.info.TelemetryFlags = b[45]
|
drv.info.ManualAddContacts = b[46]
|
||||||
//drv.info.ManualAddContacts = b[46]
|
|
||||||
drv.info.Frequency = decodeFrequency(b[47:])
|
drv.info.Frequency = decodeFrequency(b[47:])
|
||||||
drv.info.Bandwidth = decodeFrequency(b[51:])
|
drv.info.Bandwidth = decodeFrequency(b[51:])
|
||||||
drv.info.SpreadingFactor = b[55]
|
drv.info.SpreadingFactor = b[55]
|
||||||
drv.info.CodingRate = b[56]
|
drv.info.CodingRate = b[56]
|
||||||
drv.info.Name = strings.TrimRight(string(b[57:]), "\x00")
|
drv.info.Name = strings.TrimRight(string(b[57:]), "\x00")
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,6 +356,10 @@ func (drv *companionDriver) sendDeviceInfo() (err error) {
|
|||||||
}
|
}
|
||||||
b = b[1:]
|
b = b[1:]
|
||||||
|
|
||||||
|
drv.info.FirmwareVersionCode = b[0]
|
||||||
|
drv.info.MaxContacts = int(b[1]) * 2
|
||||||
|
drv.info.MaxGroupChannels = int(b[2])
|
||||||
|
drv.info.FirmwareBuildDate = decodeCString(b[7:19])
|
||||||
drv.info.Manufacturer = decodeCString(b[19:59])
|
drv.info.Manufacturer = decodeCString(b[19:59])
|
||||||
drv.info.FirmwareVersion = decodeCString(b[59:79])
|
drv.info.FirmwareVersion = decodeCString(b[59:79])
|
||||||
|
|
||||||
@@ -339,3 +374,8 @@ func (drv *companionDriver) poll() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ protocol.PacketReceiver = (*Node)(nil)
|
||||||
|
_ nodeDriver = (*companionDriver)(nil)
|
||||||
|
)
|
||||||
|
|||||||
@@ -55,6 +55,12 @@ func decodeFrequency(b []byte) float64 {
|
|||||||
return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3
|
return float64(int64(binary.LittleEndian.Uint32(b))) / 1e3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func decodeLatLon(b []byte) (lat float64, lng float64) {
|
||||||
|
lat = float64(int64(binary.LittleEndian.Uint32(b[0:]))) / 1e6
|
||||||
|
lng = float64(int64(binary.LittleEndian.Uint32(b[4:]))) / 1e6
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
func decodeCString(b []byte) string {
|
func decodeCString(b []byte) string {
|
||||||
if i := bytes.IndexByte(b, 0x00); i > -1 {
|
if i := bytes.IndexByte(b, 0x00); i > -1 {
|
||||||
return string(b[:i])
|
return string(b[:i])
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
package protocol
|
|
||||||
|
|
||||||
// Packet represents a raw packet.
|
|
||||||
type Packet struct {
|
|
||||||
Protocol string `json:"protocol"` // Protocol name
|
|
||||||
SNR float64 `json:"snr"` // Signal-to-noise Ratio
|
|
||||||
RSSI int8 `json:"rssi"` // Received Signal Strength Indicator
|
|
||||||
Raw []byte `json:"raw"` // Raw packet
|
|
||||||
}
|
|
||||||
|
|
||||||
type Device interface {
|
|
||||||
// Close the device.
|
|
||||||
Close() error
|
|
||||||
}
|
|
||||||
|
|
||||||
// Receiver of packets.
|
|
||||||
type Receiver interface {
|
|
||||||
Device
|
|
||||||
|
|
||||||
// RawPackets starts receiving raw packets.
|
|
||||||
RawPackets() <-chan *Packet
|
|
||||||
}
|
|
||||||
|
|
||||||
// Transmitter of packets.
|
|
||||||
type Transmitter interface {
|
|
||||||
Device
|
|
||||||
|
|
||||||
// SendRawPacket sends a raw (encoded) packet.
|
|
||||||
SendRawPacket(*Packet) error
|
|
||||||
}
|
|
||||||
42
protocol/protocol.go
Normal file
42
protocol/protocol.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.maze.io/go/ham/radio"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Protocol standard names (as used in this package).
|
||||||
|
const (
|
||||||
|
ADSB = "adsb"
|
||||||
|
APRS = "aprs"
|
||||||
|
APRS_IS = "aprsis"
|
||||||
|
AX25 = "ax25"
|
||||||
|
LoRa = "lora"
|
||||||
|
MeshCore = "meshcore"
|
||||||
|
Meshtastic = "meshtastic"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Packet represents a raw packet.
|
||||||
|
type Packet struct {
|
||||||
|
Time time.Time `json:"time,omitempty"` // Receive time stamp
|
||||||
|
Protocol string `json:"protocol"` // Protocol name
|
||||||
|
SNR float64 `json:"snr"` // Signal-to-noise Ratio
|
||||||
|
RSSI int8 `json:"rssi"` // Received Signal Strength Indicator
|
||||||
|
Raw []byte `json:"raw"` // Raw packet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receiver of packets.
|
||||||
|
type PacketReceiver interface {
|
||||||
|
radio.Device
|
||||||
|
|
||||||
|
// RawPackets starts receiving raw packets.
|
||||||
|
RawPackets() <-chan *Packet
|
||||||
|
}
|
||||||
|
|
||||||
|
type PacketTransmitter interface {
|
||||||
|
radio.Device
|
||||||
|
|
||||||
|
// SendRawPacket sends a raw (encoded) packet.
|
||||||
|
SendRawPacket(*Packet) error
|
||||||
|
}
|
||||||
103
radio/radio.go
Normal file
103
radio/radio.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package radio
|
||||||
|
|
||||||
|
import "math"
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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
|
||||||
|
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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user