From 9bc5a14c693515a9360fb392f5dc83b41c38d82c Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 29 Jul 2026 11:37:18 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20Yaesu=20panel=20=E2=80=94=20S=20units,?= =?UTF-8?q?=20sidebands,=20three-step=20ATT,=20readable=20sliders,=20own?= =?UTF-8?q?=20tab?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First look on the FTDX10 turned up six things: The S meter printed a raw percentage — "57" tells an operator nothing, and it is the S number that goes into a report. It now reads S1-S9/S9+dB, the same value the click-to-fill RST already used. CW, RTTY and the data modes exist on BOTH sidebands and the operator is the one who knows which they want. The buttons now carry the rig's actual sideband and a double-click flips it; PSK is added, riding the rig's DATA mode as it does on the radio itself. This also means the mode row drives the rig directly (MD0 with the exact mode) instead of going through the ADIF path, which could only pick a sideband by convention. The attenuator is a 6/12/18 dB pad on these rigs, not the single step I assumed — two thirds of the control was unreachable. Sliders had no visible filled side: --muted is barely lighter than the card it sits on, so the whole track read as one bar. They also came in three kinds (two bare range inputs among them). One component now, explicit track colour, and it takes a min/max so power in watts and DNR 1-15 look like the rest. The panel stretched across the whole window; it is a column of controls, so it is now capped and every row stays readable. And it gets its own Yaesu tab, like FlexRadio and Icom, rather than only being available as a Main-view pane. --- app.go | 7 ++ changelog.json | 4 +- frontend/src/App.tsx | 8 ++ frontend/src/components/YaesuPanel.tsx | 131 +++++++++++++++++-------- frontend/src/lib/i18n.tsx | 4 +- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 + frontend/wailsjs/go/models.ts | 2 + internal/cat/yaesu.go | 3 + internal/cat/yaesu_panel.go | 101 +++++++++++++++++-- 10 files changed, 214 insertions(+), 52 deletions(-) diff --git a/app.go b/app.go index 49854d5..5857640 100644 --- a/app.go +++ b/app.go @@ -14978,3 +14978,10 @@ func (a *App) yaesuDo(fn func(cat.YaesuController) error) error { } return a.cat.YaesuDo(fn) } + +// SetYaesuModeRaw picks an exact rig mode, sideband included ("CW-L", "DATA-U"). +// The panel's mode buttons use this rather than SetCATMode, which takes an ADIF +// mode and can only choose a sideband by convention. +func (a *App) SetYaesuModeRaw(mode string) error { + return a.yaesuDo(func(y cat.YaesuController) error { return y.SetYaesuModeRaw(mode) }) +} diff --git a/changelog.json b/changelog.json index b88786e..efe3544 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 pane gives S/PO/SWR meters, band and mode buttons, AF/RF/squelch, AGC, IPO-AMP1-AMP2, ATT, NB, DNR, narrow filter, power, mic gain, VOX, split and ATU tune. Enable it in Settings → Main view panes.", + "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 — double-click a button to switch it.", "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 volet console offre les mesures S/PO/ROS, les boutons de bande et de mode, AF/RF/squelch, AGC, IPO-AMP1-AMP2, ATT, NB, DNR, filtre étroit, puissance, gain micro, VOX, split et accord d'antenne. À activer dans Réglages → Volets de la vue principale.", + "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 — double-cliquez pour en changer.", "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 18909bb..1ecd43e 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -5373,6 +5373,7 @@ export default function App() { )} {catState.backend === 'flex' && FlexRadio} {catState.backend === 'icom' && Icom} + {catState.backend === 'yaesu' && Yaesu} {statsTabOpen && ( {t('stats.tab')} @@ -5788,6 +5789,13 @@ export default function App() { {/* Icom CI-V receive-DSP control panel — only when the CAT backend is an Icom. */} + {/* Yaesu CAT control panel — only when the CAT backend is a Yaesu. */} + {catState.backend === 'yaesu' && ( + + { setRstSent(r); rstUserEditedRef.current = true; }} /> + + )} + {catState.backend === 'icom' && ( { setRstSent(r); rstUserEditedRef.current = true; }} /> diff --git a/frontend/src/components/YaesuPanel.tsx b/frontend/src/components/YaesuPanel.tsx index 35d5f0c..85211d2 100644 --- a/frontend/src/components/YaesuPanel.tsx +++ b/frontend/src/components/YaesuPanel.tsx @@ -5,14 +5,14 @@ import { SetYaesuPower, SetYaesuMicGain, SetYaesuAFGain, SetYaesuRFGain, SetYaesuSquelch, SetYaesuAGC, SetYaesuPreamp, SetYaesuAtt, SetYaesuNB, SetYaesuNR, SetYaesuNRLevel, SetYaesuNarrow, SetYaesuVOX, SetYaesuSplit, SetYaesuBand, TuneYaesuATU, - GetCATState, SetCATMode, + SetYaesuModeRaw, GetCATState, } from '../../wailsjs/go/main/App'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { sMeterRST } from '@/lib/rst'; type YaesuState = { - available: boolean; model?: string; mode?: string; + available: boolean; model?: string; mode?: string; raw_mode?: string; transmitting: boolean; split: boolean; s_meter: number; power_meter: number; swr_meter: number; rf_power: number; mic_gain: number; af_gain: number; rf_gain: number; squelch: number; @@ -32,13 +32,48 @@ const ZERO: YaesuState = { // the radio's own band keys do. That is why these are band names, not Hz. const BANDS = ['160m', '80m', '40m', '30m', '20m', '17m', '15m', '12m', '10m', '6m']; -const MODES = ['SSB', 'CW', 'RTTY', 'FT8', 'AM', 'FM']; +// Mode buttons. CW, RTTY, DIGI and PSK exist on BOTH sidebands on a Yaesu and +// the operator is the one who knows which they want, so each carries its +// current sideband and DOUBLE-CLICK flips it. SSB picks its sideband from the +// frequency, as the band plan dictates, and AM/FM have none. +// +// PSK rides on the rig's DATA mode, like the other digital modes — the button +// exists because the operator thinks in modes, not in what the radio calls them. +type ModeBtn = { id: string; label: string; sideband: boolean; rig: (side: 'U' | 'L') => string }; +const MODES: ModeBtn[] = [ + { id: 'SSB', label: 'SSB', sideband: false, rig: () => 'SSB' }, + { id: 'CW', label: 'CW', sideband: true, rig: (s) => 'CW-' + s }, + { id: 'RTTY', label: 'RTTY', sideband: true, rig: (s) => 'RTTY-' + s }, + { id: 'DIGI', label: 'DIGI', sideband: true, rig: (s) => 'DATA-' + s }, + { id: 'PSK', label: 'PSK', sideband: true, rig: (s) => 'DATA-' + s }, + { id: 'AM', label: 'AM', sideband: false, rig: () => 'AM' }, + { id: 'FM', label: 'FM', sideband: false, rig: () => 'FM' }, +]; + +// Which button the rig's current raw mode belongs to, and on which sideband. +function activeMode(raw?: string): { id: string; side: 'U' | 'L' } | null { + switch ((raw || '').toUpperCase()) { + case 'USB': return { id: 'SSB', side: 'U' }; + case 'LSB': return { id: 'SSB', side: 'L' }; + case 'CW-U': return { id: 'CW', side: 'U' }; + case 'CW-L': return { id: 'CW', side: 'L' }; + case 'RTTY-U': return { id: 'RTTY', side: 'U' }; + case 'RTTY-L': return { id: 'RTTY', side: 'L' }; + case 'DATA-U': return { id: 'DIGI', side: 'U' }; + case 'DATA-L': return { id: 'DIGI', side: 'L' }; + case 'AM': return { id: 'AM', side: 'U' }; + case 'FM': return { id: 'FM', side: 'U' }; + } + return null; +} // The FTDX10/FTDX101 preamp is a three-way front-end selector, not an on/off: // IPO bypasses the preamp entirely (best on a quiet, high-signal band), AMP1 and // AMP2 add gain. Presenting it as a toggle would hide the middle position. const PREAMPS = [{ v: '0', l: 'IPO' }, { v: '1', l: 'AMP1' }, { v: '2', l: 'AMP2' }]; const AGCS = [{ v: 'FAST', l: 'FAST' }, { v: 'MID', l: 'MID' }, { v: 'SLOW', l: 'SLOW' }, { v: 'AUTO', l: 'AUTO' }]; +// The attenuator is a three-step pad on these rigs (6/12/18 dB), not a toggle. +const ATTS = [{ v: '0', l: 'OFF' }, { v: '6', l: '6dB' }, { v: '12', l: '12dB' }, { v: '18', l: '18dB' }]; function fmtVFO(hz?: number): string { if (!hz || hz <= 0) return '––.–––.––'; @@ -60,13 +95,6 @@ function bandOfHz(hz?: number): string { return ''; } -function modeMatches(btn: string, cur?: string): boolean { - if (!cur) return false; - if (btn === 'SSB') return cur === 'SSB' || cur === 'USB' || cur === 'LSB'; - if (btn === 'FT8') return cur === 'FT8' || cur === 'FT4' || cur === 'DATA'; - return btn === cur; -} - // Split the 0-100 S-meter reading into S units + dB over S9. // @@ -83,10 +111,11 @@ function sParts(v: number): { s: number; over: number; label: string } { return { s, over: 0, label: `S${s}` }; } -function Slider({ value, onChange, disabled, accent = '#2563eb' }: { - value: number; onChange: (v: number) => void; disabled?: boolean; accent?: string; +function Slider({ value, onChange, disabled, accent = 'var(--primary)', min = 0, max = 100 }: { + value: number; onChange: (v: number) => void; disabled?: boolean; accent?: string; min?: number; max?: number; }) { - const v = Math.max(0, Math.min(100, value)); + const v = Math.max(min, Math.min(max, value)); + const pct = max > min ? ((v - min) / (max - min)) * 100 : 0; const ref = useRef(null); // React's onWheel is passive, so preventDefault is ignored there — attach a // native non-passive listener, and read live values through refs so the @@ -94,13 +123,15 @@ function Slider({ value, onChange, disabled, accent = '#2563eb' }: { const valRef = useRef(value); valRef.current = value; const cbRef = useRef(onChange); cbRef.current = onChange; const disRef = useRef(disabled); disRef.current = disabled; + const minRef = useRef(min); minRef.current = min; + const maxRef = useRef(max); maxRef.current = max; useEffect(() => { const el = ref.current; if (!el) return; const onWheel = (e: WheelEvent) => { if (disRef.current) return; e.preventDefault(); - const nv = Math.max(0, Math.min(100, valRef.current + (e.deltaY < 0 ? 1 : -1))); + const nv = Math.max(minRef.current, Math.min(maxRef.current, valRef.current + (e.deltaY < 0 ? 1 : -1))); if (nv !== valRef.current) cbRef.current(nv); }; el.addEventListener('wheel', onWheel, { passive: false }); @@ -109,12 +140,19 @@ function Slider({ value, onChange, disabled, accent = '#2563eb' }: { return ( onChange(parseInt(e.target.value, 10))} - className={cn('flex-1 h-1.5 rounded-full appearance-none cursor-pointer disabled:opacity-30 disabled:cursor-default', - '[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:rounded-full', - '[&::-webkit-slider-thumb]:bg-card [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:shadow-sm')} - style={{ background: `linear-gradient(to right, ${accent} ${v}%, var(--muted))`, borderColor: accent }} + className={cn('flex-1 h-2 rounded-full appearance-none cursor-pointer disabled:opacity-40 disabled:cursor-default', + '[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:rounded-full', + '[&::-webkit-slider-thumb]:bg-background [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:shadow', + '[&::-webkit-slider-thumb]:cursor-grab [&::-webkit-slider-thumb]:active:cursor-grabbing')} + // The filled side was invisible against the dark theme: --muted is barely + // lighter than the card it sits on, so the whole track read as one bar. + // An explicit translucent track keeps both halves distinct in either theme. + style={{ + background: `linear-gradient(to right, ${accent} 0%, ${accent} ${pct}%, color-mix(in srgb, var(--foreground) 18%, transparent) ${pct}%, color-mix(in srgb, var(--foreground) 18%, transparent) 100%)`, + borderColor: accent, + }} /> ); } @@ -166,8 +204,8 @@ function Row({ label, children }: { label: string; children: React.ReactNode }) ); } -function Meter({ label, value, accent, onClick, title }: { - label: string; value: number; accent: string; onClick?: () => void; title?: string; +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 = ( @@ -176,7 +214,9 @@ function Meter({ label, value, accent, onClick, title }: {
- {v} + {/* 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) { @@ -244,7 +284,8 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi } return ( -
+
+
{/* VFO + status */}
@@ -268,7 +309,7 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi {/* Meters */} - { const sp = sParts(view.s_meter); onReportRST(sMeterRST(sp.s, sp.over, view.mode)); } : undefined} title={t('yaesu.sToRst')} /> @@ -288,14 +329,25 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi ))}
- {MODES.map((m) => ( - - ))} + {MODES.map((m) => { + const act = activeMode(view.raw_mode); + const on = act?.id === m.id; + // The sideband shown is the rig's when this mode is active, else the + // one the band plan implies — so a button says what pressing it will + // actually do rather than a stale letter. + const side: 'U' | 'L' = on && act ? act.side : (freqHz > 0 && freqHz < 10_000_000 ? 'L' : 'U'); + const flip: 'U' | 'L' = side === 'U' ? 'L' : 'U'; + return ( + + ); + })}
@@ -318,7 +370,9 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi push('preamp', parseInt(v, 10), () => SetYaesuPreamp(parseInt(v, 10)))} /> - 0} onClick={() => push('att', view.att > 0 ? 0 : 12, () => SetYaesuAtt(view.att > 0 ? 0 : 12))} label="ATT" /> + + + push('att', parseInt(v, 10), () => SetYaesuAtt(parseInt(v, 10)))} /> @@ -333,9 +387,8 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi {/* 1-15 on the rig, shown as-is rather than rescaled to a percentage: the radio's own display counts 1-15, and matching it is what makes the panel readable next to the front panel. */} - { const v = parseInt(e.target.value, 10); push('nr_level', v, () => SetYaesuNRLevel(v)); }} - className="flex-1 h-1.5 rounded-full appearance-none cursor-pointer disabled:opacity-30 accent-primary" /> + push('nr_level', v, () => SetYaesuNRLevel(v))} /> {view.nr_level || 1} @@ -345,9 +398,8 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi {/* Watts, not a percentage: the rig reports and takes watts, and a percentage would be a second unit to reconcile every time. */} - { const v = parseInt(e.target.value, 10); push('rf_power', v, () => SetYaesuPower(v)); }} - className="flex-1 h-1.5 rounded-full appearance-none cursor-pointer accent-primary" /> + push('rf_power', v, () => SetYaesuPower(v))} /> {view.rf_power}W @@ -362,6 +414,7 @@ export function YaesuPanel({ onReportRST }: { onReportRST?: (rst: string) => voi +
); } diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index d58bab3..d8ffb68 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', + '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, double-click to switch sideband (U/L)', '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é', + '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, double-cliquer pour changer de bande latérale (U/L)', '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 e93212c..ee168ce 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -925,6 +925,8 @@ export function SetYaesuBand(arg1:string):Promise; export function SetYaesuMicGain(arg1:number):Promise; +export function SetYaesuModeRaw(arg1:string):Promise; + export function SetYaesuNB(arg1:boolean):Promise; export function SetYaesuNR(arg1:boolean):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index fdc3153..19fccfa 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -1798,6 +1798,10 @@ export function SetYaesuMicGain(arg1) { return window['go']['main']['App']['SetYaesuMicGain'](arg1); } +export function SetYaesuModeRaw(arg1) { + return window['go']['main']['App']['SetYaesuModeRaw'](arg1); +} + export function SetYaesuNB(arg1) { return window['go']['main']['App']['SetYaesuNB'](arg1); } diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts index dc902e3..7ca3780 100644 --- a/frontend/wailsjs/go/models.ts +++ b/frontend/wailsjs/go/models.ts @@ -1078,6 +1078,7 @@ export namespace cat { available: boolean; model?: string; mode?: string; + raw_mode?: string; transmitting: boolean; split: boolean; s_meter: number; @@ -1106,6 +1107,7 @@ export namespace cat { this.available = source["available"]; this.model = source["model"]; this.mode = source["mode"]; + this.raw_mode = source["raw_mode"]; this.transmitting = source["transmitting"]; this.split = source["split"]; this.s_meter = source["s_meter"]; diff --git a/internal/cat/yaesu.go b/internal/cat/yaesu.go index f209b6c..7a794a1 100644 --- a/internal/cat/yaesu.go +++ b/internal/cat/yaesu.go @@ -212,6 +212,9 @@ func (y *Yaesu) ReadState() (RigState, error) { y.curFreq = s.FreqHz if r, err := y.ask("MD0;"); err == nil && len(r) >= 4 { + // Keep the RAW mode too: ADIF folds CW-U/CW-L and DATA-U/DATA-L together, + // but the panel has to show which sideband the rig is actually on. + y.panel.RawMode = yaesuRawModeName(r[3]) if m, ok := yaesuModeToADIF[r[3]]; ok { if m == "DATA" { m = y.digital diff --git a/internal/cat/yaesu_panel.go b/internal/cat/yaesu_panel.go index c41f4e8..1e3305a 100644 --- a/internal/cat/yaesu_panel.go +++ b/internal/cat/yaesu_panel.go @@ -28,6 +28,9 @@ type YaesuTXState struct { Available bool `json:"available"` Model string `json:"model,omitempty"` Mode string `json:"mode,omitempty"` + // RawMode is the rig mode with its sideband (CW-U, DATA-L…), which ADIF + // deliberately does not record but the panel has to show. + RawMode string `json:"raw_mode,omitempty"` Transmitting bool `json:"transmitting"` Split bool `json:"split"` @@ -70,6 +73,7 @@ type YaesuController interface { SetYaesuVOX(bool) error SetYaesuSplit(bool) error SetYaesuBand(string) error + SetYaesuModeRaw(string) error TuneYaesuATU() error } @@ -276,6 +280,46 @@ func (y *Yaesu) SetYaesuSplit(on bool) error { // SetYaesuBand switches band with BS, which lands the rig on ITS OWN last-used // frequency for that band — the radio's band memory, not a frequency we choose. +// SetYaesuModeRaw selects an exact rig mode, sideband included — "CW-U", +// "DATA-L", "RTTY-U"… The plain SetMode path takes an ADIF mode and can only +// choose a sideband by convention, but CW, RTTY and the data modes are routinely +// run on EITHER sideband and the operator is the one who knows which. This is +// what the panel's mode buttons use. +func (y *Yaesu) SetYaesuModeRaw(name string) error { + d, ok := yaesuRawModeDigit(name) + if !ok { + return fmt.Errorf("yaesu: unknown rig mode %q", name) + } + return y.setAndRefresh(fmt.Sprintf("MD0%c;", d)) +} + +// yaesuRawModeDigit maps a rig-mode name to its MD digit. +func yaesuRawModeDigit(name string) (byte, bool) { + switch strings.ToUpper(strings.TrimSpace(name)) { + case "LSB": + return '1', true + case "USB": + return '2', true + case "CW-U", "CWU": + return '3', true + case "CW-L", "CWL": + return '7', true + case "FM": + return '4', true + case "AM": + return '5', true + case "RTTY-L", "RTTYL": + return '6', true + case "RTTY-U", "RTTYU": + return '9', true + case "DATA-L", "DATAL", "DIGI-L": + return '8', true + case "DATA-U", "DATAU", "DIGI-U": + return 'C', true + } + return 0, false +} + func (y *Yaesu) SetYaesuBand(band string) error { code, ok := yaesuBandCode(band) if !ok { @@ -357,21 +401,32 @@ func yaesuAGCCode(name string) int { return 4 // AUTO — the safe default for anything unrecognised } -// The FTDX10's attenuator is a single 12 dB step (RA00/RA01). Reporting the dB -// rather than the raw code lets the panel label the button honestly, and leaves -// room for models with more steps. +// The FTDX10/FTDX101 attenuator is a THREE-step pad (RA00..RA03 = off, 6, 12, +// 18 dB), not the single step this first assumed — a panel offering only one +// step hides two thirds of the control. Reporting the dB rather than the raw +// code lets the buttons label themselves honestly. func yaesuAttDB(code int) int { - if code == 0 { - return 0 + switch code { + case 1: + return 6 + case 2: + return 12 + case 3: + return 18 } - return 12 + return 0 } func yaesuAttCode(db int) int { - if db <= 0 { - return 0 + switch { + case db >= 18: + return 3 + case db >= 12: + return 2 + case db >= 6: + return 1 } - return 1 + return 0 } // yaesuBandCode maps an ADIF band to the BS command's band number. @@ -402,3 +457,31 @@ func yaesuBandCode(band string) (int, bool) { } return 0, false } + +// yaesuRawModeName is the inverse of yaesuRawModeDigit: what the rig is on, +// sideband included, for the panel's mode buttons to highlight. +func yaesuRawModeName(d byte) string { + switch d { + case '1': + return "LSB" + case '2': + return "USB" + case '3': + return "CW-U" + case '4': + return "FM" + case '5': + return "AM" + case '6': + return "RTTY-L" + case '7': + return "CW-L" + case '8': + return "DATA-L" + case '9': + return "RTTY-U" + case 'C': + return "DATA-U" + } + return "" +}