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
+14
View File
@@ -14991,3 +14991,17 @@ func (a *App) SetYaesuModeRaw(mode string) error {
func (a *App) SetYaesuSplitOffset(offsetHz int64) error { func (a *App) SetYaesuSplitOffset(offsetHz int64) error {
return a.yaesuDo(func(y cat.YaesuController) error { return y.SetYaesuSplitOffset(offsetHz) }) 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() })
}
+2
View File
@@ -4,6 +4,7 @@
"date": "2026-07-28", "date": "2026-07-28",
"en": [ "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.", "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.", "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).", "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.", "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": [ "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.", "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.", "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).", "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.", "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.",
+42 -8
View File
@@ -5,7 +5,7 @@ import {
SetYaesuPower, SetYaesuMicGain, SetYaesuAFGain, SetYaesuRFGain, SetYaesuSquelch, SetYaesuPower, SetYaesuMicGain, SetYaesuAFGain, SetYaesuRFGain, SetYaesuSquelch,
SetYaesuAGC, SetYaesuPreamp, SetYaesuAtt, SetYaesuNB, SetYaesuNR, SetYaesuNRLevel, SetYaesuAGC, SetYaesuPreamp, SetYaesuAtt, SetYaesuNB, SetYaesuNR, SetYaesuNRLevel,
SetYaesuNarrow, SetYaesuVOX, SetYaesuSplit, SetYaesuBand, TuneYaesuATU, SetYaesuNarrow, SetYaesuVOX, SetYaesuSplit, SetYaesuBand, TuneYaesuATU,
SetYaesuModeRaw, SetYaesuSplitOffset, GetCATState, SetYaesuModeRaw, SetYaesuSplitOffset, SetYaesuKeySpeed, SetYaesuBreakIn, YaesuZeroIn, GetCATState,
} from '../../wailsjs/go/main/App'; } from '../../wailsjs/go/main/App';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n'; 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; rf_power: number; mic_gain: number; af_gain: number; rf_gain: number; squelch: number;
agc?: string; preamp: number; att: number; agc?: string; preamp: number; att: number;
nb: boolean; nr: boolean; nr_level: number; narrow: boolean; vox: boolean; 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 = { const ZERO: YaesuState = {
@@ -268,6 +268,9 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi
} }
const band = bandOfHz(freqHz); 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) { if (!st.available) {
return ( return (
@@ -427,18 +430,49 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi
onChange={(v) => push('rf_power', v, () => SetYaesuPower(v))} /> 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> <span className="w-10 text-right text-xs font-mono tabular-nums text-muted-foreground">{view.rf_power}W</span>
</Row> </Row>
<Row label="MIC"> {/* Microphone gain and VOX are meaningless in CW — the rig ignores both
<Slider value={view.mic_gain} onChange={(v) => push('mic_gain', v, () => SetYaesuMicGain(v))} /> — so they are hidden rather than shown as dead controls. */}
<span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{view.mic_gain}</span> {!isCW && (
</Row> <>
<Row label="VOX"> <Row label="MIC">
<Chip on={view.vox} onClick={() => push('vox', !view.vox, () => SetYaesuVOX(!view.vox))} label="VOX" /> <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)))} <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"> 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')} {t('yaesu.refresh')}
</button> </button>
</Row> </Row>
</Card> </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>
</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.map1': 'Map — great-circle + beam', 'settings.pane.map2': 'Map — locator (street)',
'settings.pane.cluster': 'Cluster spots', 'settings.pane.worked': 'Worked before', 'settings.pane.cluster': 'Cluster spots', 'settings.pane.worked': 'Worked before',
'settings.pane.recent': 'Recent QSOs', 'settings.pane.netcontrol': 'Net control', '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.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.light-sage': 'Sage light', 'theme.dim-slate': 'Dim slate', 'theme.dark-warm': 'Warm dark',
'theme.dark-graphite': 'Graphite dark', 'theme.high-contrast': 'High contrast', '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.map1': 'Carte — orthodromie + faisceau', 'settings.pane.map2': 'Carte — locator (rue)',
'settings.pane.cluster': 'Spots cluster', 'settings.pane.worked': 'Déjà contactés', '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.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.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.light-sage': 'Clair sauge', 'theme.dim-slate': 'Ardoise tamisé', 'theme.dark-warm': 'Sombre chaud',
'theme.dark-graphite': 'Sombre graphite', 'theme.high-contrast': 'Contraste élevé', '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 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 SetYaesuMicGain(arg1:number):Promise<void>;
export function SetYaesuModeRaw(arg1:string):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 WinkeyerStop():Promise<void>;
export function WorkedBefore(arg1:string,arg2:number):Promise<qso.WorkedBefore>; 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); 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) { export function SetYaesuMicGain(arg1) {
return window['go']['main']['App']['SetYaesuMicGain'](arg1); return window['go']['main']['App']['SetYaesuMicGain'](arg1);
} }
@@ -2009,3 +2017,7 @@ export function WinkeyerStop() {
export function WorkedBefore(arg1, arg2) { export function WorkedBefore(arg1, arg2) {
return window['go']['main']['App']['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; nr_level: number;
narrow: boolean; narrow: boolean;
vox: boolean; vox: boolean;
key_speed: number;
break_in: boolean;
static createFrom(source: any = {}) { static createFrom(source: any = {}) {
return new YaesuTXState(source); return new YaesuTXState(source);
@@ -1128,6 +1130,8 @@ export namespace cat {
this.nr_level = source["nr_level"]; this.nr_level = source["nr_level"];
this.narrow = source["narrow"]; this.narrow = source["narrow"];
this.vox = source["vox"]; this.vox = source["vox"];
this.key_speed = source["key_speed"];
this.break_in = source["break_in"];
} }
} }
+41
View File
@@ -55,6 +55,10 @@ type YaesuTXState struct {
NRLevel int `json:"nr_level"` // 1-15 NRLevel int `json:"nr_level"` // 1-15
Narrow bool `json:"narrow"` // NAR filter Narrow bool `json:"narrow"` // NAR filter
VOX bool `json:"vox"` 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 // YaesuController is the typed escape the Manager exposes for the panel, in the
@@ -75,6 +79,9 @@ type YaesuController interface {
SetYaesuNRLevel(int) error SetYaesuNRLevel(int) error
SetYaesuNarrow(bool) error SetYaesuNarrow(bool) error
SetYaesuVOX(bool) error SetYaesuVOX(bool) error
SetYaesuKeySpeed(int) error
SetYaesuBreakIn(bool) error
YaesuZeroIn() error
SetYaesuSplit(bool) error SetYaesuSplit(bool) error
SetYaesuSplitOffset(int64) error SetYaesuSplitOffset(int64) error
SetYaesuBand(string) error SetYaesuBand(string) error
@@ -173,6 +180,15 @@ func (y *Yaesu) readPanelSettings() {
if v, ok := y.askNum("VX;", "VX", 1); ok { if v, ok := y.askNum("VX;", "VX", 1); ok {
y.panel.VOX = v != 0 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. // 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 y.panel.SplitTXHz = tx
return nil 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;")
}