diff --git a/app.go b/app.go index 743daad..16ec7d5 100644 --- a/app.go +++ b/app.go @@ -709,13 +709,18 @@ type App struct { // Halt Tx is routed by id, and the panel's Halt button must work even when // nothing is transmitting at that instant — so the id is remembered from // every Status rather than read off a live transmission. - lastTxID atomic.Value // string - extsvc *extsvc.Manager - winkeyer *winkeyer.Manager - clublog *clublog.Manager - clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in) - motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled - ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off + lastTxID atomic.Value // string + extsvc *extsvc.Manager + winkeyer *winkeyer.Manager + clublog *clublog.Manager + clublogMW *clublog.MostWanted // ClubLog "Most Wanted" DXCC ranking (opt-in) + motorAnt motorAntenna // motorized antenna (Ultrabeam or SteppIR); nil when disabled + ubFollowStop chan struct{} // stops the "follow frequency" loop; nil when off + // qsoRecPushed records which SOURCE the recorder was started on: the network + // stream, or a sound device. Switching CAT backends changes the answer, and a + // recorder left capturing a device that no longer carries the radio records + // silence without saying so. + qsoRecPushed bool motorStartMu sync.Mutex // serialises startUltrabeam: two restarts at once left two poll loops on one COM port motorInhibStop chan struct{} // stops the "inhibit TX while moving" loop; nil when off motorMoveCmdNs atomic.Int64 // unixnano of the last commanded antenna move (grace window) @@ -8321,7 +8326,19 @@ func (a *App) startQSORecorderIfEnabled() { if !cfg.QSORecord { return } - if err := a.qsoRec.Start(cfg.FromRadio, cfg.RecordingDevice, cfg.PrerollSeconds); err != nil { + // Over the network there IS no radio sound device. + // + // An IC-705 or IC-7610 reached by LAN streams its receive audio through the + // Icom protocol, and Windows sees nothing: the operator's audio settings can + // only offer the PC's own microphone and speakers, so "From radio" has + // nothing right to point at. The stream is pushed into the recorder instead + // — same samples, no sound card in the middle, and no virtual cable to set up. + from := cfg.FromRadio + a.qsoRecPushed = a.icomNetAudioActive() + if a.qsoRecPushed { + from = audio.PushedSource + } + if err := a.qsoRec.Start(from, cfg.RecordingDevice, cfg.PrerollSeconds); err != nil { applog.Printf("qso-rec: start failed: %v", err) return } @@ -8336,6 +8353,17 @@ func (a *App) startQSORecorderIfEnabled() { applog.Printf("qso-rec: running (preroll %ds, mix=%v, gains rx=%.2f mic=%.2f)", cfg.PrerollSeconds, cfg.RecordingDevice != "" && cfg.RecordingDevice != cfg.FromRadio, fromGain, micGain) } +// icomNetAudioActive reports whether the receive audio is arriving over the +// network rather than from a sound card — the Icom LAN backend with its audio +// option on. +func (a *App) icomNetAudioActive() bool { + s, err := a.GetCATSettings() + if err != nil { + return false + } + return s.Enabled && s.Backend == "icom-net" && s.IcomNetAudio +} + // qsoRecDir returns the configured recordings folder, defaulting to // /Recordings, and ensures it exists. func (a *App) qsoRecDir() string { @@ -14892,6 +14920,13 @@ func (a *App) reloadCAT() { a.catFlexDecodeSpots = s.Enabled && s.Backend == "flex" && s.FlexDecodeSpots a.catFlexDecodeSecs = s.FlexDecodeSecs a.catFlexDVKDax = s.Enabled && s.Backend == "flex" && s.FlexDVKDax + // The recorder's source depends on the backend: over the Icom LAN there is no + // sound card to capture, the audio is pushed in. Restarted only when the + // answer CHANGES, so an ordinary settings save never interrupts a recording. + if want := s.Enabled && s.Backend == "icom-net" && s.IcomNetAudio; want != a.qsoRecPushed { + applog.Printf("qso-rec: audio source changes (network=%v) — restarting the recorder", want) + go a.startQSORecorderIfEnabled() + } a.reloadCATShare(s) // Nothing about the link changed → leave it connected. See catLinkSig. if sig := catLinkSig(s); sig == a.catSig { @@ -15000,8 +15035,16 @@ func (a *App) reloadCAT() { } else { codec := audio.NewPCM16Codec() audioSink = func(payload []byte) { - if pcm, err := codec.Decode(payload); err == nil { - a.audioMgr.PushMonitorAudio(pcm) + pcm, err := codec.Decode(payload) + if err != nil { + return + } + a.audioMgr.PushMonitorAudio(pcm) + // And to the QSO recorder, which has no device to capture from + // on this backend. It drops the samples unless a recording is + // actually running, so this costs a function call when idle. + if a.qsoRec != nil { + a.qsoRec.PushRX(pcm) } } applog.Printf("icom-net audio: RX audio streaming ENABLED (experimental) → %q", acfg.ListeningDevice) diff --git a/app_gate.go b/app_gate.go index 1f58253..dfa2cdf 100644 --- a/app_gate.go +++ b/app_gate.go @@ -19,6 +19,7 @@ var deniedCallHashes = map[string]struct{}{ "d1ae6212ec057f9d5c6f379fb57acedac7c94142c9d49667dc04b2016d03d1b6": {}, "0741c9e394b42f43191899105553b47155ddc3026da12b5360701f9c181ff123": {}, "ab4926a3a0ab76d41b5b99cd3ad0683584970c341c29427c1dfa4b3c329ce415": {}, + "9d17c9c213a6cc89c12d7520bcf21c86a0cf43d17e82e74f33f3a77cb865d28a": {}, } // callDenied reports whether a callsign is on deniedCallHashes. The call is diff --git a/changelog.json b/changelog.json index db1bb9a..224bf1d 100644 --- a/changelog.json +++ b/changelog.json @@ -1,4 +1,22 @@ [ + { + "version": "0.26.7", + "date": "", + "en": [ + "The NEW badge on a county now shows in the entry form itself, inside the field, and not only on the Info tab — it answers the question at the moment the operator is deciding whether to call.", + "OmniRig: a Yaesu is no longer sent OmniRig's SetSimplexMode when tuning. It is a no-op on some (an FT-891 never moved) and harmful on others — on an FT-2000 it turned split on and moved reception to VFO B, which is why OpsLog then showed B instead of A. The direct frequency write, which is what tunes these rigs, is unchanged.", + "The basemap buttons on the main map are shifted clear of the zoom controls — Light sat a few pixels from the minus button and was being clicked by mistake.", + "Cluster, French interface: the WKD CALL status now reads DÉJÀ QSO instead of DÉJÀ CTC.", + "Icom over LAN: the receive audio streamed by the rig now feeds the QSO recorder as well as the speakers. On a network connection Windows sees no radio sound card at all — the audio settings could only offer the PC microphone — so the stream is pushed straight into the recorder, with no virtual cable to set up." + ], + "fr": [ + "Le badge NOUV du comté s'affiche maintenant dans le formulaire de saisie lui-même, à l'intérieur du champ, et plus seulement dans l'onglet Info — il répond au moment où l'opérateur décide d'appeler ou non.", + "OmniRig : le SetSimplexMode d'OmniRig n'est plus envoyé aux Yaesu lors d'un changement de fréquence. Il est sans effet sur certains (un FT-891 ne bougeait pas) et nuisible sur d'autres — sur un FT-2000 il activait le split et faisait passer la réception sur le VFO B, d'où l'affichage du B au lieu du A. L'écriture directe de la fréquence, qui est ce qui accorde réellement ces postes, ne change pas.", + "Les boutons de fond de carte sont décalés à l'écart des commandes de zoom — Light était à quelques pixels du bouton moins et se faisait cliquer par erreur.", + "Cluster, interface française : le statut « DÉJÀ CTC » devient « DÉJÀ QSO ».", + "Icom en réseau : l'audio reçu diffusé par le poste alimente maintenant l'enregistreur de QSO en plus des haut-parleurs. Sur une liaison réseau, Windows ne voit aucune carte son de la radio — les réglages audio ne proposaient que le micro du PC — donc le flux est injecté directement dans l'enregistreur, sans câble virtuel à installer." + ] + }, { "version": "0.26.6", "date": "", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6311f60..bcd71ec 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5214,9 +5214,21 @@ export default function App() { }; const geoDetailRow = (
-
- + setDetails((d) => ({ ...d, cnty: e.target.value }))} /> + {entryNewCounty && ( + + {t('detp.newCounty')} + + )}
{/* Basemap picker — Light / Street / Satellite (key-free tiles). */} -
+ {/* Clear of the zoom buttons, not merely next to them: at left-12 the + first basemap sat a few pixels from the − button and operators kept + hitting Light when they meant to zoom out. */} +
{(Object.keys(BASEMAPS) as BasemapKey[]).map((k) => (