From 10ef98496271da00ec7ff5093ae554f4db91ecca Mon Sep 17 00:00:00 2001 From: rouggy Date: Fri, 28 Aug 2026 23:56:49 +0200 Subject: [PATCH] feat(k3): align RIT/XIT and the power step with the other consoles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 'None of the consoles look alike' is the complaint ShiftRow exists to answer, and the Elecraft panel was still driving its offset with a row of nudge buttons. It now uses the shared control — ±, wheel, typed value — plus the same Ctrl+←/→ 10 Hz clarifier the Icom and TCI panels have. The K3 keeps ONE offset for RIT and XIT, so both rows show it, exactly like the Icom's. The backend gains the absolute setter the shared control speaks (SetKenwoodRITOffset, funnelled through the same RO write as the nudge), and the power slider moves in 1 W steps — nobody asks a K3 for 'between 10 and 15 watts' in fives. Also per review: the chase switches are labelled 'Chase POTA' / 'Chase SOTA', matching 'Chase new grids' beside them. --- app_k3.go | 5 +++ changelog.json | 6 ++- frontend/src/components/ElecraftPanel.tsx | 45 +++++++++++++++-------- frontend/src/lib/i18n.tsx | 4 +- frontend/wailsjs/go/main/App.d.ts | 2 + frontend/wailsjs/go/main/App.js | 4 ++ internal/cat/kenwood_panel.go | 11 ++++++ 7 files changed, 57 insertions(+), 20 deletions(-) diff --git a/app_k3.go b/app_k3.go index 5cb5a65..095cbae 100644 --- a/app_k3.go +++ b/app_k3.go @@ -30,6 +30,11 @@ func (a *App) SetKenwoodPanelMode(mode string) error { return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodPanelMode(mode) }) } +// SetKenwoodRITOffset sets the RIT/XIT offset to an absolute value in Hz. +func (a *App) SetKenwoodRITOffset(hz int) error { + return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodRITOffset(hz) }) +} + func (a *App) SetKenwoodPower(w int) error { return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodPower(w) }) } diff --git a/changelog.json b/changelog.json index 3056240..6202182 100644 --- a/changelog.json +++ b/changelog.json @@ -7,14 +7,16 @@ "Sending a spot while in split now pre-fills the RX frequency — where the station actually is — instead of the TX frequency, which spotted the pile-up five up from the DX.", "Cluster: NEW SLOT gets its own colour — the sky cyan the panadapter palette already uses for it, clearly apart from POTA green and the yellows. The NEW SLOT and NEW COUNTY filter chips now wear the same colours as the badges they select, and the NEW CALL filter works even when the slot-highlight display option is off.", "Elecraft console: a mode row — CW, USB, LSB, DATA and DATA RTTY. The two DATA buttons set the K3’s submode as well (DATA A for FT8/FT4, FSK D for RTTY): switching to DATA by mode alone kept whatever submode the last session left, which is how a K3 “in DATA” keys FT8 with no audio.", - "DX Cluster settings: “I chase POTA” and “I chase SOTA”, on by default. Unticked, the NEW POTA badge, colour and filter disappear — a new-band + new-POTA spot reads NEW BAND alone — and the reference columns stay empty." + "DX Cluster settings: “I chase POTA” and “I chase SOTA”, on by default. Unticked, the NEW POTA badge, colour and filter disappear — a new-band + new-POTA spot reads NEW BAND alone — and the reference columns stay empty.", + "Elecraft console: RIT and XIT use the same control as the Icom and TCI consoles — ± buttons, mouse wheel, typed value, Ctrl+←/→ — and the power slider moves in 1 W steps instead of 5." ], "fr": [ "TCI (SunSDR) : l'abonnement aux mesures est renouvelé quand la radio annonce qu'elle est prête — envoyé seulement à la connexion, il pouvait tomber pendant l'envoi initial de l'état et être ignoré, laissant la puissance et le ROS vides en émission. Le journal enregistre aussi l'abonnement et les premières trames de mesure, pour distinguer « la radio ne les envoie jamais » de « elles arrivaient et étaient perdues ».", "Envoyer un spot en split préremplit désormais la fréquence RX — là où la station se trouve réellement — au lieu de la fréquence TX, qui spottait le pile-up cinq au-dessus du DX.", "Cluster : NOUVEAU SLOT reçoit sa propre couleur — le cyan ciel que la palette du panadapter lui donne déjà, bien distinct du vert POTA et des jaunes. Les puces de filtre NOUVEAU SLOT et NOUVEAU COMTÉ portent désormais les couleurs des badges qu'elles sélectionnent, et le filtre NOUVEAU CALL fonctionne même quand l'option de surlignage par slot est désactivée.", "Console Elecraft : une rangée de modes — CW, USB, LSB, DATA et DATA RTTY. Les deux boutons DATA règlent aussi le sous-mode du K3 (DATA A pour FT8/FT4, FSK D pour le RTTY) : passer en DATA par le seul mode gardait le sous-mode de la session précédente, et un K3 « en DATA » manipulait le FT8 sans audio.", - "Réglages DX Cluster : « Je chasse le POTA » et « Je chasse le SOTA », cochés par défaut. Décochés, le badge, la couleur et le filtre NOUVEAU POTA disparaissent — un spot nouvelle bande + nouveau POTA affiche seulement NOUVELLE BANDE — et les colonnes de références restent vides." + "Réglages DX Cluster : « Je chasse le POTA » et « Je chasse le SOTA », cochés par défaut. Décochés, le badge, la couleur et le filtre NOUVEAU POTA disparaissent — un spot nouvelle bande + nouveau POTA affiche seulement NOUVELLE BANDE — et les colonnes de références restent vides.", + "Console Elecraft : le RIT et le XIT utilisent la même commande que les consoles Icom et TCI — boutons ±, molette, valeur tapée, Ctrl+←/→ — et le curseur de puissance avance par pas de 1 W au lieu de 5." ] }, { diff --git a/frontend/src/components/ElecraftPanel.tsx b/frontend/src/components/ElecraftPanel.tsx index a474016..0ec38ad 100644 --- a/frontend/src/components/ElecraftPanel.tsx +++ b/frontend/src/components/ElecraftPanel.tsx @@ -4,12 +4,13 @@ import { GetKenwoodState, RefreshKenwood, SetKenwoodPower, SetKenwoodAFGain, SetKenwoodTX, TuneKenwoodATU, SetKenwoodRFGain, SetKenwoodMicGain, SetKenwoodSquelch, SetKenwoodPreamp, SetKenwoodAtt, SetKenwoodNB, SetKenwoodNR, SetKenwoodAGC, SetKenwoodFilter, SetKenwoodAntenna, - SetKenwoodRIT, SetKenwoodXIT, ClearKenwoodRIT, SetKenwoodKeySpeed, ToggleKenwoodATU, NudgeKenwoodRIT, SetKenwoodPanelMode, + SetKenwoodRIT, SetKenwoodXIT, ClearKenwoodRIT, SetKenwoodKeySpeed, ToggleKenwoodATU, SetKenwoodRITOffset, SetKenwoodPanelMode, GetCATState, } from '../../wailsjs/go/main/App'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { sMeterRST } from '@/lib/rst'; +import { ShiftRow } from '@/components/ShiftRow'; import { MeterBar } from '@/components/MeterBar'; import { WheelRange } from '@/components/WheelRange'; @@ -102,6 +103,21 @@ function isDataMode(m?: string): boolean { export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) => void }) { const { t } = useI18n(); const [st, setSt] = useState(ZERO); + // Ctrl+Left/Right shifts the RIT by ±10 Hz while RIT is on — the same + // keyboard clarifier the Icom and TCI consoles have, for zero-beating a + // caller without touching the mouse. + const stRef = useRef(st); stRef.current = st; + useEffect(() => { + const onKey = (e: KeyboardEvent) => { + if (!e.ctrlKey || (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')) return; + const v = stRef.current; + if (!v.available || !v.rit) return; + e.preventDefault(); + SetKenwoodRITOffset((v.rit_offset || 0) + (e.key === 'ArrowRight' ? 10 : -10)).catch(() => {}); + }; + window.addEventListener('keydown', onKey); + return () => window.removeEventListener('keydown', onKey); + }, []); const [freqHz, setFreqHz] = useState(0); const [err, setErr] = useState(''); // Optimistic overlay: a slider must follow the finger, not the poll. Dropped @@ -248,7 +264,7 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
- {/* RIT / XIT. Clear zeroes both offsets at once — the radio's own RC, - and what an operator means by "clear it". */} + {/* RIT / XIT — the shared ShiftRow, exactly as the Icom and TCI consoles + drive theirs: ± / wheel / type, Ctrl+←/→ while RIT is on. The K3 has + ONE offset for both, so both rows show it, like the Icom's. */} +
+ SetKenwoodRIT(!view.rit).catch(setErrMsg)} + onSet={(hz) => SetKenwoodRITOffset(hz).catch(setErrMsg)} /> + SetKenwoodXIT(!view.xit).catch(setErrMsg)} + onSet={(hz) => SetKenwoodRITOffset(hz).catch(setErrMsg)} /> +
- SetKenwoodRIT(!view.rit).catch(setErrMsg)} /> - SetKenwoodXIT(!view.xit).catch(setErrMsg)} /> - {/* The offset itself. A lit RIT button says the feature is on and - nothing about where it has put the receiver. */} - {[-100, -10, 10, 100].map((d) => ( - 0 ? '+' : '') + d} on={false} off={off} - onClick={() => NudgeKenwoodRIT(d).catch(setErrMsg)} /> - ))} - - {view.rit_offset > 0 ? '+' : ''}{view.rit_offset || 0} Hz - - ClearKenwoodRIT().catch(setErrMsg)} /> {/* Antenna only when the radio answered AN — a K3 without the internal ATU has one socket and no switch to offer. */} {view.antenna > 0 && (<> diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx index 789ee87..7c4b494 100644 --- a/frontend/src/lib/i18n.tsx +++ b/frontend/src/lib/i18n.tsx @@ -333,7 +333,7 @@ const en: Dict = { 'gsc.scope': 'Match a square by', 'gsc.hunt': 'Chase', 'gsc.huntNew': 'New — never worked', 'gsc.huntUnconf': 'New and unconfirmed', 'gsc.scope_band_digi': 'This band + any digital mode', 'gsc.scope_band_mode': 'This band + this exact mode', 'gsc.scope_band_ftx': 'This band + any FT mode (FT8/FT4/FT2)', 'gsc.scope_mix_digi': 'Any band + any digital mode', 'gsc.scope_mix_mode': 'Any band + this exact mode', 'gsc.scope_mix_ftx': 'Any band + any FT mode (FT8/FT4/FT2)', 'gsc.hint': 'Decides when a square stops being NEW. Narrower means more squares to chase: per band and per exact mode is the most demanding, any band and any digital mode the least. Chasing unconfirmed as well keeps a square wanted until a QSL, LoTW or eQSL confirmation arrives — it is still missing from the award until then.', 'gsm.basemap': 'Basemap', 'gsm.title': 'Grid squares', 'gsm.all': 'All', 'gsm.phone': 'Phone', 'gsm.cw': 'CW', 'gsm.digital': 'Digital', 'gsm.ftx': 'FTx', 'gsm.confirmed': 'confirmed', 'gsm.worked': 'worked', 'gsm.colConfirmed': 'Colour for confirmed squares', 'gsm.colWorked': 'Colour for worked (unconfirmed) squares', 'gsm.colReset': 'Back to the theme colours', 'gsm.refresh': 'Recount from the log', 'gsm.count': '{n} squares · {c} confirmed', 'bo.nearKm': 'Count receivers within', 'bo.nearKmHint': 'A report proves YOUR path only if it was collected near you. Smaller is more local but leaves fewer receivers listening — too small and the watch has nothing to look at. 300 km borrows a whole region; 100 km suits 2 m, where a duct is narrow.', - 'bo.open': 'open', 'bo.liveTip': '{band} is open — {n} stations, ~{km} km, {sector}{season}. Click for the band map.', 'bo.enable': 'Watch for band openings', 'bo.enableHint': '(10, 12, 6, 4 and 2 m. Switching this on adds the two RBN nodes and subscribes to the PSK Reporter feed — the detection needs far more ears than a cluster can give it.)', 'bo.feedUp': 'PSK Reporter feed up — {n} decodes seen', 'bo.feedDown': 'PSK Reporter feed down — needs your station grid, and a moment to connect', 'clu.spotTtl': 'Spot lifetime', 'clu.spotTtlNever': 'Keep', 'clu.spotMax': 'Spots kept', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 keeps them.', 'clu.chasePota': 'I chase POTA', 'clu.chasePotaHint': 'Off: no NEW POTA badge or filter, and the POTA column stays empty — a new-band + new-POTA spot reads NEW BAND alone.', 'clu.chaseSota': 'I chase SOTA', 'clu.chaseSotaHint': 'Off: the SOTA column stays empty.', 'clu.chaseGrids': 'Chase new grids', 'clu.chaseGridsHint': '(learns locators from your own WSJT-X decodes AND from PSK Reporter, and keeps them in their own database so the cluster shows them from the first second)', 'clu.chaseGridsStat': '{n} locators known — {p} waiting to be written', 'clu.workedSameSlot': 'Already worked only on the same slot', + 'bo.open': 'open', 'bo.liveTip': '{band} is open — {n} stations, ~{km} km, {sector}{season}. Click for the band map.', 'bo.enable': 'Watch for band openings', 'bo.enableHint': '(10, 12, 6, 4 and 2 m. Switching this on adds the two RBN nodes and subscribes to the PSK Reporter feed — the detection needs far more ears than a cluster can give it.)', 'bo.feedUp': 'PSK Reporter feed up — {n} decodes seen', 'bo.feedDown': 'PSK Reporter feed down — needs your station grid, and a moment to connect', 'clu.spotTtl': 'Spot lifetime', 'clu.spotTtlNever': 'Keep', 'clu.spotMax': 'Spots kept', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 keeps them.', 'clu.chasePota': 'Chase POTA', 'clu.chasePotaHint': 'Off: no NEW POTA badge or filter, and the POTA column stays empty — a new-band + new-POTA spot reads NEW BAND alone.', 'clu.chaseSota': 'Chase SOTA', 'clu.chaseSotaHint': 'Off: the SOTA column stays empty.', 'clu.chaseGrids': 'Chase new grids', 'clu.chaseGridsHint': '(learns locators from your own WSJT-X decodes AND from PSK Reporter, and keeps them in their own database so the cluster shows them from the first second)', 'clu.chaseGridsStat': '{n} locators known — {p} waiting to be written', 'clu.workedSameSlot': 'Already worked only on the same slot', 'clu.macros': 'Command buttons', 'clu.macrosHint': 'A named button beside the cluster command box. Leave the command empty and the button is not shown.', 'clu.macroLabel': 'Button', 'clu.macroCmd': 'Command', 'clu.workedSameSlotHint': '— a spot shows "worked" only if you worked that call on the SAME band and mode, not just anywhere.', @@ -835,7 +835,7 @@ const fr: Dict = { 'bo.nearKm': 'Compter les récepteurs à moins de', 'bo.nearKmHint': 'Un report ne prouve TON chemin que s’il a été collecté près de chez toi. Plus petit est plus local, mais laisse moins de récepteurs à l’écoute — trop petit, la veille n’a plus rien à observer. 300 km emprunte les oreilles de toute une région ; 100 km convient au 2 m, où un conduit est étroit.', 'bo.open': 'ouvert', 'bo.liveTip': '{band} est ouvert — {n} stations, ~{km} km, {sector}{season}. Cliquer pour le bandmap.', 'bo.enable': 'Surveiller les ouvertures de bande', 'bo.enableHint': "(10, 12, 6, 4 et 2 m. Activer ajoute les deux nœuds RBN et souscrit au flux PSK Reporter — la détection a besoin de bien plus d oreilles qu un cluster ne peut en fournir.)", 'bo.feedUp': 'Flux PSK Reporter actif — {n} décodages vus', 'bo.feedDown': 'Flux PSK Reporter inactif — il faut ton locator, et un instant pour se connecter', 'clu.workedSameSlot': 'Déjà contacté seulement sur le même slot', 'clu.macros': 'Boutons de commande', 'clu.macrosHint': 'Un bouton nommé à côté du champ de commande du cluster. Laisse la commande vide et le bouton n’est pas affiché.', - 'clu.macroLabel': 'Bouton', 'clu.macroCmd': 'Commande', 'clu.spotTtl': 'Durée de vie des spots', 'clu.spotTtlNever': 'Garder', 'clu.spotMax': 'Nombre de spots conservés', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 les conserve.', 'clu.chasePota': 'Je chasse le POTA', 'clu.chasePotaHint': "Décoché : plus de badge ni de filtre NOUVEAU POTA, et la colonne POTA reste vide — un spot nouvelle bande + nouveau POTA affiche seulement NOUVELLE BANDE.", 'clu.chaseSota': 'Je chasse le SOTA', 'clu.chaseSotaHint': 'Décoché : la colonne SOTA reste vide.', 'clu.chaseGrids': 'Chasser les nouveaux locators', 'clu.chaseGridsHint': '(apprend les locators depuis TES propres décodages WSJT-X ET depuis PSK Reporter, et les garde dans leur propre base pour que le cluster les affiche dès la première seconde)', 'clu.chaseGridsStat': '{n} locators connus — {p} en attente d’écriture', + 'clu.macroLabel': 'Bouton', 'clu.macroCmd': 'Commande', 'clu.spotTtl': 'Durée de vie des spots', 'clu.spotTtlNever': 'Garder', 'clu.spotMax': 'Nombre de spots conservés', 'clu.spotMaxHint': '', 'clu.spotTtlHint': 'minutes — 0 les conserve.', 'clu.chasePota': 'Chasser le POTA', 'clu.chasePotaHint': "Décoché : plus de badge ni de filtre NOUVEAU POTA, et la colonne POTA reste vide — un spot nouvelle bande + nouveau POTA affiche seulement NOUVELLE BANDE.", 'clu.chaseSota': 'Chasser le SOTA', 'clu.chaseSotaHint': 'Décoché : la colonne SOTA reste vide.', 'clu.chaseGrids': 'Chasser les nouveaux locators', 'clu.chaseGridsHint': '(apprend les locators depuis TES propres décodages WSJT-X ET depuis PSK Reporter, et les garde dans leur propre base pour que le cluster les affiche dès la première seconde)', 'clu.chaseGridsStat': '{n} locators connus — {p} en attente d’écriture', 'clu.workedSameSlotHint': "— un spot n'est « contacté » que si cet indicatif a été fait sur la MÊME bande et le même mode, pas n'importe où.", diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts index 9c96c9e..54236f9 100644 --- a/frontend/wailsjs/go/main/App.d.ts +++ b/frontend/wailsjs/go/main/App.d.ts @@ -1155,6 +1155,8 @@ export function SetKenwoodRFGain(arg1:number):Promise; export function SetKenwoodRIT(arg1:boolean):Promise; +export function SetKenwoodRITOffset(arg1:number):Promise; + export function SetKenwoodSquelch(arg1:number):Promise; export function SetKenwoodTX(arg1:boolean):Promise; diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js index fd5fe33..dd95b75 100644 --- a/frontend/wailsjs/go/main/App.js +++ b/frontend/wailsjs/go/main/App.js @@ -2250,6 +2250,10 @@ export function SetKenwoodRIT(arg1) { return window['go']['main']['App']['SetKenwoodRIT'](arg1); } +export function SetKenwoodRITOffset(arg1) { + return window['go']['main']['App']['SetKenwoodRITOffset'](arg1); +} + export function SetKenwoodSquelch(arg1) { return window['go']['main']['App']['SetKenwoodSquelch'](arg1); } diff --git a/internal/cat/kenwood_panel.go b/internal/cat/kenwood_panel.go index 2f6655b..f1b6604 100644 --- a/internal/cat/kenwood_panel.go +++ b/internal/cat/kenwood_panel.go @@ -112,6 +112,7 @@ type KenwoodPanelController interface { SetKenwoodRIT(bool) error SetKenwoodXIT(bool) error NudgeKenwoodRIT(int) error + SetKenwoodRITOffset(int) error ClearKenwoodRIT() error SetKenwoodTX(bool) error TuneKenwoodATU() error @@ -578,6 +579,16 @@ func (k *Kenwood) SetKenwoodXIT(on bool) error { // ClearKenwoodRIT zeroes the RIT/XIT offset, both at once — which is what RC // does and what the operator means by "clear it". +// SetKenwoodRITOffset writes the offset as an ABSOLUTE value — what the shared +// ShiftRow control speaks. Nudge stays for the keyboard clarifier; both funnel +// into the same RO write. +func (k *Kenwood) SetKenwoodRITOffset(hz int) error { + k.mu.Lock() + cur := k.panel.RITOffset + k.mu.Unlock() + return k.NudgeKenwoodRIT(hz - cur) +} + func (k *Kenwood) ClearKenwoodRIT() error { return k.setPanel("RC;") }