//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 }