diff --git a/app_k3.go b/app_k3.go
index 69606ff..603794b 100644
--- a/app_k3.go
+++ b/app_k3.go
@@ -32,6 +32,60 @@ func (a *App) SetKenwoodAFGain(p int) error {
return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodAFGain(p) })
}
+func (a *App) SetKenwoodRFGain(p int) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodRFGain(p) })
+}
+
+func (a *App) SetKenwoodMicGain(p int) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodMicGain(p) })
+}
+
+func (a *App) SetKenwoodSquelch(p int) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodSquelch(p) })
+}
+
+func (a *App) SetKenwoodPreamp(on bool) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodPreamp(on) })
+}
+
+func (a *App) SetKenwoodAtt(on bool) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodAtt(on) })
+}
+
+func (a *App) SetKenwoodNB(on bool) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodNB(on) })
+}
+
+func (a *App) SetKenwoodNR(on bool) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodNR(on) })
+}
+
+func (a *App) SetKenwoodAGC(name string) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodAGC(name) })
+}
+
+func (a *App) SetKenwoodFilter(hz int) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodFilter(hz) })
+}
+
+func (a *App) SetKenwoodAntenna(n int) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodAntenna(n) })
+}
+
+func (a *App) SetKenwoodRIT(on bool) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodRIT(on) })
+}
+
+func (a *App) SetKenwoodXIT(on bool) error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodXIT(on) })
+}
+
+// ClearKenwoodRIT zeroes the RIT and XIT offsets together, which is what the
+// radio's own RC does and what an operator means by "clear it".
+func (a *App) ClearKenwoodRIT() error {
+ return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.ClearKenwoodRIT() })
+}
+
// SetKenwoodTX is the panel's MOX.
func (a *App) SetKenwoodTX(on bool) error {
return a.kenwoodPanelDo(func(k cat.KenwoodPanelController) error { return k.SetKenwoodTX(on) })
diff --git a/changelog.json b/changelog.json
index b3ec8dc..218fdde 100644
--- a/changelog.json
+++ b/changelog.json
@@ -6,13 +6,13 @@
"Withdrawing deleted QSOs from Club Log is now paced and capped at 25 per deletion. Their delete endpoint is a real-time one, meant for an operator removing a contact they just mis-logged; Club Log watches the rate and blocks the IP of anything that batches through it. Past the cap OpsLog stops and says so — there is no bulk-delete API, and hundreds of removals belong on clublog.org, which has a tool for it.",
"Auto-call, withdrawn earlier, is now disarmed where it was remembered: a stored 'enabled' is switched off and written back the first time OpsLog reads it. The running guard already stopped this build from calling anyone, but the stored flag survived — and any build without that guard would key the transmitter for a feature with no switch left to turn it off.",
"When TQSL refuses an upload, the log now carries the exact ADIF record it was given and the station location it was told to sign with. 'No QSOs processed' covers several unrelated causes and the temp file is deleted the moment TQSL returns, so the one piece of evidence that mattered was the one nobody could see.",
- "An Elecraft console for the K3/K4: power, volume, S-meter, SWR, MOX and an ATU tune, in a tab of its own when the CAT backend is Elecraft or Kenwood. The meter scaling is marked provisional and the raw readings go to the log — it was written from the K3 Programmer's Reference without a radio to hand, and a wrongly scaled SWR bar reports a good match on a bad antenna."
+ "An Elecraft console for the K3/K4, in a tab of its own when the CAT backend is Elecraft or Kenwood: power, volume, RF and mic gain, squelch, preamp, attenuator, NB, NR, AGC, filter width, antenna, RIT/XIT with a clear, keyer speed, S-meter, SWR, MOX and an ATU tune. The meter scaling is marked provisional and the raw readings go to the log — it was written from the K3 Programmer's Reference without a radio to hand, and a wrongly scaled SWR bar reports a good match on a bad antenna."
],
"fr": [
"Le retrait des QSO supprimés chez Club Log est désormais cadencé et limité à 25 par suppression. Leur point d'entrée de suppression est temps réel, prévu pour un opérateur qui retire un contact qu'il vient de mal enregistrer ; Club Log surveille le rythme et bloque l'IP de ce qui passe des lots par là. Au-delà de la limite, OpsLog s'arrête et le dit — il n'existe pas d'API de suppression en masse, et des centaines de retraits se font sur clublog.org, qui a l'outil pour ça.",
"L'appel automatique, retiré précédemment, est maintenant désarmé là où il était mémorisé : un « activé » enregistré est éteint et réécrit dès la première lecture par OpsLog. Le garde-fou à l'exécution empêchait déjà cette version d'appeler qui que ce soit, mais l'indicateur enregistré survivait — et toute version sans ce garde-fou passait à l'émission pour une fonction dont il ne reste aucun interrupteur.",
"Quand TQSL refuse un envoi, le journal contient désormais l'enregistrement ADIF exact qui lui a été remis et l'emplacement de station demandé pour la signature. « No QSOs processed » recouvre plusieurs causes sans rapport et le fichier temporaire est supprimé dès que TQSL rend la main : la seule pièce à conviction utile était justement invisible.",
- "Une console Elecraft pour les K3/K4 : puissance, volume, S-mètre, ROS, MOX et accord de l'ATU, dans un onglet dédié quand le CAT est réglé sur Elecraft ou Kenwood. L'échelle des mesures est signalée comme provisoire et les valeurs brutes partent dans le journal — la console a été écrite d'après le manuel de programmation du K3, sans radio sous la main, et une barre de ROS mal calibrée annonce un bon accord sur une mauvaise antenne."
+ "Une console Elecraft pour les K3/K4, dans un onglet dédié quand le CAT est réglé sur Elecraft ou Kenwood : puissance, volume, gain HF et micro, squelch, préampli, atténuateur, NB, NR, AGC, largeur de filtre, antenne, RIT/XIT avec effacement, vitesse du manipulateur, S-mètre, ROS, MOX et accord de l'ATU. L'échelle des mesures est signalée comme provisoire et les valeurs brutes partent dans le journal — la console a été écrite d'après le manuel de programmation du K3, sans radio sous la main, et une barre de ROS mal calibrée annonce un bon accord sur une mauvaise antenne."
]
},
{
diff --git a/frontend/src/components/ElecraftPanel.tsx b/frontend/src/components/ElecraftPanel.tsx
index 6f64ed4..60c6a26 100644
--- a/frontend/src/components/ElecraftPanel.tsx
+++ b/frontend/src/components/ElecraftPanel.tsx
@@ -2,6 +2,9 @@ import { useEffect, useRef, useState } from 'react';
import { Radio, Power, Activity, AudioLines, SlidersHorizontal } from 'lucide-react';
import {
GetKenwoodState, RefreshKenwood, SetKenwoodPower, SetKenwoodAFGain, SetKenwoodTX, TuneKenwoodATU,
+ SetKenwoodRFGain, SetKenwoodMicGain, SetKenwoodSquelch, SetKenwoodPreamp, SetKenwoodAtt,
+ SetKenwoodNB, SetKenwoodNR, SetKenwoodAGC, SetKenwoodFilter, SetKenwoodAntenna,
+ SetKenwoodRIT, SetKenwoodXIT, ClearKenwoodRIT, SetKenwoodKeySpeed,
GetCATState,
} from '../../wailsjs/go/main/App';
import { cn } from '@/lib/utils';
@@ -14,16 +17,26 @@ type KenwoodState = {
transmitting: boolean; split: boolean; split_tx_hz?: number;
s_meter: number; s_meter_raw: number;
power_meter: number; swr: number; swr_raw: number;
- rf_power: number; af_gain: number;
+ rf_power: number; af_gain: number; rf_gain: number; mic_gain: number; squelch: number;
+ preamp: boolean; att: boolean; nb: boolean; nr: boolean; agc?: string;
+ filter_hz: number; antenna: number; rit: boolean; xit: boolean; key_speed: number;
meters_provisional: boolean;
};
const ZERO: KenwoodState = {
available: false, elecraft: false, transmitting: false, split: false,
s_meter: 0, s_meter_raw: 0, power_meter: 0, swr: 0, swr_raw: 0,
- rf_power: 0, af_gain: 0, meters_provisional: true,
+ rf_power: 0, af_gain: 0, rf_gain: 0, mic_gain: 0, squelch: 0,
+ preamp: false, att: false, nb: false, nr: false,
+ filter_hz: 0, antenna: 0, rit: false, xit: false, key_speed: 0,
+ meters_provisional: true,
};
+// Filter widths worth a button. CW work happens at 200-500 Hz, SSB at 2.4-2.8
+// kHz; the rest of the range is reachable from the radio's own knob, and a
+// panel offering thirty widths is slower to use than the knob it replaces.
+const FILTERS = [200, 400, 700, 1000, 1800, 2400, 2800];
+
// Raw S-meter → S units. The K3 answers 0-21 across S0…S9+60; S9 is taken at
// raw 9 and each step above it as 6 dB. PROVISIONAL, like the rest of the
// scaling: the raw value is on screen and in the log, so a real radio settles
@@ -47,6 +60,18 @@ function sSegColor(frac: number) {
return '#16a34a';
}
+// One button shape for every on/off control, so the panel reads as one
+// instrument instead of a collection of differently-styled switches.
+function Toggle({ label, on, off, onClick }: { label: string; on: boolean; off: boolean; onClick: () => void }) {
+ return (
+
+ );
+}
+
export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) => void }) {
const { t } = useI18n();
const [st, setSt] = useState(ZERO);
@@ -54,7 +79,7 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
const [err, setErr] = useState('');
// Optimistic overlay: a slider must follow the finger, not the poll. Dropped
// once the radio has had time to answer with the value it actually took.
- const [local, setLocal] = useState<{ rf_power?: number; af_gain?: number }>({});
+ const [local, setLocal] = useState<{ rf_power?: number; af_gain?: number; rf_gain?: number; mic_gain?: number; squelch?: number; key_speed?: number }>({});
const localAtRef = useRef(0);
useEffect(() => {
@@ -80,6 +105,8 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
const view = { ...st, ...local };
const off = !st.available;
+ const setErrMsg = (e: any) => setErr(String(e?.message ?? e));
+
const put = (patch: typeof local, run: () => Promise) => {
setLocal((p) => ({ ...p, ...patch }));
localAtRef.current = Date.now();
@@ -169,6 +196,86 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
className="flex-1 accent-primary" />
{view.af_gain ?? 0}
+
+
+
+
+ {/* Receive chain. Toggles, because that is what they are on the radio:
+ one press each, and the state comes back from the rig rather than
+ from what the button was asked to do. */}
+
+
+ {/* RIT / XIT. Clear zeroes both offsets at once — the radio's own RC,
+ and what an operator means by "clear it". */}
+
+ SetKenwoodRIT(!view.rit).catch(setErrMsg)} />
+ SetKenwoodXIT(!view.xit).catch(setErrMsg)} />
+ 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 && (<>
+
+ {t('k3.antenna')}
+ {[1, 2].map((n) => (
+ SetKenwoodAntenna(n).catch(setErrMsg)} />
+ ))}
+ >)}
+
+
+ {/* CW keyer speed — the radio's own keyer, the one the K3 sends with. */}
+
);
diff --git a/frontend/src/lib/i18n.tsx b/frontend/src/lib/i18n.tsx
index aeeea86..be71159 100644
--- a/frontend/src/lib/i18n.tsx
+++ b/frontend/src/lib/i18n.tsx
@@ -426,7 +426,7 @@ const en: Dict = {
'tgp.online': 'online', 'tgp.offline': 'offline', 'tgp.close': 'Close', 'tgp.connecting': 'Connecting…', 'tgp.swr': 'SWR', 'tgp.power': 'Fwd power', 'tgp.tune': 'Tune', 'tgp.tuning': 'Tuning…', 'tgp.tuneHint': 'Start an automatic tuning cycle on the active channel — key the rig into a carrier so the tuner can measure SWR', 'tgp.bypass': 'Bypass', 'tgp.bypassHint': 'Toggle global bypass — route the antenna straight through, tuner out of line', 'tgp.operate': 'Operate', 'tgp.standby': 'Standby', 'tgp.operateHint': 'Toggle Operate / Standby',
'tgp.title': 'Tuner Genius', 'tgp.chActive': 'Channel {letter} — active', 'tgp.chSelect': 'Make channel {letter} active', 'tgp.chActiveTag': 'active', 'tgp.ant': 'Ant', 'tgp.bypassed': 'Bypassed', 'tgp.inLine': 'In line',
'flxp.ritHint': 'RIT — shifts your RECEIVE frequency only. Wheel, ± or arrow keys to adjust (Ctrl = 100 Hz). The offset is kept when you switch it off.', 'flxp.xitHint': 'XIT — shifts your TRANSMIT frequency only. Wheel, ± or arrow keys to adjust (Ctrl = 100 Hz). The offset is kept when you switch it off.',
- 'flxp.smartsdrRemote': 'SmartSDR remote control', 'flxp.offline': 'OFFLINE', 'flxp.waiting': 'Waiting for the FlexRadio… (set CAT to FlexRadio and connect)', 'flxp.transmit': 'Transmit', 'flxp.rfPower': 'RF Power', 'flxp.tunePwr': 'Tune Pwr', 'flxp.rstChaseHint': 'Chase the pile-up: when the CW skimmer marks a report ({m}) on the panadapter, move the TRANSMIT slice there — that is where the DX was listening a second ago. The receive slice never moves. Right-click to change the marker text.', 'flxp.rstChaseMarkerHint': 'The text the skimmer writes for a report — whatever SDC is set to send (599, 5NN…). Several can be given, separated by commas; add the old-report marker to chase those too.', 'flxp.rstChaseOffset': 'off', 'k3.console': 'Elecraft Console', 'k3.waiting': 'Waiting for the radio… (set CAT to Elecraft or Kenwood and connect)', 'k3.power': 'Power', 'k3.volume': 'Volume', 'k3.refreshHint': 'Re-read the settings from the radio — for when a knob was turned on the front panel.', 'k3.sMeterHint': 'Click to use this reading as the report sent. Raw value from the rig: {raw}.', 'k3.tuneHint': 'Start an ATU tuning cycle (K3: a tap of the ATU TUNE button). The exact command sent is written to the log.', 'k3.provisional': 'Meter scaling is provisional: it has not yet been confirmed against a real K3, and the raw readings are written to the log so it can be.', 'flxp.splitHint': 'Split: RX/TX on separate slices. ON creates a TX slice +1 kHz (CW) / +5 kHz (SSB) up, like SmartSDR.', 'flxp.sliceHint': 'Click to make this the active slice — frequency, mode, DSP and spot-clicks all follow it.', 'flxp.txSlice': 'This slice transmits', 'flxp.setTxSlice': 'Move TX to this slice (transmit here)', 'flxp.voxDly': 'VOX Dly', 'flxp.speed': 'Speed', 'flxp.pitch': 'Pitch', 'flxp.delay': 'Delay',
+ 'flxp.smartsdrRemote': 'SmartSDR remote control', 'flxp.offline': 'OFFLINE', 'flxp.waiting': 'Waiting for the FlexRadio… (set CAT to FlexRadio and connect)', 'flxp.transmit': 'Transmit', 'flxp.rfPower': 'RF Power', 'flxp.tunePwr': 'Tune Pwr', 'flxp.rstChaseHint': 'Chase the pile-up: when the CW skimmer marks a report ({m}) on the panadapter, move the TRANSMIT slice there — that is where the DX was listening a second ago. The receive slice never moves. Right-click to change the marker text.', 'flxp.rstChaseMarkerHint': 'The text the skimmer writes for a report — whatever SDC is set to send (599, 5NN…). Several can be given, separated by commas; add the old-report marker to chase those too.', 'flxp.rstChaseOffset': 'off', 'k3.console': 'Elecraft Console', 'k3.waiting': 'Waiting for the radio… (set CAT to Elecraft or Kenwood and connect)', 'k3.rfGain': 'RF gain', 'k3.micGain': 'Mic', 'k3.squelch': 'Squelch', 'k3.filter': 'Filter', 'k3.antenna': 'Antenna', 'k3.clear': 'CLEAR', 'k3.keySpeed': 'Keyer', 'k3.power': 'Power', 'k3.volume': 'Volume', 'k3.refreshHint': 'Re-read the settings from the radio — for when a knob was turned on the front panel.', 'k3.sMeterHint': 'Click to use this reading as the report sent. Raw value from the rig: {raw}.', 'k3.tuneHint': 'Start an ATU tuning cycle (K3: a tap of the ATU TUNE button). The exact command sent is written to the log.', 'k3.provisional': 'Meter scaling is provisional: it has not yet been confirmed against a real K3, and the raw readings are written to the log so it can be.', 'flxp.splitHint': 'Split: RX/TX on separate slices. ON creates a TX slice +1 kHz (CW) / +5 kHz (SSB) up, like SmartSDR.', 'flxp.sliceHint': 'Click to make this the active slice — frequency, mode, DSP and spot-clicks all follow it.', 'flxp.txSlice': 'This slice transmits', 'flxp.setTxSlice': 'Move TX to this slice (transmit here)', 'flxp.voxDly': 'VOX Dly', 'flxp.speed': 'Speed', 'flxp.pitch': 'Pitch', 'flxp.delay': 'Delay',
'flxp.receiveActive': 'Receive (active slice)', 'flxp.muted': 'Muted — click to unmute', 'flxp.mute': 'Mute RX audio', 'flxp.filter': 'Filter', 'flxp.amplifier': 'Amplifier', 'flxp.atuTune': 'TUNE', 'flxp.atuTuneHint': 'Start a tuning cycle on the built-in ATU. The radio keys a carrier itself to measure the match.', 'flxp.atuBypass': 'BYPASS', 'flxp.atuBypassHint': 'Take the ATU out of line (straight through).', 'flxp.atuMem': 'MEM', 'flxp.atuMemHint': 'Reuse the stored tuning solution for this frequency instead of tuning again.', 'flxp.atuIdle': 'not tuned', 'flxp.atuTuning': 'tuning…', 'flxp.atuOk': 'tuned', 'flxp.atuFail': 'TUNE FAILED', 'flxp.atuBypassed': 'bypassed', 'flxp.atuAborted': 'aborted', 'flxp.ampInLine': 'Amplifier is in line (transmitting through PA).', 'flxp.ampBypassed': 'Amplifier bypassed (standby).', 'flxp.pgConnected': 'PowerGenius connected', 'flxp.pgOffline': 'PowerGenius offline', 'flxp.fan': 'Fan', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Broadcast', 'flxp.fault': 'FAULT', 'flxp.meters': 'Meters', 'flxp.voltage': 'VOLTAGE', 'flxp.paTemp': 'PA TEMP', 'flxp.txFilter': 'TX filter', 'flxp.micProfile': 'Mic profile', 'flxp.noMeters': "No meters yet — waiting for the radio's UDP stream…", 'flxp.amplifierHdr': 'AMPLIFIER', 'flxp.outputPower': 'OUTPUT POWER', 'flxp.speOffline': 'SPE offline', 'flxp.acomOffline': 'ACOM offline', 'flxp.ampPick': 'Choose which amplifier this card shows', 'flxp.dspV4Hint': 'SmartSDR v4 DSP (8000/Aurora series)', 'flxp.daxHint': 'DAX as the transmit audio source (SmartSDR transmit-bar DAX button) — for WSJT-X & co', 'flxp.rnnHint': 'RNN — AI noise reduction (on/off)', 'flxp.anftHint': 'ANFT — FFT-based automatic notch filter (on/off)', 'flxp.dspNoise': 'Noise', 'flxp.dspMore': 'Show/hide advanced DSP (WNB, v4 NR/notch)',
'icmp.spectrum': 'Spectrum', 'icmp.scopeFixed': 'Fixed — double-click / wheel to tune', 'icmp.scopeCenter': 'Center — follows VFO', 'icmp.scopeOff': 'Scope off', 'icmp.scopePanDown': 'Shift scope −50 kHz', 'icmp.scopePanUp': 'Shift scope +50 kHz', 'icmp.scopeCenterVfo': 'Center scope on the current frequency (±50 kHz)', 'icmp.notConnected': "Icom not connected. Enable the Icom CI-V backend in Settings → CAT and connect the radio's USB port.", 'icmp.refresh': 'Refresh', 'icmp.meters': 'Meters', 'icmp.transmit': 'Transmit', 'icmp.power': 'Power', 'icmp.mic': 'Mic', 'icmp.receive': 'Receive', 'icmp.preamp': 'Preamp', 'icmp.filter': 'Filter', 'icmp.noiseNotch': 'Noise / Notch', 'icmp.autoNotch': 'Auto notch filter', 'icmp.apf': 'Audio peak filter (CW)', 'icmp.clarifiers': 'RIT / ΔTX', 'icmp.ritHint': 'Wheel or ± to shift · Ctrl+←/→ shifts RIT when active', 'icmp.bandsAntenna': 'Bands & Antenna', 'icmp.bandCurrent': 'The rig is on {b} m', 'icmp.antenna': 'Antenna', 'icmp.passband': 'Passband / Notch', 'icmp.pbtCenter': 'Center PBT', 'icmp.manualNotch': 'Manual notch — MN on, then set position', 'icmp.squelch': 'Squelch', 'icmp.powerOnHint': 'Power the radio ON (boots ~15 s)', 'icmp.powerOffHint': 'Power the radio OFF', 'icmp.powerOffConfirm': 'Switch the radio OFF?',
'rst.clickToFill': 'Click to set RST tx from the signal',
@@ -898,7 +898,7 @@ const fr: Dict = {
'tgp.online': 'en ligne', 'tgp.offline': 'hors ligne', 'tgp.close': 'Fermer', 'tgp.connecting': 'Connexion…', 'tgp.swr': 'ROS', 'tgp.power': 'Puiss. directe', 'tgp.tune': 'Accord', 'tgp.tuning': 'Accord…', 'tgp.tuneHint': "Lancer un cycle d'accord automatique sur le canal actif — passe la radio en porteuse pour que le coupleur mesure le ROS", 'tgp.bypass': 'Bypass', 'tgp.bypassHint': "Basculer le bypass global — antenne en direct, coupleur hors ligne", 'tgp.operate': 'Operate', 'tgp.standby': 'Standby', 'tgp.operateHint': 'Basculer Operate / Standby',
'tgp.title': 'Tuner Genius', 'tgp.chActive': 'Canal {letter} — actif', 'tgp.chSelect': 'Activer le canal {letter}', 'tgp.chActiveTag': 'actif', 'tgp.ant': 'Ant', 'tgp.bypassed': 'Bypass', 'tgp.inLine': 'En ligne',
'flxp.ritHint': "RIT — décale uniquement ta fréquence de RÉCEPTION. Molette, ± ou flèches pour ajuster (Ctrl = 100 Hz). Le décalage est conservé quand tu l'éteins.", 'flxp.xitHint': "XIT — décale uniquement ta fréquence d'ÉMISSION. Molette, ± ou flèches pour ajuster (Ctrl = 100 Hz). Le décalage est conservé quand tu l'éteins.",
- 'flxp.smartsdrRemote': 'Contrôle à distance SmartSDR', 'flxp.offline': 'HORS LIGNE', 'flxp.waiting': 'En attente du FlexRadio… (règle le CAT sur FlexRadio et connecte)', 'flxp.transmit': 'Émission', 'flxp.rfPower': 'Puissance RF', 'flxp.tunePwr': 'Puiss. TUNE', 'flxp.rstChaseHint': "Chasser le pile-up : quand le skimmer CW marque un report ({m}) sur le panadapter, déplacer la slice d'ÉMISSION dessus — c'est là que le DX écoutait il y a une seconde. La slice de réception ne bouge jamais. Clic droit pour changer le texte du marqueur.", 'flxp.rstChaseMarkerHint': "Le texte que le skimmer écrit pour un report — ce que SDC est réglé à envoyer (599, 5NN…). On peut en mettre plusieurs, séparés par des virgules ; ajoute le marqueur des reports anciens pour les chasser aussi.", 'flxp.rstChaseOffset': 'off', 'k3.console': 'Console Elecraft', 'k3.waiting': 'En attente de la radio… (règle le CAT sur Elecraft ou Kenwood et connecte)', 'k3.power': 'Puissance', 'k3.volume': 'Volume', 'k3.refreshHint': "Relire les réglages depuis la radio — quand un bouton a été tourné en façade.", 'k3.sMeterHint': 'Cliquer pour utiliser cette lecture comme report envoyé. Valeur brute de la radio : {raw}.', 'k3.tuneHint': "Lancer un cycle d'accord de l'ATU (K3 : appui sur la touche ATU TUNE). La commande exacte envoyée est écrite dans le journal.", 'k3.provisional': "L'échelle des mesures est provisoire : elle n'a pas encore été confirmée sur un vrai K3, et les valeurs brutes sont écrites dans le journal pour qu'elle puisse l'être.", 'flxp.splitHint': 'Split : RX/TX sur des slices séparées. ON crée une slice TX +1 kHz (CW) / +5 kHz (SSB) au-dessus, comme SmartSDR.', 'flxp.sliceHint': 'Cliquer pour rendre cette slice active — fréquence, mode, DSP et clics de spot la suivent tous.', 'flxp.txSlice': 'Cette slice émet', 'flxp.setTxSlice': 'Déplacer le TX sur cette slice (émettre ici)', 'flxp.voxDly': 'Délai VOX', 'flxp.speed': 'Vitesse', 'flxp.pitch': 'Tonalité', 'flxp.delay': 'Délai',
+ 'flxp.smartsdrRemote': 'Contrôle à distance SmartSDR', 'flxp.offline': 'HORS LIGNE', 'flxp.waiting': 'En attente du FlexRadio… (règle le CAT sur FlexRadio et connecte)', 'flxp.transmit': 'Émission', 'flxp.rfPower': 'Puissance RF', 'flxp.tunePwr': 'Puiss. TUNE', 'flxp.rstChaseHint': "Chasser le pile-up : quand le skimmer CW marque un report ({m}) sur le panadapter, déplacer la slice d'ÉMISSION dessus — c'est là que le DX écoutait il y a une seconde. La slice de réception ne bouge jamais. Clic droit pour changer le texte du marqueur.", 'flxp.rstChaseMarkerHint': "Le texte que le skimmer écrit pour un report — ce que SDC est réglé à envoyer (599, 5NN…). On peut en mettre plusieurs, séparés par des virgules ; ajoute le marqueur des reports anciens pour les chasser aussi.", 'flxp.rstChaseOffset': 'off', 'k3.console': 'Console Elecraft', 'k3.waiting': 'En attente de la radio… (règle le CAT sur Elecraft ou Kenwood et connecte)', 'k3.rfGain': 'Gain HF', 'k3.micGain': 'Micro', 'k3.squelch': 'Squelch', 'k3.filter': 'Filtre', 'k3.antenna': 'Antenne', 'k3.clear': 'EFFACER', 'k3.keySpeed': 'Manip', 'k3.power': 'Puissance', 'k3.volume': 'Volume', 'k3.refreshHint': "Relire les réglages depuis la radio — quand un bouton a été tourné en façade.", 'k3.sMeterHint': 'Cliquer pour utiliser cette lecture comme report envoyé. Valeur brute de la radio : {raw}.', 'k3.tuneHint': "Lancer un cycle d'accord de l'ATU (K3 : appui sur la touche ATU TUNE). La commande exacte envoyée est écrite dans le journal.", 'k3.provisional': "L'échelle des mesures est provisoire : elle n'a pas encore été confirmée sur un vrai K3, et les valeurs brutes sont écrites dans le journal pour qu'elle puisse l'être.", 'flxp.splitHint': 'Split : RX/TX sur des slices séparées. ON crée une slice TX +1 kHz (CW) / +5 kHz (SSB) au-dessus, comme SmartSDR.', 'flxp.sliceHint': 'Cliquer pour rendre cette slice active — fréquence, mode, DSP et clics de spot la suivent tous.', 'flxp.txSlice': 'Cette slice émet', 'flxp.setTxSlice': 'Déplacer le TX sur cette slice (émettre ici)', 'flxp.voxDly': 'Délai VOX', 'flxp.speed': 'Vitesse', 'flxp.pitch': 'Tonalité', 'flxp.delay': 'Délai',
'flxp.receiveActive': 'Réception (slice active)', 'flxp.muted': 'Coupé — clic pour rétablir', 'flxp.mute': "Couper l'audio RX", 'flxp.filter': 'Filtre', 'flxp.amplifier': 'Amplificateur', 'flxp.atuTune': 'ACCORD', 'flxp.atuTuneHint': "Lance un cycle d'accord sur le coupleur intégré. La radio émet elle-même une porteuse pour mesurer l'adaptation.", 'flxp.atuBypass': 'BYPASS', 'flxp.atuBypassHint': 'Sort le coupleur de la ligne (passage direct).', 'flxp.atuMem': 'MEM', 'flxp.atuMemHint': "Réutilise l'accord mémorisé pour cette fréquence au lieu de refaire un cycle.", 'flxp.atuIdle': 'non accordé', 'flxp.atuTuning': 'accord en cours…', 'flxp.atuOk': 'accordé', 'flxp.atuFail': 'ÉCHEC ACCORD', 'flxp.atuBypassed': 'contourné', 'flxp.atuAborted': 'interrompu', 'flxp.ampInLine': 'Amplificateur en ligne (émission via le PA).', 'flxp.ampBypassed': 'Amplificateur en bypass (standby).', 'flxp.pgConnected': 'PowerGenius connecté', 'flxp.pgOffline': 'PowerGenius hors ligne', 'flxp.fan': 'Ventilo', 'flxp.fanStandard': 'Standard', 'flxp.fanContest': 'Contest', 'flxp.fanBroadcast': 'Diffusion', 'flxp.fault': 'DÉFAUT', 'flxp.meters': 'Mesures', 'flxp.voltage': 'TENSION', 'flxp.paTemp': 'TEMP PA', 'flxp.txFilter': 'Filtre TX', 'flxp.micProfile': 'Profil micro', 'flxp.noMeters': 'Aucune mesure — en attente du flux UDP de la radio…', 'flxp.amplifierHdr': 'AMPLIFICATEUR', 'flxp.outputPower': 'PUISSANCE DE SORTIE', 'flxp.speOffline': 'SPE hors ligne', 'flxp.acomOffline': 'ACOM hors ligne', 'flxp.ampPick': 'Choisir quel amplificateur cette carte affiche', 'flxp.dspV4Hint': 'DSP SmartSDR v4 (séries 8000/Aurora)', 'flxp.daxHint': "DAX comme source audio d'émission (bouton DAX du bandeau transmit de SmartSDR) — pour WSJT-X & co", 'flxp.rnnHint': 'RNN — réduction de bruit par IA (on/off)', 'flxp.anftHint': 'ANFT — filtre notch automatique FFT (on/off)', 'flxp.dspNoise': 'Bruit', 'flxp.dspMore': 'Afficher/masquer le DSP avancé (WNB, NR/notch v4)',
'icmp.spectrum': 'Spectre', 'icmp.scopeFixed': 'Fixe — double-clic / molette pour accorder', 'icmp.scopeCenter': 'Centré — suit le VFO', 'icmp.scopeOff': 'Scope éteint', 'icmp.scopePanDown': 'Décaler le scope −50 kHz', 'icmp.scopePanUp': 'Décaler le scope +50 kHz', 'icmp.scopeCenterVfo': 'Centrer le scope sur la fréquence actuelle (±50 kHz)', 'icmp.notConnected': 'Icom non connecté. Active le backend CI-V Icom dans Réglages → CAT et connecte le port USB de la radio.', 'icmp.refresh': 'Rafraîchir', 'icmp.meters': 'Mesures', 'icmp.transmit': 'Émission', 'icmp.power': 'Puissance', 'icmp.mic': 'Micro', 'icmp.receive': 'Réception', 'icmp.preamp': 'Préampli', 'icmp.filter': 'Filtre', 'icmp.noiseNotch': 'Bruit / Notch', 'icmp.autoNotch': 'Filtre notch auto', 'icmp.apf': 'Filtre de pic audio (CW)', 'icmp.clarifiers': 'RIT / ΔTX', 'icmp.ritHint': 'Molette ou ± pour décaler · Ctrl+←/→ décale le RIT si actif', 'icmp.bandsAntenna': 'Bandes & Antenne', 'icmp.bandCurrent': 'Le poste est sur {b} m', 'icmp.antenna': 'Antenne', 'icmp.passband': 'Passe-bande / Notch', 'icmp.pbtCenter': 'Centrer PBT', 'icmp.manualNotch': 'Notch manuel — active MN, puis règle la position', 'icmp.squelch': 'Squelch', 'icmp.powerOnHint': 'Allumer la radio (démarre en ~15 s)', 'icmp.powerOffHint': 'Éteindre la radio', 'icmp.powerOffConfirm': 'Éteindre la radio ?',
'rst.clickToFill': 'Clic pour remplir le RST tx depuis le signal',
diff --git a/frontend/wailsjs/go/main/App.d.ts b/frontend/wailsjs/go/main/App.d.ts
index 6ae9012..62ecd01 100644
--- a/frontend/wailsjs/go/main/App.d.ts
+++ b/frontend/wailsjs/go/main/App.d.ts
@@ -119,6 +119,8 @@ export function CheckForUpdate():Promise;
export function CheckHamlogKey(arg1:string):Promise;
+export function ClearKenwoodRIT():Promise;
+
export function ClearLookupCache():Promise;
export function CloseAutostartPrograms():Promise;
@@ -1105,12 +1107,36 @@ export function SetFlexRSTChaseEnabled(arg1:boolean):Promise;
export function SetKenwoodAFGain(arg1:number):Promise;
+export function SetKenwoodAGC(arg1:string):Promise;
+
+export function SetKenwoodAntenna(arg1:number):Promise;
+
+export function SetKenwoodAtt(arg1:boolean):Promise;
+
+export function SetKenwoodFilter(arg1:number):Promise;
+
export function SetKenwoodKeySpeed(arg1:number):Promise;
+export function SetKenwoodMicGain(arg1:number):Promise;
+
+export function SetKenwoodNB(arg1:boolean):Promise;
+
+export function SetKenwoodNR(arg1:boolean):Promise;
+
export function SetKenwoodPower(arg1:number):Promise;
+export function SetKenwoodPreamp(arg1:boolean):Promise;
+
+export function SetKenwoodRFGain(arg1:number):Promise;
+
+export function SetKenwoodRIT(arg1:boolean):Promise;
+
+export function SetKenwoodSquelch(arg1:number):Promise;
+
export function SetKenwoodTX(arg1:boolean):Promise;
+export function SetKenwoodXIT(arg1:boolean):Promise;
+
export function SetLinkedAmps(arg1:Array):Promise;
export function SetMotorFollow(arg1:boolean,arg2:number,arg3:string):Promise;
diff --git a/frontend/wailsjs/go/main/App.js b/frontend/wailsjs/go/main/App.js
index 7fe9b65..3052e5d 100644
--- a/frontend/wailsjs/go/main/App.js
+++ b/frontend/wailsjs/go/main/App.js
@@ -178,6 +178,10 @@ export function CheckHamlogKey(arg1) {
return window['go']['main']['App']['CheckHamlogKey'](arg1);
}
+export function ClearKenwoodRIT() {
+ return window['go']['main']['App']['ClearKenwoodRIT']();
+}
+
export function ClearLookupCache() {
return window['go']['main']['App']['ClearLookupCache']();
}
@@ -2150,18 +2154,66 @@ export function SetKenwoodAFGain(arg1) {
return window['go']['main']['App']['SetKenwoodAFGain'](arg1);
}
+export function SetKenwoodAGC(arg1) {
+ return window['go']['main']['App']['SetKenwoodAGC'](arg1);
+}
+
+export function SetKenwoodAntenna(arg1) {
+ return window['go']['main']['App']['SetKenwoodAntenna'](arg1);
+}
+
+export function SetKenwoodAtt(arg1) {
+ return window['go']['main']['App']['SetKenwoodAtt'](arg1);
+}
+
+export function SetKenwoodFilter(arg1) {
+ return window['go']['main']['App']['SetKenwoodFilter'](arg1);
+}
+
export function SetKenwoodKeySpeed(arg1) {
return window['go']['main']['App']['SetKenwoodKeySpeed'](arg1);
}
+export function SetKenwoodMicGain(arg1) {
+ return window['go']['main']['App']['SetKenwoodMicGain'](arg1);
+}
+
+export function SetKenwoodNB(arg1) {
+ return window['go']['main']['App']['SetKenwoodNB'](arg1);
+}
+
+export function SetKenwoodNR(arg1) {
+ return window['go']['main']['App']['SetKenwoodNR'](arg1);
+}
+
export function SetKenwoodPower(arg1) {
return window['go']['main']['App']['SetKenwoodPower'](arg1);
}
+export function SetKenwoodPreamp(arg1) {
+ return window['go']['main']['App']['SetKenwoodPreamp'](arg1);
+}
+
+export function SetKenwoodRFGain(arg1) {
+ return window['go']['main']['App']['SetKenwoodRFGain'](arg1);
+}
+
+export function SetKenwoodRIT(arg1) {
+ return window['go']['main']['App']['SetKenwoodRIT'](arg1);
+}
+
+export function SetKenwoodSquelch(arg1) {
+ return window['go']['main']['App']['SetKenwoodSquelch'](arg1);
+}
+
export function SetKenwoodTX(arg1) {
return window['go']['main']['App']['SetKenwoodTX'](arg1);
}
+export function SetKenwoodXIT(arg1) {
+ return window['go']['main']['App']['SetKenwoodXIT'](arg1);
+}
+
export function SetLinkedAmps(arg1) {
return window['go']['main']['App']['SetLinkedAmps'](arg1);
}
diff --git a/frontend/wailsjs/go/models.ts b/frontend/wailsjs/go/models.ts
index 4d307e6..18ccdeb 100644
--- a/frontend/wailsjs/go/models.ts
+++ b/frontend/wailsjs/go/models.ts
@@ -1082,6 +1082,19 @@ export namespace cat {
swr_raw: number;
rf_power: number;
af_gain: number;
+ rf_gain: number;
+ mic_gain: number;
+ squelch: number;
+ preamp: boolean;
+ att: boolean;
+ nb: boolean;
+ nr: boolean;
+ agc?: string;
+ filter_hz: number;
+ antenna: number;
+ rit: boolean;
+ xit: boolean;
+ key_speed: number;
meters_provisional: boolean;
static createFrom(source: any = {}) {
@@ -1104,6 +1117,19 @@ export namespace cat {
this.swr_raw = source["swr_raw"];
this.rf_power = source["rf_power"];
this.af_gain = source["af_gain"];
+ this.rf_gain = source["rf_gain"];
+ this.mic_gain = source["mic_gain"];
+ this.squelch = source["squelch"];
+ this.preamp = source["preamp"];
+ this.att = source["att"];
+ this.nb = source["nb"];
+ this.nr = source["nr"];
+ this.agc = source["agc"];
+ this.filter_hz = source["filter_hz"];
+ this.antenna = source["antenna"];
+ this.rit = source["rit"];
+ this.xit = source["xit"];
+ this.key_speed = source["key_speed"];
this.meters_provisional = source["meters_provisional"];
}
}
diff --git a/internal/cat/kenwood_panel.go b/internal/cat/kenwood_panel.go
index 846a525..63bc371 100644
--- a/internal/cat/kenwood_panel.go
+++ b/internal/cat/kenwood_panel.go
@@ -49,6 +49,28 @@ type KenwoodTXState struct {
RFPower int `json:"rf_power"` // watts, the PC setting
AFGain int `json:"af_gain"` // 0-100
+ RFGain int `json:"rf_gain"` // 0-100
+ MicGain int `json:"mic_gain"` // 0-100
+ Squelch int `json:"squelch"` // 0-100
+
+ // Receive controls. Preamp and attenuator are the K3's single-step ones
+ // (PA/RA); a rig that answers neither leaves them false and the row shows
+ // the state it read, not a state it assumed.
+ Preamp bool `json:"preamp"`
+ Att bool `json:"att"`
+ NB bool `json:"nb"`
+ NR bool `json:"nr"`
+ AGC string `json:"agc,omitempty"` // "OFF", "SLOW", "FAST"
+ // FilterHz is the DSP bandwidth in Hz (the K3 reports it in 10 Hz units).
+ FilterHz int `json:"filter_hz"`
+ // Antenna is 1 or 2 on a K3 with the internal ATU, 0 when the rig does not
+ // answer AN — which is also how the panel knows to hide the row rather than
+ // offer a switch that goes nowhere.
+ Antenna int `json:"antenna"`
+
+ RIT bool `json:"rit"`
+ XIT bool `json:"xit"`
+ KeySpeed int `json:"key_speed"` // WPM
// MetersProvisional says the meter scaling has not been confirmed against a
// real radio. The panel says so rather than presenting a guess as a
@@ -63,6 +85,19 @@ type KenwoodPanelController interface {
RefreshKenwood() error
SetKenwoodPower(int) error
SetKenwoodAFGain(int) error
+ SetKenwoodRFGain(int) error
+ SetKenwoodMicGain(int) error
+ SetKenwoodSquelch(int) error
+ SetKenwoodPreamp(bool) error
+ SetKenwoodAtt(bool) error
+ SetKenwoodNB(bool) error
+ SetKenwoodNR(bool) error
+ SetKenwoodAGC(string) error
+ SetKenwoodFilter(int) error
+ SetKenwoodAntenna(int) error
+ SetKenwoodRIT(bool) error
+ SetKenwoodXIT(bool) error
+ ClearKenwoodRIT() error
SetKenwoodTX(bool) error
TuneKenwoodATU() error
}
@@ -140,6 +175,116 @@ func (k *Kenwood) readPanelSettings() {
} else if v, ok := k.askNum("AG0;", "AG0", 3); ok {
k.panel.AFGain = scale255(v)
}
+ // RF gain. The K3 counts 000-250 in dB of reduction; a Kenwood uses the
+ // familiar 0-255. Both are shown as a percentage, so the slider means the
+ // same thing on either radio.
+ if v, ok := k.askNum("RG;", "RG", 3); ok {
+ k.panel.RFGain = scalePercent(v, k.rfGainFull())
+ }
+ if v, ok := k.askNum("MG;", "MG", 3); ok {
+ k.panel.MicGain = scalePercent(v, k.micGainFull())
+ }
+ if v, ok := k.askNum("SQ;", "SQ", 3); ok {
+ k.panel.Squelch = scalePercent(v, k.squelchFull())
+ } else if v, ok := k.askNum("SQ0;", "SQ0", 3); ok {
+ k.panel.Squelch = scale255(v)
+ }
+ if v, ok := k.askNum("PA;", "PA", 1); ok {
+ k.panel.Preamp = v != 0
+ }
+ if v, ok := k.askNum("RA;", "RA", 2); ok {
+ k.panel.Att = v != 0
+ }
+ if v, ok := k.askNum("NB;", "NB", 1); ok {
+ k.panel.NB = v != 0
+ }
+ if v, ok := k.askNum("NR;", "NR", 1); ok {
+ k.panel.NR = v != 0
+ }
+ if v, ok := k.askNum("GT;", "GT", 3); ok {
+ k.panel.AGC = kenwoodAGCName(v)
+ }
+ // Filter width. The K3 answers BW in 10 Hz units (BW0270 = 2.7 kHz); a
+ // Kenwood that does not implement it says nothing and the row stays as it
+ // was rather than showing a zero-width filter.
+ if v, ok := k.askNum("BW;", "BW", 4); ok && v > 0 {
+ k.panel.FilterHz = v * 10
+ }
+ // Antenna. Only a K3 with the internal ATU answers; 0 means "this radio has
+ // no antenna switching", which is what hides the row.
+ if v, ok := k.askNum("AN;", "AN", 1); ok {
+ k.panel.Antenna = v
+ }
+ if v, ok := k.askNum("RT;", "RT", 1); ok {
+ k.panel.RIT = v != 0
+ }
+ if v, ok := k.askNum("XT;", "XT", 1); ok {
+ k.panel.XIT = v != 0
+ }
+ if v, ok := k.askNum("KS;", "KS", 3); ok {
+ k.panel.KeySpeed = v
+ }
+}
+
+// The full-scale value of the analogue controls differs between an Elecraft and
+// a Kenwood, and using one rig's scale on the other silently halves or doubles
+// every setting. Kept as three small functions rather than a table so each one
+// carries the range it comes from.
+func (k *Kenwood) rfGainFull() int {
+ if k.elecraft {
+ return 250 // K3: 000-250, dB of reduction
+ }
+ return 255
+}
+
+func (k *Kenwood) micGainFull() int {
+ if k.elecraft {
+ return 60 // K3: 000-060
+ }
+ return 255
+}
+
+func (k *Kenwood) squelchFull() int {
+ if k.elecraft {
+ return 29 // K3: 000-029
+ }
+ return 255
+}
+
+// scalePercent turns a raw value into 0-100 against its own full scale.
+func scalePercent(v, full int) int {
+ if full <= 0 || v <= 0 {
+ return 0
+ }
+ if v >= full {
+ return 100
+ }
+ return v * 100 / full
+}
+
+// kenwoodAGCName decodes GT. The K3 answers 000 (off), 002 (slow) or 004
+// (fast); a Kenwood uses the same three values for the same three states.
+func kenwoodAGCName(v int) string {
+ switch {
+ case v == 0:
+ return "OFF"
+ case v <= 2:
+ return "SLOW"
+ default:
+ return "FAST"
+ }
+}
+
+// kenwoodAGCValue is the inverse, for the buttons.
+func kenwoodAGCValue(name string) int {
+ switch strings.ToUpper(strings.TrimSpace(name)) {
+ case "OFF":
+ return 0
+ case "SLOW":
+ return 2
+ default:
+ return 4
+ }
}
// kenwoodMeterProbes are the candidate meter commands, asked once per
@@ -241,6 +386,91 @@ func (k *Kenwood) SetKenwoodAFGain(p int) error {
return k.setPanel(fmt.Sprintf("AG%03d;", p*255/100))
}
+// SetKenwoodRFGain sets the RF gain, 0-100 of the rig's own scale.
+func (k *Kenwood) SetKenwoodRFGain(p int) error {
+ return k.setPanel(fmt.Sprintf("RG%03d;", clampPercentTo(p, k.rfGainFull())))
+}
+
+func (k *Kenwood) SetKenwoodMicGain(p int) error {
+ return k.setPanel(fmt.Sprintf("MG%03d;", clampPercentTo(p, k.micGainFull())))
+}
+
+func (k *Kenwood) SetKenwoodSquelch(p int) error {
+ return k.setPanel(fmt.Sprintf("SQ%03d;", clampPercentTo(p, k.squelchFull())))
+}
+
+func (k *Kenwood) SetKenwoodPreamp(on bool) error {
+ return k.setPanel(fmt.Sprintf("PA%d;", boolDigit(on)))
+}
+
+// SetKenwoodAtt switches the attenuator. Two digits: the K3 reads RA01 as its
+// single 10 dB pad, and a Kenwood with several steps takes the first one.
+func (k *Kenwood) SetKenwoodAtt(on bool) error {
+ if on {
+ return k.setPanel("RA01;")
+ }
+ return k.setPanel("RA00;")
+}
+
+func (k *Kenwood) SetKenwoodNB(on bool) error {
+ return k.setPanel(fmt.Sprintf("NB%d;", boolDigit(on)))
+}
+
+func (k *Kenwood) SetKenwoodNR(on bool) error {
+ return k.setPanel(fmt.Sprintf("NR%d;", boolDigit(on)))
+}
+
+func (k *Kenwood) SetKenwoodAGC(name string) error {
+ return k.setPanel(fmt.Sprintf("GT%03d;", kenwoodAGCValue(name)))
+}
+
+// SetKenwoodFilter sets the DSP bandwidth in Hz — sent in the K3's 10 Hz units.
+func (k *Kenwood) SetKenwoodFilter(hz int) error {
+ if hz < 50 {
+ hz = 50
+ }
+ if hz > 4000 {
+ hz = 4000
+ }
+ return k.setPanel(fmt.Sprintf("BW%04d;", hz/10))
+}
+
+// SetKenwoodAntenna selects antenna 1 or 2 (a K3 with the internal ATU).
+func (k *Kenwood) SetKenwoodAntenna(n int) error {
+ if n < 1 {
+ n = 1
+ }
+ if n > 2 {
+ n = 2
+ }
+ return k.setPanel(fmt.Sprintf("AN%d;", n))
+}
+
+func (k *Kenwood) SetKenwoodRIT(on bool) error {
+ return k.setPanel(fmt.Sprintf("RT%d;", boolDigit(on)))
+}
+
+func (k *Kenwood) SetKenwoodXIT(on bool) error {
+ return k.setPanel(fmt.Sprintf("XT%d;", boolDigit(on)))
+}
+
+// ClearKenwoodRIT zeroes the RIT/XIT offset, both at once — which is what RC
+// does and what the operator means by "clear it".
+func (k *Kenwood) ClearKenwoodRIT() error {
+ return k.setPanel("RC;")
+}
+
+// clampPercentTo maps 0-100 onto a rig's own full scale.
+func clampPercentTo(p, full int) int {
+ if p < 0 {
+ p = 0
+ }
+ if p > 100 {
+ p = 100
+ }
+ return p * full / 100
+}
+
// SetKenwoodTX keys or unkeys the transmitter — the panel's MOX.
//
// Goes through SetPTT rather than writing TX;/RX; here, so the backend's own