Files

113 lines
3.0 KiB
Go

//go:build windows
package cat
import (
"context"
"net"
"regexp"
"strconv"
"syscall"
"time"
"golang.org/x/sys/windows"
)
// FlexRadio is one radio found by discovery.
type FlexRadio struct {
IP string `json:"ip"`
Port int `json:"port"`
Model string `json:"model"`
Nickname string `json:"nickname"`
Serial string `json:"serial"`
Callsign string `json:"callsign"`
}
// FlexRadios on the LAN broadcast a discovery datagram to UDP :4992 about once a
// second. DiscoverFlex listens for that broadcast for the given duration and
// returns the unique radios seen. Best effort: if the port can't be bound
// (SmartSDR running, firewall…), it returns what it has (often nothing) and the
// user falls back to entering the IP by hand.
func DiscoverFlex(timeout time.Duration) ([]FlexRadio, error) {
if timeout <= 0 {
timeout = 2 * time.Second
}
// Bind :4992 with SO_REUSEADDR so we coexist with SmartSDR, which also
// listens for the same broadcast.
lc := net.ListenConfig{
Control: func(_, _ string, c syscall.RawConn) error {
var serr error
_ = c.Control(func(fd uintptr) {
serr = windows.SetsockoptInt(windows.Handle(fd), windows.SOL_SOCKET, windows.SO_REUSEADDR, 1)
})
return serr
},
}
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
pc, err := lc.ListenPacket(ctx, "udp4", ":4992")
if err != nil {
return nil, err
}
defer pc.Close()
_ = pc.SetReadDeadline(time.Now().Add(timeout))
found := map[string]FlexRadio{}
buf := make([]byte, 2048)
for {
n, _, err := pc.ReadFrom(buf)
if err != nil {
break // deadline reached or socket closed
}
if r, ok := parseFlexDiscovery(buf[:n]); ok && r.IP != "" {
if _, dup := found[r.IP]; !dup {
found[r.IP] = r
}
}
}
out := make([]FlexRadio, 0, len(found))
for _, r := range found {
out = append(out, r)
}
return out, nil
}
var (
reFlexModel = regexp.MustCompile(`model=(\S+)`)
reFlexIP = regexp.MustCompile(`ip=(\S+)`)
reFlexPort = regexp.MustCompile(`port=(\d+)`)
reFlexSerial = regexp.MustCompile(`serial=(\S+)`)
reFlexNickname = regexp.MustCompile(`nickname=(\S+)`)
reFlexCallsign = regexp.MustCompile(`callsign=(\S+)`)
)
// parseFlexDiscovery extracts radio fields from a VITA-49 discovery datagram.
// The payload carries a space-separated key=value ASCII blob after a binary
// header, so we scan the whole packet text for the keys we need.
func parseFlexDiscovery(pkt []byte) (FlexRadio, bool) {
s := string(pkt)
m := reFlexIP.FindStringSubmatch(s)
if m == nil {
return FlexRadio{}, false
}
r := FlexRadio{IP: m[1], Port: 4992}
if mm := reFlexPort.FindStringSubmatch(s); mm != nil {
if p, err := strconv.Atoi(mm[1]); err == nil && p > 0 {
r.Port = p
}
}
if mm := reFlexModel.FindStringSubmatch(s); mm != nil {
r.Model = mm[1]
}
if mm := reFlexSerial.FindStringSubmatch(s); mm != nil {
r.Serial = mm[1]
}
if mm := reFlexNickname.FindStringSubmatch(s); mm != nil {
r.Nickname = mm[1]
}
if mm := reFlexCallsign.FindStringSubmatch(s); mm != nil {
r.Callsign = mm[1]
}
return r, true
}