feat: remote_call UDP accepts <FREQ>/<MODE> from DXHunter and tunes the active CAT backend
This commit is contained in:
@@ -4,6 +4,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
@@ -14,6 +16,14 @@ import (
|
||||
"hamlog/internal/applog"
|
||||
)
|
||||
|
||||
// remoteFreqRe / remoteModeRe pull the optional tune request out of a
|
||||
// ServiceRemoteCall packet: "<FREQ>10.136" (MHz) and "<MODE>FT8". Both accept
|
||||
// an optional closing tag for proper-XML senders.
|
||||
var (
|
||||
remoteFreqRe = regexp.MustCompile(`(?i)<FREQ>\s*([0-9]+(?:\.[0-9]+)?)`)
|
||||
remoteModeRe = regexp.MustCompile(`(?i)<MODE>\s*([A-Z0-9-]+)`)
|
||||
)
|
||||
|
||||
// reusingListenConfig builds a net.ListenConfig that sets SO_REUSEADDR
|
||||
// (and SO_REUSEPORT on Unix) on the underlying socket before bind. This
|
||||
// is the only way for two processes to share a UDP port on Windows — Go
|
||||
@@ -57,6 +67,13 @@ type Event struct {
|
||||
// Call after previously reporting one — i.e. the operator cleared the call in
|
||||
// the digital app. OpsLog clears its entry to match.
|
||||
ClearCall bool
|
||||
|
||||
// TuneFreqHz / TuneMode carry an explicit "tune the radio" request embedded
|
||||
// in a ServiceRemoteCall packet (DXHunter spot click sends
|
||||
// "<CALLSIGN>VP5G<FREQ>10.136<MODE>FT8"). Zero/empty = no tune requested —
|
||||
// the packet only fills the entry callsign, exactly as before.
|
||||
TuneFreqHz int64
|
||||
TuneMode string
|
||||
}
|
||||
|
||||
// Server is a single inbound UDP listener.
|
||||
@@ -248,9 +265,25 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
||||
// "CALL F4XYZ" (text prefix)
|
||||
// "<CALLSIGN>F4XYZ<CALLSIGN>" (DXHunter-style tags)
|
||||
// "<CALLSIGN>F4XYZ</CALLSIGN>" (proper XML)
|
||||
// "<CALLSIGN>VP5G<FREQ>10.136<MODE>FT8" (DXHunter with CAT tune)
|
||||
// Strip every angle-bracket tag, normalise whitespace, take the
|
||||
// last non-empty token. Upper-case for downstream consistency.
|
||||
text := string(pkt)
|
||||
// Optional tune request: <FREQ>MHz and <MODE>str ride along with the
|
||||
// callsign so a DXHunter spot click can drive OpsLog's CAT. Extract
|
||||
// (and cut) them BEFORE the generic tag-stripping below, which would
|
||||
// otherwise leave their values as stray tokens and corrupt the
|
||||
// "last token = callsign" heuristic.
|
||||
if m := remoteFreqRe.FindStringSubmatch(text); m != nil {
|
||||
if mhz, err := strconv.ParseFloat(m[1], 64); err == nil && mhz > 0 {
|
||||
ev.TuneFreqHz = int64(mhz * 1e6)
|
||||
}
|
||||
text = strings.Replace(text, m[0], " ", 1)
|
||||
}
|
||||
if m := remoteModeRe.FindStringSubmatch(text); m != nil {
|
||||
ev.TuneMode = strings.ToUpper(m[1])
|
||||
text = strings.Replace(text, m[0], " ", 1)
|
||||
}
|
||||
// Drop every <...> tag (open or close) — works for both
|
||||
// <CALLSIGN>...<CALLSIGN> and <CALLSIGN>...</CALLSIGN>.
|
||||
for {
|
||||
@@ -286,8 +319,9 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
||||
return
|
||||
}
|
||||
// Empty events are useless; skip — EXCEPT a clear signal, which is meant to be
|
||||
// empty (the DX Call was cleared in the digital app).
|
||||
if ev.DXCall == "" && ev.LoggedADIF == "" && ev.DecodeCall == "" && !ev.ClearCall {
|
||||
// empty (the DX Call was cleared in the digital app), and a tune-only
|
||||
// request (freq with no callsign).
|
||||
if ev.DXCall == "" && ev.LoggedADIF == "" && ev.DecodeCall == "" && !ev.ClearCall && ev.TuneFreqHz == 0 {
|
||||
return
|
||||
}
|
||||
select {
|
||||
|
||||
Reference in New Issue
Block a user