113 lines
3.0 KiB
Go
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
|
|
}
|