From 28b6f04ea43dc437dce3e6367dd29b930baab9be Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 16 Jul 2026 11:52:42 +0200 Subject: [PATCH] feat: When a call is cleared from MSHV/WSJTx/JTDX it also clears the call field of OpsLog --- app.go | 6 ++++++ frontend/src/App.tsx | 8 +++++++- internal/integrations/udp/server.go | 24 +++++++++++++++++++++--- 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/app.go b/app.go index 06944f9..1dab336 100644 --- a/app.go +++ b/app.go @@ -8887,6 +8887,12 @@ func (a *App) consumeUDPEvents() { "source": ev.Source, "adif": ev.LoggedADIF, }) + case ev.ClearCall: + applog.Printf("udp: emit udp:clear_call (DX Call cleared in the digital app)\n") + wruntime.EventsEmit(a.ctx, "udp:clear_call", map[string]any{ + "service": string(ev.Service), + "source": ev.Source, + }) case ev.DXCall != "" && ev.Service == udp.ServiceRemoteCall: applog.Printf("udp: emit udp:remote_call %q\n", ev.DXCall) wruntime.EventsEmit(a.ctx, "udp:remote_call", ev.DXCall) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 56c4aea..cb26d3d 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1787,6 +1787,12 @@ export default function App() { const unsubRC = EventsOn('udp:remote_call', (raw: string) => { if (applyUdpCall(raw, true)) restartRecordingForNewTarget(String(raw ?? '')); // explicit remote pick }); + // The DX Call was cleared in WSJT-X / JTDX / MSHV → clear our entry to match. + // Only when something is actually in the entry, so an idle digital app doesn't + // wipe a call being typed by hand. + const unsubClear = EventsOn('udp:clear_call', () => { + if (callsignRef.current?.value?.trim() || callsign.trim()) resetEntry(); + }); // Clicked one of OpsLog's spots on the FlexRadio panadapter → fill the call // (the radio already tuned via trigger_action=Tune, and CAT reads the freq). // An explicit click always wins over whatever call is currently in the field. @@ -1811,7 +1817,7 @@ export default function App() { else setError('UDP auto-log: ' + msg); } }); - return () => { unsubDX?.(); unsubRC?.(); unsubFlexSpot?.(); unsubProg?.(); unsubLog?.(); }; + return () => { unsubDX?.(); unsubRC?.(); unsubClear?.(); unsubFlexSpot?.(); unsubProg?.(); unsubLog?.(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/internal/integrations/udp/server.go b/internal/integrations/udp/server.go index 5fbfe91..8b737b8 100644 --- a/internal/integrations/udp/server.go +++ b/internal/integrations/udp/server.go @@ -52,6 +52,11 @@ type Event struct { DecodeFreqHz int64 // RF frequency (dial + audio offset) DecodeSNR int // reported SNR (dB) DecodeCQ bool // the decode was a CQ + + // ClearCall is set when a WSJT/JTDX/MSHV Status message reports an EMPTY DX + // Call after previously reporting one — i.e. the operator cleared the call in + // the digital app. OpsLog clears its entry to match. + ClearCall bool } // Server is a single inbound UDP listener. @@ -64,7 +69,8 @@ type Server struct { stopped bool mu sync.Mutex - lastDialHz int64 // WSJT: dial freq from the last Status, added to Decode offsets + lastDialHz int64 // WSJT: dial freq from the last Status, added to Decode offsets + lastDX string // WSJT: last non-empty DX Call seen, to detect a clear } func newServer(cfg Config, out chan<- Event) *Server { @@ -213,6 +219,17 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) { ev.Mode = w.Mode ev.FreqHz = w.FreqHz ev.LoggedADIF = w.LoggedADIF + // A Status with an empty DX Call, right after one that had a call, means the + // operator cleared it in WSJT-X / JTDX / MSHV. Fire ONE clear (tracked per + // server) — an idle app sends empty Status every second, and we must not + // re-clear (which would fight a manual entry) on each of those. + s.mu.Lock() + prev := s.lastDX + s.lastDX = w.DXCall + s.mu.Unlock() + if w.DXCall == "" && prev != "" { + ev.ClearCall = true + } case ServiceADIF: // JTAlert / GridTracker forward a text ADIF record after a QSO is // logged. Guard against keep-alive / non-ADIF chatter on the socket: @@ -268,8 +285,9 @@ func (s *Server) handle(pkt []byte, remote *net.UDPAddr) { default: return } - // Empty events are useless; skip. - if ev.DXCall == "" && ev.LoggedADIF == "" && ev.DecodeCall == "" { + // 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 { return } select {