diff --git a/app.go b/app.go index 7dff0c3..dec19c8 100644 --- a/app.go +++ b/app.go @@ -8771,7 +8771,11 @@ func (a *App) NetDeactivate(id int64) (int64, error) { const ( defaultEmailSubject = "Our QSO recording — {CALL}" - defaultEmailBody = "Hi,\n\nGreat to work you! Please find attached the audio recording of our QSO.\n\n{DATE} · {BAND} · {MODE}\n\n73,\n{MYCALL}" + // Same credit line as the QSL e-mail, and for the same reason it lives in the + // template rather than being appended at send time: it is a default, so an + // operator deletes it once, and one who already saved their own body never + // sees it — a stored template is returned verbatim. + defaultEmailBody = "Hi,\n\nGreat to work you! Please find attached the audio recording of our QSO.\n\n{DATE} · {BAND} · {MODE}\n\n73,\n{MYCALL}\n\n" + qslCredit ) // EmailSettings is the user's SMTP config + auto-send + message templates. diff --git a/bandchange_test.go b/bandchange_test.go new file mode 100644 index 0000000..69ad769 --- /dev/null +++ b/bandchange_test.go @@ -0,0 +1,36 @@ +package main + +import "testing" + +// A band change now has two sources: the rig, and the entry strip on a station +// whose rig OpsLog does not control. They share one memory of the last band, so +// a station that has both commands its antenna switch once per QSY rather than +// twice — the entry selector pushes a QSY, the rig reports the same band back, +// and only the first of the two is a change. +func TestBandChangeIsNotedOncePerBand(t *testing.T) { + lastTriggerBandMu.Lock() + lastTriggerBand = "" + lastTriggerBandMu.Unlock() + + if b, changed := noteBandChange("20m"); !changed || b != "20m" { + t.Fatalf("first 20m = (%q,%v), want (\"20m\",true)", b, changed) + } + // The same band from the other source — the rig echoing the QSY back. + if _, changed := noteBandChange("20M"); changed { + t.Error("the rig echoing the band back counted as a second change — the switch would be commanded twice") + } + if _, changed := noteBandChange(" 20m "); changed { + t.Error("whitespace made the same band look new") + } + if b, changed := noteBandChange("40m"); !changed || b != "40m" { + t.Errorf("40m = (%q,%v), want (\"40m\",true)", b, changed) + } + // An unknown band must not be recorded, or the next real one would look + // unchanged against it. + if _, changed := noteBandChange(""); changed { + t.Error("an empty band was treated as a change") + } + if _, changed := noteBandChange("40m"); changed { + t.Error("the empty band overwrote the last one") + } +} diff --git a/changelog.json b/changelog.json index 203c397..d91de9c 100644 --- a/changelog.json +++ b/changelog.json @@ -5,12 +5,14 @@ "en": [ "Right-click: update the US county of the selected contacts from the ULS database, replacing a county since renamed or abolished.", "A QSO logged from WSJT-X, MSHV or a net now appears in Recent QSOs at once, instead of waiting for a delayed auto-upload to send it.", - "New installs: the default QSL e-mail ends with a credit line and a link to OpsLog. It is part of the template, so delete it if you'd rather not." + "New installs: the default QSL and recording e-mails end with a credit line and a link to OpsLog. Part of the template, so delete it if unwanted.", + "Relay automatic control and band-change messages now follow the Band selector too, so a station without CAT switches its antenna when you change band." ], "fr": [ "Clic droit : mettre à jour le comté US des contacts sélectionnés depuis la base ULS, pour remplacer un comté renommé ou supprimé.", "Un QSO logué depuis WSJT-X, MSHV ou un net apparaît aussitôt dans les QSO récents, sans attendre l’envoi d’un upload automatique différé.", - "Nouvelles installations : le mail QSL par défaut se termine par une ligne de crédit et un lien vers OpsLog. Elle fait partie du modèle, supprimable." + "Nouvelles installations : les mails QSL et enregistrement par défaut finissent par une ligne de crédit et un lien vers OpsLog. Dans le modèle, supprimable.", + "Le contrôle automatique des relais et les messages de changement de bande suivent aussi le champ Band : une station sans CAT commute enfin son antenne." ] }, { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index a6d3e38..8701c7f 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -18,7 +18,7 @@ import { WorkedBefore, SetCompactMode, SetCompactHeight, RotatorGoToPath, - GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, FlexApplyBandAntenna, FlexApplyBandPower, + GetCATState, SetCATFrequency, SetCATMode, SwitchCATRig, EntryBandChanged, FlexApplyBandAntenna, FlexApplyBandPower, GetSecretStatus, UnlockSecrets, RefreshCtyDat, DownloadAllReferenceLists, RotatorGoTo, RotatorStop, GetRotatorHeading, SetActiveRotor, @@ -730,10 +730,19 @@ export default function App() { function onBandUserChange(v: string) { setBand(v); noteManualEdit(); - if (catState.enabled && catState.connected && !locks.band && !locks.freq) { + // A lock means this value is deliberately decoupled from the rig (logging an + // old contact off-frequency) — drive nothing, neither the radio nor an antenna. + if (locks.band || locks.freq) return; + if (catState.enabled && catState.connected) { const hz = qsyFreqHz(v, mode); if (hz > 0) SetCATFrequency(hz).catch(() => {}); + return; // the rig reports back, and the relays follow from its state } + // No rig to push to, so THIS is the band change. Without it, relay automatic + // control and the band-change outbound rows never heard about it — they only + // ever listened to the CAT state, which on a station without rig control + // never says anything. + EntryBandChanged(v).catch(() => {}); } function onModeUserChange(v: string) { setMode(v); diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index ecc7e9c..e9357a2 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -196,6 +196,8 @@ export function DownloadULSCounties():Promise; export function DuplicateProfile(arg1:number,arg2:string):Promise; +export function EntryBandChanged(arg1:string):Promise; + export function ExplainAward(arg1:string,arg2:string):Promise>; export function ExportADIF(arg1:string,arg2:boolean,arg3:Array):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 35e2384..b15a928 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -334,6 +334,10 @@ export function DuplicateProfile(arg1, arg2) { return window['go']['main']['App']['DuplicateProfile'](arg1, arg2); } +export function EntryBandChanged(arg1) { + return window['go']['main']['App']['EntryBandChanged'](arg1); +} + export function ExplainAward(arg1, arg2) { return window['go']['main']['App']['ExplainAward'](arg1, arg2); } diff --git a/relayauto.go b/relayauto.go index 314a643..e0a15f2 100644 --- a/relayauto.go +++ b/relayauto.go @@ -203,3 +203,34 @@ func (a *App) applyRelayAuto(freqHz int64, band string) { wruntime.EventsEmit(a.ctx, "station:relay_auto", nil) // nudge the Station Control UI to re-poll } } + +// EntryBandChanged drives the band-following features from the QSO ENTRY band +// selector, for a station whose rig OpsLog does not control. +// +// Both of them — relay automatic control and the band-change outbound rows — +// hang off the rig state, and that is right when there IS a rig: changing Band +// in the entry strip pushes a QSY to it, the new state comes back through the +// CAT callback, and the relays follow from there. Without a CAT connection +// nothing is pushed and nothing comes back, so an operator with an antenna +// switch and no rig control changed band in OpsLog and watched the switch sit +// exactly where it was. Reported as automatic control not working; it was never +// told the band had changed. +// +// The frontend calls this ONLY when the CAT push did not happen, and never when +// the band or frequency lock is on — a lock means the entry is deliberately +// decoupled from the rig (logging an old contact off-frequency), and moving an +// antenna to match a QSO from last year is worse than doing nothing. +// +// The frequency is passed as unknown, deliberately: a band selector gives a +// band and nothing else, and rules written on a frequency RANGE must be left +// alone rather than evaluated against a made-up dial reading. +func (a *App) EntryBandChanged(band string) { + band = strings.TrimSpace(band) + if band == "" { + return + } + if a.relayAutoOn.Load() { + go a.applyRelayAuto(0, band) + } + a.emitBandChangeTrigger(band, "", 0) +} diff --git a/udptrigger.go b/udptrigger.go index 6881106..0f0af2c 100644 --- a/udptrigger.go +++ b/udptrigger.go @@ -102,27 +102,54 @@ func (a *App) udpTriggerBandChange(s cat.RigState) { if a.udp == nil { return } - band := strings.ToLower(strings.TrimSpace(s.Band)) - if band == "" || !s.Connected { + if !s.Connected { return } - lastTriggerBandMu.Lock() - changed := band != lastTriggerBand - if changed { - lastTriggerBand = band + a.emitBandChangeTrigger(s.Band, s.Mode, s.FreqHz) +} + +// noteBandChange normalises a band and reports whether it is a NEW one. +// +// The state is shared by both sources on purpose. The rig reports a band change +// and so does the entry strip, and on a station that has both, one QSY produces +// both — an antenna switch must be commanded once, not twice. +func noteBandChange(raw string) (string, bool) { + band := strings.ToLower(strings.TrimSpace(raw)) + if band == "" { + return "", false } - lastTriggerBandMu.Unlock() + lastTriggerBandMu.Lock() + defer lastTriggerBandMu.Unlock() + if band == lastTriggerBand { + return band, false + } + lastTriggerBand = band + return band, true +} + +// emitBandChangeTrigger fires the band-change rows, once per NEW band. +// +// Split out of the rig-state path because the rig is not the only thing that +// changes band: a station with no CAT link changes it in the QSO entry strip, +// and that is just as much a band change to the antenna switch on the other end +// of the message. The de-duplication is shared, so the two sources cannot +// double-fire between them. +func (a *App) emitBandChangeTrigger(rawBand, mode string, freqHz int64) { + if a.udp == nil { + return + } + band, changed := noteBandChange(rawBand) if !changed { return } f := map[string]string{ "band": band, "band_m": bandMetres(band), - "mode": s.Mode, + "mode": mode, } - if s.FreqHz > 0 { - f["freq_hz"] = strconv.FormatInt(s.FreqHz, 10) - f["freq_mhz"] = fmt.Sprintf("%.6f", float64(s.FreqHz)/1e6) + if freqHz > 0 { + f["freq_hz"] = strconv.FormatInt(freqHz, 10) + f["freq_mhz"] = fmt.Sprintf("%.6f", float64(freqHz)/1e6) } a.udp.EmitTrigger(udp.TriggerBandChange, f) }