diff --git a/app.go b/app.go index cfc6b14..04e5aa1 100644 --- a/app.go +++ b/app.go @@ -1119,6 +1119,10 @@ func (a *App) startup(ctx context.Context) { wruntime.EventsEmit(a.ctx, "cat:state", s) } a.emitRadioUDP(s) + // Custom outbound rows bound to a band change (antenna switches). The + // comparison against the last band lives in the helper, so this callback + // firing on every user-relevant change costs one string compare. + a.udpTriggerBandChange(s) // Feed the frequency to any amplifier we are pretending to be a radio // for. Just two atomic stores per amp — the reply itself is built when // the amp polls, so a fast-tuning VFO costs nothing here. @@ -2780,6 +2784,7 @@ func (a *App) AddQSO(q qso.QSO) (id int64, err error) { rec := adif.SingleRecordADIF(qc) a.udp.EmitLoggedADIF(rec) a.udp.EmitLoggedQSOWSJT(wsjtLoggedQSO(qc), rec) + a.udpTriggerQSOLogged(qc) } // Nudge the grid again so the award_refs columns appear once materialised. if a.ctx != nil { @@ -7162,6 +7167,13 @@ func (a *App) lookupCallsign(callsign string, force bool) (lookup.Result, error) } } a.enrichFromULS(&r, callsign) + // Custom outbound rows bound to a lookup. After the enrichment, so a + // template sees the same grid and county the entry panel is about to show — + // two answers for one callsign is how a rotator ends up pointed elsewhere + // than the screen says. + if err == nil { + a.udpTriggerLookup(&r) + } return r, err } @@ -12208,6 +12220,7 @@ func (a *App) LogUDPLoggedADIF(adifText string) (int64, error) { rec := adif.SingleRecordADIF(qc) a.udp.EmitLoggedADIF(rec) a.udp.EmitLoggedQSOWSJT(wsjtLoggedQSO(qc), rec) + a.udpTriggerQSOLogged(qc) } if a.ctx != nil { wruntime.EventsEmit(a.ctx, "qso:logged", id) // refresh again so award_refs show @@ -14494,6 +14507,20 @@ func (a *App) GetRotatorHeading() RotatorHeading { // RotatorGoTo points the active rotor at the given azimuth (and optional // elevation if it is configured for it). func (a *App) RotatorGoTo(az int, el int) error { + return a.RotatorGoToPath(az, el, "") +} + +// RotatorGoToPath is RotatorGoTo with the path that produced the bearing — +// "SP", "LP", or "" when the caller has no opinion (the compass, a spot click). +// +// It exists for the custom outbound rows: an operator wanted the message to say +// WHICH bearing was taken, and the azimuth alone cannot tell you — 137° is short +// path to one station and long path to another. +func (a *App) RotatorGoToPath(az int, el int, path string) error { + // The trigger fires on the COMMAND, before any rotor is required: an + // operator may drive a switch or a display from the bearing without owning a + // rotator at all, and the early return below would swallow it. + a.udpTriggerRotator(az, el, strings.ToUpper(strings.TrimSpace(path))) lr, _, _, ok := a.activeRotor() if !ok { return fmt.Errorf("no rotator configured") diff --git a/changelog.json b/changelog.json index 351c477..3442dba 100644 --- a/changelog.json +++ b/changelog.json @@ -13,7 +13,8 @@ "Motorized antenna: when the controller settles somewhere other than where it was told, the log says so — that reads as “the antenna stopped responding” otherwise.", "UDP: an outbound message now logs that it was sent, and a logged QSO with no outbound ADIF row configured says so once.", "UDP: new outbound “WSJT-X logged QSO” — announces each contact on the WSJT-X UDP interface, both messages WSJT-X itself sends, for any logger that listens there rather than for plain-text ADIF (Logger32 among them).", - "Appearance: the Sahara theme is lighter, parchment rather than deep sand, with a terracotta accent." + "Appearance: the Sahara theme is lighter, parchment rather than deep sand, with a terracotta accent.", + "UDP: new outbound “Custom message” — you choose what fires it (band change, QSO logged, rotator command, callsign lookup) and what it says, with fields like {band} or {az}. It leaves as a UDP datagram or an HTTP request, which is how most antenna switches are driven." ], "fr": [ "Envoi de spot : le commentaire porte désormais les références de diplôme après le mode — celles que vous avez attribuées (POTA, SOTA, IOTA…), pas le DXCC, la zone et le préfixe que chacun déduit de l’indicatif. Un auto-spot porte VOS références d’activation.", @@ -26,7 +27,8 @@ "Antenne motorisée : quand le contrôleur se fixe ailleurs que là où on l’a envoyé, le journal le dit — sans quoi cela ressemble à une antenne qui ne répond plus.", "UDP : un message sortant inscrit désormais son envoi dans le journal, et un QSO enregistré sans ligne ADIF sortante le signale une fois.", "UDP : nouvelle sortie « QSO enregistré WSJT-X » — annonce chaque contact sur l’interface UDP WSJT-X, les deux messages que WSJT-X émet lui-même, pour tout logger qui écoute là plutôt que l’ADIF en texte brut (Logger32 entre autres).", - "Apparence : le thème Sahara s’éclaircit, parchemin plutôt que sable profond, avec un accent terre cuite." + "Apparence : le thème Sahara s’éclaircit, parchemin plutôt que sable profond, avec un accent terre cuite.", + "UDP : nouvelle sortie « Message personnalisé » — vous choisissez ce qui la déclenche (changement de bande, QSO enregistré, commande de rotor, recherche d’indicatif) et ce qu’elle dit, avec des champs comme {band} ou {az}. Elle part en datagramme UDP ou en requête HTTP, ce qui est la façon dont se pilotent la plupart des commutateurs d’antennes." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 3e1bedf..83fa68f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -17,6 +17,7 @@ import { SMTPConfigured, SendLogToDeveloper, WorkedBefore, SetCompactMode, SetCompactHeight, + RotatorGoToPath, GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, FlexApplyBandAntenna, FlexApplyBandPower, GetSecretStatus, UnlockSecrets, RefreshCtyDat, DownloadAllReferenceLists, @@ -5370,13 +5371,15 @@ export default function App() { {(() => { const p = dxPath; const disabled = !p; - const goto = (az: number) => RotatorGoTo(Math.round(az), -1).catch((err) => setError(String(err?.message ?? err))); + // The path is passed through so a custom outbound row can say which + // bearing was taken — 137° is short path to one station and long to another. + const goto = (az: number, path: 'SP' | 'LP') => RotatorGoToPath(Math.round(az), -1, path).catch((err) => setError(String(err?.message ?? err))); return (
)} - {draft.direction === 'outbound' && ( + {draft.direction === 'outbound' && draft.service_type !== 'custom' && (
)} + {draft.service_type === 'custom' && ( + + )} +