From 6d309cada1f33cb43d3c0fb7e44b21c4f17d119c Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 29 Jul 2026 11:55:28 +0200 Subject: [PATCH] fix: SPLIT placed the transmitter on a stale VFO; share the meter widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported on the FTDX10: listening on 14.244 with VFO B still holding 18.115 from an earlier session, pressing SPLIT threw the transmitter onto another band. The button only flipped the rig's split flag, and the other VFO is stale by nature — the only transmit frequency that makes sense is one derived from where the operator is listening NOW. SPLIT therefore places the TX VFO too: up 1 kHz on CW and the data modes, up 5 kHz on phone, the offsets operators actually call. The +1k / +5k buttons remain for anything else, and a test pins the mapping. The panel also drew its own flat meters while the Flex and Icom consoles use the shared LED-segment MeterBar. Two instrument styles in one application is just inconsistency — it now uses the shared component, and the local one is gone. And the three consoles are named alike: "Flex Console", "Icom Console", "Yaesu Console", in the tabs and in the Main-view pane list, in both languages. --- changelog.json | 4 +-- frontend/src/App.tsx | 6 ++-- frontend/src/components/YaesuPanel.tsx | 38 +++++++----------------- frontend/src/lib/i18n.tsx | 4 +-- internal/cat/yaesu_panel.go | 27 ++++++++++++++++- internal/cat/yaesu_split_test.go | 40 ++++++++++++++++++++++++++ 6 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 internal/cat/yaesu_split_test.go diff --git a/changelog.json b/changelog.json index c1b19aa..2558d5f 100644 --- a/changelog.json +++ b/changelog.json @@ -3,7 +3,7 @@ "version": "0.21.9", "date": "2026-07-28", "en": [ - "Yaesu control panel: with the native Yaesu backend, a console tab and Main-view pane give S/PO/SWR meters, band and mode buttons, AF/RF/squelch, AGC, IPO-AMP1-AMP2, a 6/12/18 dB attenuator, NB, DNR, narrow filter, power, mic gain, VOX, split and ATU tune. CW, RTTY, DIGI and PSK show their sideband — click an active button again to switch it. Split shows where you transmit, with one-touch up 1 kHz (CW) and up 5 kHz (phone).", + "Yaesu control panel: with the native Yaesu backend, a console tab and Main-view pane give S/PO/SWR meters, band and mode buttons, AF/RF/squelch, AGC, IPO-AMP1-AMP2, a 6/12/18 dB attenuator, NB, DNR, narrow filter, power, mic gain, VOX, split and ATU tune. CW, RTTY, DIGI and PSK show their sideband — click an active button again to switch it. Pressing SPLIT places the transmit VFO relative to where you are listening — up 1 kHz on CW and data, up 5 kHz on phone — and shows the transmit frequency; +1k and +5k buttons cover the rest.", "Native Xiegu CAT (G90, X6100, X6200, X5105): a new backend talks CI-V straight to the radio — frequency, mode, split and PTT. Pick \"Xiegu (native CI-V)\" in Settings → CAT. Untested on a radio so far, feedback welcome.", "CAT sharing: OpsLog can now serve its rig connection to other programs (Settings → CAT → Share CAT). WSJT-X, JTDX, MSHV or Log4OM connect with rig model \"Hamlib NET rigctl\" at 127.0.0.1:4532 — needed because a native CAT backend holds the radio serial port on its own. Works with every backend, and with both Hamlib command dialects (JTDX names the VFO, MSHV does not).", "Native Yaesu CAT: a new backend talks to an FTDX10/FTDX101 directly over its serial port, without OmniRig — frequency, mode, VFO A/B, split and PTT. Pick \"Yaesu (native CAT)\" in Settings → CAT. First release, feedback welcome.", @@ -16,7 +16,7 @@ "CQ and ITU zones you correct by hand in your profile stay corrected. They are derived from cty.dat, which gives the zones of the whole entity — in a country spanning several, the automatic value is wrong and it came back at every restart. They now only fill in when empty, and recompute when the callsign or grid changes." ], "fr": [ - "Panneau de contrôle Yaesu : avec le backend Yaesu natif, un onglet console et un volet de la vue principale offrent les mesures S/PO/ROS, les boutons de bande et de mode, AF/RF/squelch, AGC, IPO-AMP1-AMP2, un atténuateur 6/12/18 dB, NB, DNR, filtre étroit, puissance, gain micro, VOX, split et accord d'antenne. CW, RTTY, DIGI et PSK affichent leur bande latérale — recliquez sur le bouton actif pour en changer. Le split indique où vous émettez, avec +1 kHz (CW) et +5 kHz (phonie) en un geste.", + "Panneau de contrôle Yaesu : avec le backend Yaesu natif, un onglet console et un volet de la vue principale offrent les mesures S/PO/ROS, les boutons de bande et de mode, AF/RF/squelch, AGC, IPO-AMP1-AMP2, un atténuateur 6/12/18 dB, NB, DNR, filtre étroit, puissance, gain micro, VOX, split et accord d'antenne. CW, RTTY, DIGI et PSK affichent leur bande latérale — recliquez sur le bouton actif pour en changer. Appuyer sur SPLIT place le VFO d'émission par rapport à votre écoute — +1 kHz en CW et numérique, +5 kHz en phonie — et affiche la fréquence d'émission ; les boutons +1k et +5k couvrent le reste.", "CAT Xiegu natif (G90, X6100, X6200, X5105) : un nouveau backend dialogue en CI-V directement avec la radio — fréquence, mode, split et PTT. À choisir dans Réglages → CAT sous « Xiegu (CI-V natif) ». Pas encore testé sur une radio, retours bienvenus.", "Partage du CAT : OpsLog peut désormais servir sa liaison radio aux autres logiciels (Réglages → CAT → Partager le CAT). WSJT-X, JTDX, MSHV ou Log4OM s'y connectent avec le modèle « Hamlib NET rigctl » sur 127.0.0.1:4532 — nécessaire car un backend CAT natif occupe seul le port série. Fonctionne avec tous les backends, et avec les deux dialectes Hamlib (JTDX nomme le VFO, MSHV non).", "CAT Yaesu natif : un nouveau backend dialogue directement avec un FTDX10/FTDX101 par son port série, sans OmniRig — fréquence, mode, VFO A/B, split et PTT. À choisir dans Réglages → CAT sous « Yaesu (CAT natif) ». Première version, retours bienvenus.", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 1ecd43e..18902d5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5371,9 +5371,9 @@ export default function App() { )} - {catState.backend === 'flex' && FlexRadio} - {catState.backend === 'icom' && Icom} - {catState.backend === 'yaesu' && Yaesu} + {catState.backend === 'flex' && Flex Console} + {catState.backend === 'icom' && Icom Console} + {catState.backend === 'yaesu' && Yaesu Console} {statsTabOpen && ( {t('stats.tab')} diff --git a/frontend/src/components/YaesuPanel.tsx b/frontend/src/components/YaesuPanel.tsx index ebf13da..612eadd 100644 --- a/frontend/src/components/YaesuPanel.tsx +++ b/frontend/src/components/YaesuPanel.tsx @@ -10,6 +10,7 @@ import { import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { sMeterRST } from '@/lib/rst'; +import { MeterBar } from '@/components/MeterBar'; type YaesuState = { available: boolean; model?: string; mode?: string; raw_mode?: string; @@ -206,27 +207,6 @@ function Row({ label, children }: { label: string; children: React.ReactNode }) ); } -function Meter({ label, value, accent, text, onClick, title }: { - label: string; value: number; accent: string; text?: string; onClick?: () => void; title?: string; -}) { - const v = Math.max(0, Math.min(100, value)); - const body = ( - <> - {label} -
-
-
- {/* The S meter reads in S UNITS, not a percentage — "57" told the operator - nothing, and it is the S number that goes into a report. */} - {text ?? v} - - ); - if (onClick) { - return ; - } - return
{body}
; -} - export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => void }) { const { t } = useI18n(); const [st, setSt] = useState(ZERO); @@ -327,13 +307,17 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi {err &&

{err}

} - {/* Meters */} + {/* Meters — the SHARED MeterBar the Flex and Icom panels use, so the three + consoles read alike instead of each having its own instrument style. */} - { const sp = sParts(view.s_meter); onReportRST(sMeterRST(sp.s, sp.over, view.mode)); } : undefined} - title={t('yaesu.sToRst')} /> - - +
+ { const sp = sParts(view.s_meter); onReportRST(sMeterRST(sp.s, sp.over, view.mode)); } : undefined} + title={onReportRST ? t('yaesu.sToRst') : undefined} /> + + +
{/* Bands + modes */} diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 5532e5d..067c642 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -106,7 +106,7 @@ const en: Dict = { 'settings.pane.map1': 'Map — great-circle + beam', 'settings.pane.map2': 'Map — locator (street)', 'settings.pane.cluster': 'Cluster spots', 'settings.pane.worked': 'Worked before', 'settings.pane.recent': 'Recent QSOs', 'settings.pane.netcontrol': 'Net control', - 'settings.pane.flex': 'FlexRadio controls', 'settings.pane.icom': 'Icom console', 'settings.pane.yaesu': 'Yaesu console', 'yaesu.notConnected': 'Yaesu CAT not connected', 'yaesu.meters': 'Meters', 'yaesu.bandMode': 'Band & mode', 'yaesu.receive': 'Receive', 'yaesu.noiseFilter': 'Noise & filter', 'yaesu.transmit': 'Transmit', 'yaesu.refresh': 'Refresh', 'yaesu.tuneHint': 'Start an antenna-tuner cycle', 'yaesu.sToRst': 'Click to fill the RST sent', 'yaesu.sidebandHint': 'Click to select this mode; click again to switch sideband (U/L)', 'yaesu.splitUpHint': 'Transmit this far above the receive frequency, and turn split on', 'yaesu.txOn': 'TX', + 'settings.pane.flex': 'Flex Console', 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'Yaesu CAT not connected', 'yaesu.meters': 'Meters', 'yaesu.bandMode': 'Band & mode', 'yaesu.receive': 'Receive', 'yaesu.noiseFilter': 'Noise & filter', 'yaesu.transmit': 'Transmit', 'yaesu.refresh': 'Refresh', 'yaesu.tuneHint': 'Start an antenna-tuner cycle', 'yaesu.sToRst': 'Click to fill the RST sent', 'yaesu.sidebandHint': 'Click to select this mode; click again to switch sideband (U/L)', 'yaesu.splitUpHint': 'Transmit this far above the receive frequency, and turn split on', 'yaesu.txOn': 'TX', 'theme.auto': 'Auto (system)', 'theme.light-warm': 'Warm light', 'theme.light-cool': 'Cool light', 'theme.light-sage': 'Sage light', 'theme.dim-slate': 'Dim slate', 'theme.dark-warm': 'Warm dark', 'theme.dark-graphite': 'Graphite dark', 'theme.high-contrast': 'High contrast', @@ -518,7 +518,7 @@ const fr: Dict = { 'settings.pane.map1': 'Carte — orthodromie + faisceau', 'settings.pane.map2': 'Carte — locator (rue)', 'settings.pane.cluster': 'Spots cluster', 'settings.pane.worked': 'Déjà contactés', 'settings.pane.recent': 'QSO récents', 'settings.pane.netcontrol': 'Gestion de net', - 'settings.pane.flex': 'Contrôles FlexRadio', 'settings.pane.icom': 'Console Icom', 'settings.pane.yaesu': 'Console Yaesu', 'yaesu.notConnected': 'CAT Yaesu non connecté', 'yaesu.meters': 'Mesures', 'yaesu.bandMode': 'Bande et mode', 'yaesu.receive': 'Réception', 'yaesu.noiseFilter': 'Bruit et filtre', 'yaesu.transmit': 'Émission', 'yaesu.refresh': 'Actualiser', 'yaesu.tuneHint': "Lancer un cycle d'accord d'antenne", 'yaesu.sToRst': 'Cliquer pour remplir le RST envoyé', 'yaesu.sidebandHint': 'Cliquer pour choisir ce mode ; recliquer pour changer de bande latérale (U/L)', 'yaesu.splitUpHint': "Émettre à cette distance au-dessus de la fréquence de réception, et activer le split", 'yaesu.txOn': 'TX', + 'settings.pane.flex': 'Flex Console', 'settings.pane.icom': 'Icom Console', 'settings.pane.yaesu': 'Yaesu Console', 'yaesu.notConnected': 'CAT Yaesu non connecté', 'yaesu.meters': 'Mesures', 'yaesu.bandMode': 'Bande et mode', 'yaesu.receive': 'Réception', 'yaesu.noiseFilter': 'Bruit et filtre', 'yaesu.transmit': 'Émission', 'yaesu.refresh': 'Actualiser', 'yaesu.tuneHint': "Lancer un cycle d'accord d'antenne", 'yaesu.sToRst': 'Cliquer pour remplir le RST envoyé', 'yaesu.sidebandHint': 'Cliquer pour choisir ce mode ; recliquer pour changer de bande latérale (U/L)', 'yaesu.splitUpHint': "Émettre à cette distance au-dessus de la fréquence de réception, et activer le split", 'yaesu.txOn': 'TX', 'theme.auto': 'Auto (système)', 'theme.light-warm': 'Clair chaud', 'theme.light-cool': 'Clair froid', 'theme.light-sage': 'Clair sauge', 'theme.dim-slate': 'Ardoise tamisé', 'theme.dark-warm': 'Sombre chaud', 'theme.dark-graphite': 'Sombre graphite', 'theme.high-contrast': 'Contraste élevé', diff --git a/internal/cat/yaesu_panel.go b/internal/cat/yaesu_panel.go index 1ff4b1d..e139e73 100644 --- a/internal/cat/yaesu_panel.go +++ b/internal/cat/yaesu_panel.go @@ -278,13 +278,38 @@ func (y *Yaesu) SetYaesuVOX(on bool) error { // other one would be silently ignored, and the operator would get a split button // that does nothing. func (y *Yaesu) SetYaesuSplit(on bool) error { + // Turning split ON also PLACES the transmit VFO. Flipping the flag alone + // transmits wherever the other VFO happens to sit — it held 18.115 from an + // earlier session while the operator was listening on 14.244, so pressing + // SPLIT threw them onto another band entirely. The other VFO is stale by + // nature; the only frequency that makes sense is one derived from where the + // operator is listening NOW. + // + // The distance is the mode's usual one: 1 kHz on CW and the data modes, + // 5 kHz on phone. The +1k / +5k buttons remain for anything else. + if on { + return y.SetYaesuSplitOffset(y.defaultSplitOffset()) + } y.mu.Lock() cmd := y.splitCmd y.mu.Unlock() if cmd == "" { return fmt.Errorf("yaesu: this rig answered neither ST; nor FT; — split cannot be set") } - return y.setAndRefresh(fmt.Sprintf("%s%d;", cmd, boolDigit(on))) + return y.setAndRefresh(fmt.Sprintf("%s0;", cmd)) +} + +// defaultSplitOffset is what SPLIT means on this mode: up 1 kHz on CW and the +// data modes, up 5 kHz on phone — the offsets operators actually call. +func (y *Yaesu) defaultSplitOffset() int64 { + y.mu.Lock() + raw := strings.ToUpper(y.panel.RawMode) + y.mu.Unlock() + switch { + case strings.HasPrefix(raw, "CW"), strings.HasPrefix(raw, "RTTY"), strings.HasPrefix(raw, "DATA"): + return 1000 + } + return 5000 } // SetYaesuBand switches band with BS, which lands the rig on ITS OWN last-used diff --git a/internal/cat/yaesu_split_test.go b/internal/cat/yaesu_split_test.go new file mode 100644 index 0000000..3cab816 --- /dev/null +++ b/internal/cat/yaesu_split_test.go @@ -0,0 +1,40 @@ +package cat + +import "testing" + +// What SPLIT means when the operator presses it. +// +// Flipping the rig's split flag alone transmits wherever the OTHER VFO happens +// to sit — reported from a real FTDX10: listening on 14.244 with VFO B left on +// 18.115 from an earlier session, pressing SPLIT threw the transmitter onto +// another band. The other VFO is stale by nature, so the transmit frequency has +// to be derived from where the operator is listening now. +func TestYaesuDefaultSplitOffset(t *testing.T) { + cases := []struct { + raw string + want int64 + }{ + // CW and the data modes work 1 kHz up. + {"CW-U", 1000}, + {"CW-L", 1000}, + {"RTTY-U", 1000}, + {"RTTY-L", 1000}, + {"DATA-U", 1000}, + {"DATA-L", 1000}, + // Phone works 5 kHz up. + {"USB", 5000}, + {"LSB", 5000}, + {"AM", 5000}, + {"FM", 5000}, + // Unknown or not yet read: the phone offset is the safer default — too + // wide is audible and obvious, too narrow lands on top of the DX. + {"", 5000}, + } + for _, c := range cases { + y := &Yaesu{} + y.panel.RawMode = c.raw + if got := y.defaultSplitOffset(); got != c.want { + t.Errorf("mode %q → split offset %d Hz, want %d", c.raw, got, c.want) + } + } +}