feat: the Yaesu console follows the mode — CW gets its own controls

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.
This commit is contained in:
2026-07-29 12:07:28 +02:00
parent 8cc6997eec
commit c9218310ae
8 changed files with 123 additions and 10 deletions
+42 -8
View File
@@ -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))} />
<span className="w-10 text-right text-xs font-mono tabular-nums text-muted-foreground">{view.rf_power}W</span>
</Row>
<Row label="MIC">
<Slider value={view.mic_gain} onChange={(v) => push('mic_gain', v, () => SetYaesuMicGain(v))} />
<span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{view.mic_gain}</span>
</Row>
<Row label="VOX">
<Chip on={view.vox} onClick={() => 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 && (
<>
<Row label="MIC">
<Slider value={view.mic_gain} onChange={(v) => push('mic_gain', v, () => SetYaesuMicGain(v))} />
<span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{view.mic_gain}</span>
</Row>
<Row label="VOX">
<Chip on={view.vox} onClick={() => push('vox', !view.vox, () => SetYaesuVOX(!view.vox))} label="VOX" />
</Row>
</>
)}
<Row label="">
<button type="button" onClick={() => RefreshYaesuPanel().catch((e) => setErr(String(e?.message ?? e)))}
className="ml-auto px-2 py-1 rounded-md text-[11px] font-bold border border-border bg-card text-muted-foreground hover:bg-muted">
{t('yaesu.refresh')}
</button>
</Row>
</Card>
{/* CW — only in CW, where these replace the phone controls above. */}
{isCW && (
<Card icon={Radio} title={t('yaesu.cw')}>
<Row label="SPEED">
<Slider value={view.key_speed || 20} min={4} max={60} accent="var(--primary)"
onChange={(v) => push('key_speed', v, () => SetYaesuKeySpeed(v))} />
<span className="w-12 text-right text-xs font-mono tabular-nums text-muted-foreground">{view.key_speed || 20} wpm</span>
</Row>
<div className="flex items-center gap-2 flex-wrap">
<Chip on={!!view.break_in} onClick={() => 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. */}
<button type="button" onClick={() => YaesuZeroIn().catch((e) => setErr(String(e?.message ?? e)))}
title={t('yaesu.zinHint')}
className="px-2 py-1 rounded-md text-[11px] font-bold border border-border bg-card text-muted-foreground hover:bg-muted">
ZIN
</button>
</div>
</Card>
)}
</div>
</div>
);
+2 -2
View File
@@ -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é',
+6
View File
@@ -923,6 +923,10 @@ export function SetYaesuAtt(arg1:number):Promise<void>;
export function SetYaesuBand(arg1:string):Promise<void>;
export function SetYaesuBreakIn(arg1:boolean):Promise<void>;
export function SetYaesuKeySpeed(arg1:number):Promise<void>;
export function SetYaesuMicGain(arg1:number):Promise<void>;
export function SetYaesuModeRaw(arg1:string):Promise<void>;
@@ -1030,3 +1034,5 @@ export function WinkeyerSetSpeed(arg1:number):Promise<void>;
export function WinkeyerStop():Promise<void>;
export function WorkedBefore(arg1:string,arg2:number):Promise<qso.WorkedBefore>;
export function YaesuZeroIn():Promise<void>;
+12
View File
@@ -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']();
}
+4
View File
@@ -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"];
}
}