From c9218310aefa6240ee0c0e942b1f403221bec1d5 Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 29 Jul 2026 12:07:28 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20the=20Yaesu=20console=20follows=20the?= =?UTF-8?q?=20mode=20=E2=80=94=20CW=20gets=20its=20own=20controls?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Microphone gain and VOX are meaningless in CW: the rig ignores both, so showing them is showing dead controls. They are hidden, and a CW card takes their place with keyer speed (KS), break-in (BI) and ZIN (ZI), the zero-in that retunes so the station being received lands on the operator's own pitch. ZIN is a one-shot with no state, so it is a plain button rather than a chip that would look latched, and no settings read-back follows — the frequency change arrives through the normal poll like any other. The keyer values are read on the slow beat whatever the mode, so the card is already populated the instant the operator switches to CW instead of filling in a poll cycle later. Which controls show is decided by the RIG's mode, not the logged one: the logged mode can be a digital sub-mode the radio knows nothing about. --- app.go | 14 ++++++++ changelog.json | 2 ++ frontend/src/components/YaesuPanel.tsx | 50 +++++++++++++++++++++----- frontend/src/lib/i18n.tsx | 4 +-- frontend/wailsjs/go/main/App.d.ts | 6 ++++ frontend/wailsjs/go/main/App.js | 12 +++++++ frontend/wailsjs/go/models.ts | 4 +++ internal/cat/yaesu_panel.go | 41 +++++++++++++++++++++ 8 files changed, 123 insertions(+), 10 deletions(-) diff --git a/app.go b/app.go index fe23fb0..3a28be4 100644 --- a/app.go +++ b/app.go @@ -14991,3 +14991,17 @@ func (a *App) SetYaesuModeRaw(mode string) error { func (a *App) SetYaesuSplitOffset(offsetHz int64) error { return a.yaesuDo(func(y cat.YaesuController) error { return y.SetYaesuSplitOffset(offsetHz) }) } + +func (a *App) SetYaesuKeySpeed(wpm int) error { + return a.yaesuDo(func(y cat.YaesuController) error { return y.SetYaesuKeySpeed(wpm) }) +} + +func (a *App) SetYaesuBreakIn(on bool) error { + return a.yaesuDo(func(y cat.YaesuController) error { return y.SetYaesuBreakIn(on) }) +} + +// YaesuZeroIn retunes the rig so the received CW signal lands on the operator's +// own pitch — the radio's ZIN key. +func (a *App) YaesuZeroIn() error { + return a.yaesuDo(func(y cat.YaesuController) error { return y.YaesuZeroIn() }) +} diff --git a/changelog.json b/changelog.json index 2558d5f..ef87148 100644 --- a/changelog.json +++ b/changelog.json @@ -4,6 +4,7 @@ "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. 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.", + "The Yaesu console follows the mode: in CW the microphone gain and VOX disappear and a CW card takes their place with keyer speed, break-in and ZIN (zero-in).", "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.", @@ -17,6 +18,7 @@ ], "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. 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.", + "La console Yaesu suit le mode : en CW, le gain micro et le VOX disparaissent au profit d'une carte CW avec la vitesse du manipulateur, le break-in et ZIN (zéro-in).", "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/components/YaesuPanel.tsx b/frontend/src/components/YaesuPanel.tsx index 800da89..54865c6 100644 --- a/frontend/src/components/YaesuPanel.tsx +++ b/frontend/src/components/YaesuPanel.tsx @@ -5,7 +5,7 @@ import { SetYaesuPower, SetYaesuMicGain, SetYaesuAFGain, SetYaesuRFGain, SetYaesuSquelch, SetYaesuAGC, SetYaesuPreamp, SetYaesuAtt, SetYaesuNB, SetYaesuNR, SetYaesuNRLevel, SetYaesuNarrow, SetYaesuVOX, SetYaesuSplit, SetYaesuBand, TuneYaesuATU, - SetYaesuModeRaw, SetYaesuSplitOffset, GetCATState, + SetYaesuModeRaw, SetYaesuSplitOffset, SetYaesuKeySpeed, SetYaesuBreakIn, YaesuZeroIn, GetCATState, } from '../../wailsjs/go/main/App'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; @@ -19,7 +19,7 @@ type YaesuState = { rf_power: number; mic_gain: number; af_gain: number; rf_gain: number; squelch: number; agc?: string; preamp: number; att: number; nb: boolean; nr: boolean; nr_level: number; narrow: boolean; vox: boolean; - split_tx_hz?: number; + split_tx_hz?: number; key_speed?: number; break_in?: boolean; }; const ZERO: YaesuState = { @@ -268,6 +268,9 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi } const band = bandOfHz(freqHz); + // CW changes what belongs on the panel: no microphone, no VOX, but a keyer + // speed, break-in and ZIN. Driven by the RIG's mode, not the logged one. + const isCW = (view.raw_mode || '').toUpperCase().startsWith('CW'); if (!st.available) { return ( @@ -427,18 +430,49 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi onChange={(v) => push('rf_power', v, () => SetYaesuPower(v))} /> {view.rf_power}W - - push('mic_gain', v, () => SetYaesuMicGain(v))} /> - {view.mic_gain} - - - push('vox', !view.vox, () => SetYaesuVOX(!view.vox))} label="VOX" /> + {/* Microphone gain and VOX are meaningless in CW — the rig ignores both + — so they are hidden rather than shown as dead controls. */} + {!isCW && ( + <> + + push('mic_gain', v, () => SetYaesuMicGain(v))} /> + {view.mic_gain} + + + push('vox', !view.vox, () => SetYaesuVOX(!view.vox))} label="VOX" /> + + + )} + + + {/* CW — only in CW, where these replace the phone controls above. */} + {isCW && ( + + + push('key_speed', v, () => SetYaesuKeySpeed(v))} /> + {view.key_speed || 20} wpm + +
+ push('break_in', !view.break_in, () => SetYaesuBreakIn(!view.break_in))} + label="BK-IN" title={t('yaesu.breakInHint')} /> + {/* ZIN is a one-shot: the rig retunes so the station being received + lands on the operator's own CW pitch. Not a toggle, so it is a + plain button rather than a chip that would look latched. */} + +
+
+ )} ); diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 067c642..28a4139 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': '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', + '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', 'yaesu.cw': 'CW', 'yaesu.breakInHint': 'Break-in: the rig switches to receive between characters', 'yaesu.zinHint': 'Zero-in: retune so the station you hear lands on your CW pitch', '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': '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', + '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', 'yaesu.cw': 'CW', 'yaesu.breakInHint': "Break-in : la radio repasse en réception entre les caractères", 'yaesu.zinHint': "Zéro-in : réaccorde pour que la station entendue tombe sur votre note CW", '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/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 25c30cb..1bea998 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -923,6 +923,10 @@ export function SetYaesuAtt(arg1:number):Promise; export function SetYaesuBand(arg1:string):Promise; +export function SetYaesuBreakIn(arg1:boolean):Promise; + +export function SetYaesuKeySpeed(arg1:number):Promise; + export function SetYaesuMicGain(arg1:number):Promise; export function SetYaesuModeRaw(arg1:string):Promise; @@ -1030,3 +1034,5 @@ export function WinkeyerSetSpeed(arg1:number):Promise; export function WinkeyerStop():Promise; export function WorkedBefore(arg1:string,arg2:number):Promise; + +export function YaesuZeroIn():Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index 02af3fa..93bcd1c 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -1794,6 +1794,14 @@ export function SetYaesuBand(arg1) { return window['go']['main']['App']['SetYaesuBand'](arg1); } +export function SetYaesuBreakIn(arg1) { + return window['go']['main']['App']['SetYaesuBreakIn'](arg1); +} + +export function SetYaesuKeySpeed(arg1) { + return window['go']['main']['App']['SetYaesuKeySpeed'](arg1); +} + export function SetYaesuMicGain(arg1) { return window['go']['main']['App']['SetYaesuMicGain'](arg1); } @@ -2009,3 +2017,7 @@ export function WinkeyerStop() { export function WorkedBefore(arg1, arg2) { return window['go']['main']['App']['WorkedBefore'](arg1, arg2); } + +export function YaesuZeroIn() { + return window['go']['main']['App']['YaesuZeroIn'](); +} diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index b74fbc6..5429bfc 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1098,6 +1098,8 @@ export namespace cat { nr_level: number; narrow: boolean; vox: boolean; + key_speed: number; + break_in: boolean; static createFrom(source: any = {}) { return new YaesuTXState(source); @@ -1128,6 +1130,8 @@ export namespace cat { this.nr_level = source["nr_level"]; this.narrow = source["narrow"]; this.vox = source["vox"]; + this.key_speed = source["key_speed"]; + this.break_in = source["break_in"]; } } diff --git a/internal/cat/yaesu_panel.go b/internal/cat/yaesu_panel.go index e139e73..2173b3f 100644 --- a/internal/cat/yaesu_panel.go +++ b/internal/cat/yaesu_panel.go @@ -55,6 +55,10 @@ type YaesuTXState struct { NRLevel int `json:"nr_level"` // 1-15 Narrow bool `json:"narrow"` // NAR filter VOX bool `json:"vox"` + // CW-only controls. Read (and shown) only in CW, where MIC and VOX mean + // nothing and these are what an operator reaches for. + KeySpeed int `json:"key_speed"` // WPM + BreakIn bool `json:"break_in"` } // YaesuController is the typed escape the Manager exposes for the panel, in the @@ -75,6 +79,9 @@ type YaesuController interface { SetYaesuNRLevel(int) error SetYaesuNarrow(bool) error SetYaesuVOX(bool) error + SetYaesuKeySpeed(int) error + SetYaesuBreakIn(bool) error + YaesuZeroIn() error SetYaesuSplit(bool) error SetYaesuSplitOffset(int64) error SetYaesuBand(string) error @@ -173,6 +180,15 @@ func (y *Yaesu) readPanelSettings() { if v, ok := y.askNum("VX;", "VX", 1); ok { y.panel.VOX = v != 0 } + // CW keyer. Read unconditionally — it is two more queries on the SLOW beat, + // and having the value ready means the CW card is populated the instant the + // operator switches mode rather than a poll cycle later. + if v, ok := y.askNum("KS;", "KS", 3); ok { + y.panel.KeySpeed = v + } + if v, ok := y.askNum("BI;", "BI", 1); ok { + y.panel.BreakIn = v != 0 + } } // askNum sends a query and reads a fixed-width decimal field out of the reply. @@ -563,3 +579,28 @@ func (y *Yaesu) SetYaesuSplitOffset(offsetHz int64) error { y.panel.SplitTXHz = tx return nil } + +// SetYaesuKeySpeed sets the internal keyer speed in words per minute. The rig +// clamps to its own 4-60 range; clamping here too keeps a slider from sending a +// value that would simply be ignored, which reads as a dead control. +func (y *Yaesu) SetYaesuKeySpeed(wpm int) error { + return y.setAndRefresh(fmt.Sprintf("KS%03d;", clampInt(wpm, 4, 60))) +} + +// SetYaesuBreakIn toggles CW break-in (BK). +func (y *Yaesu) SetYaesuBreakIn(on bool) error { + return y.setAndRefresh(fmt.Sprintf("BI%d;", boolDigit(on))) +} + +// YaesuZeroIn is the CW ZIN function: the rig retunes itself so the station +// being received lands exactly on the operator's CW pitch. It is a one-shot +// action with no state to read back, so no settings refresh follows — and the +// frequency change arrives through the normal poll like any other. +func (y *Yaesu) YaesuZeroIn() error { + y.mu.Lock() + defer y.mu.Unlock() + if y.port == nil { + return fmt.Errorf("yaesu: not connected") + } + return y.write("ZI;") +}