feat: remote_call UDP accepts <FREQ>/<MODE> from DXHunter and tunes the active CAT backend
This commit is contained in:
@@ -10844,9 +10844,24 @@ func (a *App) consumeUDPEvents() {
|
|||||||
"service": string(ev.Service),
|
"service": string(ev.Service),
|
||||||
"source": ev.Source,
|
"source": ev.Source,
|
||||||
})
|
})
|
||||||
case ev.DXCall != "" && ev.Service == udp.ServiceRemoteCall:
|
case (ev.DXCall != "" || ev.TuneFreqHz > 0) && ev.Service == udp.ServiceRemoteCall:
|
||||||
applog.Printf("udp: emit udp:remote_call %q\n", ev.DXCall)
|
// CAT tune riding along with the callsign (DXHunter spot click:
|
||||||
wruntime.EventsEmit(a.ctx, "udp:remote_call", ev.DXCall)
|
// "<CALLSIGN>VP5G<FREQ>10.136<MODE>FT8"). Freq first so the rig is
|
||||||
|
// already moving while the frontend fills the entry field.
|
||||||
|
if ev.TuneFreqHz > 0 {
|
||||||
|
applog.Printf("udp: remote_call tune request %.3f MHz mode=%q\n", float64(ev.TuneFreqHz)/1e6, ev.TuneMode)
|
||||||
|
if err := a.SetCATFrequency(ev.TuneFreqHz); err != nil {
|
||||||
|
applog.Printf("udp: remote_call tune failed: %v\n", err)
|
||||||
|
} else if ev.TuneMode != "" {
|
||||||
|
if err := a.SetCATMode(ev.TuneMode); err != nil {
|
||||||
|
applog.Printf("udp: remote_call mode set failed: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ev.DXCall != "" {
|
||||||
|
applog.Printf("udp: emit udp:remote_call %q\n", ev.DXCall)
|
||||||
|
wruntime.EventsEmit(a.ctx, "udp:remote_call", ev.DXCall)
|
||||||
|
}
|
||||||
case ev.DXCall != "":
|
case ev.DXCall != "":
|
||||||
applog.Printf("udp: emit udp:dx_call %q (mode=%s freq=%d)\n", ev.DXCall, ev.Mode, ev.FreqHz)
|
applog.Printf("udp: emit udp:dx_call %q (mode=%s freq=%d)\n", ev.DXCall, ev.Mode, ev.FreqHz)
|
||||||
wruntime.EventsEmit(a.ctx, "udp:dx_call", map[string]any{
|
wruntime.EventsEmit(a.ctx, "udp:dx_call", map[string]any{
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
@@ -14,6 +16,14 @@ import (
|
|||||||
"hamlog/internal/applog"
|
"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
|
// reusingListenConfig builds a net.ListenConfig that sets SO_REUSEADDR
|
||||||
// (and SO_REUSEPORT on Unix) on the underlying socket before bind. This
|
// (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
|
// 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
|
// Call after previously reporting one — i.e. the operator cleared the call in
|
||||||
// the digital app. OpsLog clears its entry to match.
|
// the digital app. OpsLog clears its entry to match.
|
||||||
ClearCall bool
|
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.
|
// Server is a single inbound UDP listener.
|
||||||
@@ -248,9 +265,25 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
|||||||
// "CALL F4XYZ" (text prefix)
|
// "CALL F4XYZ" (text prefix)
|
||||||
// "<CALLSIGN>F4XYZ<CALLSIGN>" (DXHunter-style tags)
|
// "<CALLSIGN>F4XYZ<CALLSIGN>" (DXHunter-style tags)
|
||||||
// "<CALLSIGN>F4XYZ</CALLSIGN>" (proper XML)
|
// "<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
|
// Strip every angle-bracket tag, normalise whitespace, take the
|
||||||
// last non-empty token. Upper-case for downstream consistency.
|
// last non-empty token. Upper-case for downstream consistency.
|
||||||
text := string(pkt)
|
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
|
// Drop every <...> tag (open or close) — works for both
|
||||||
// <CALLSIGN>...<CALLSIGN> and <CALLSIGN>...</CALLSIGN>.
|
// <CALLSIGN>...<CALLSIGN> and <CALLSIGN>...</CALLSIGN>.
|
||||||
for {
|
for {
|
||||||
@@ -286,8 +319,9 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Empty events are useless; skip — EXCEPT a clear signal, which is meant to be
|
// Empty events are useless; skip — EXCEPT a clear signal, which is meant to be
|
||||||
// empty (the DX Call was cleared in the digital app).
|
// empty (the DX Call was cleared in the digital app), and a tune-only
|
||||||
if ev.DXCall == "" && ev.LoggedADIF == "" && ev.DecodeCall == "" && !ev.ClearCall {
|
// request (freq with no callsign).
|
||||||
|
if ev.DXCall == "" && ev.LoggedADIF == "" && ev.DecodeCall == "" && !ev.ClearCall && ev.TuneFreqHz == 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
select {
|
select {
|
||||||
|
|||||||
Reference in New Issue
Block a user