feat(elecraft): the rest of the main controls on the K3/K4 console
So the tester can exercise everything in one session instead of one control per build: RF and mic gain, squelch, preamp, attenuator, NB, NR, AGC, filter width, antenna, RIT/XIT with a clear, and the keyer speed. The analogue scales differ between an Elecraft and a Kenwood — RF gain 000-250, mic 000-060, squelch 000-029 against 0-255 — so each is mapped through its own full scale and shown as a percentage. Using one rig's range on the other silently halves or doubles every setting. Antenna only appears when the radio answers AN: a K3 without the internal ATU has one socket, and a switch that goes nowhere is worse than none.
This commit is contained in:
@@ -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 (
|
||||
<button type="button" disabled={off} onClick={onClick}
|
||||
className={cn('px-2 py-1 rounded-md text-[11px] font-bold tracking-wide border transition-all disabled:opacity-30',
|
||||
on ? 'bg-primary text-primary-foreground border-primary' : 'bg-card text-muted-foreground border-border hover:border-primary/60')}>
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) => void }) {
|
||||
const { t } = useI18n();
|
||||
const [st, setSt] = useState<KenwoodState>(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<any>) => {
|
||||
setLocal((p) => ({ ...p, ...patch }));
|
||||
localAtRef.current = Date.now();
|
||||
@@ -169,6 +196,86 @@ export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) =>
|
||||
className="flex-1 accent-primary" />
|
||||
<span className="w-12 text-right font-mono tabular-nums">{view.af_gain ?? 0}</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<span className="w-16 shrink-0 text-muted-foreground">{t('k3.rfGain')}</span>
|
||||
<input type="range" min={0} max={100} step={1} disabled={off}
|
||||
value={view.rf_gain ?? 0}
|
||||
onChange={(e) => put({ rf_gain: Number(e.target.value) }, () => SetKenwoodRFGain(Number(e.target.value)))}
|
||||
className="flex-1 accent-primary" />
|
||||
<span className="w-12 text-right font-mono tabular-nums">{view.rf_gain ?? 0}</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<span className="w-16 shrink-0 text-muted-foreground">{t('k3.micGain')}</span>
|
||||
<input type="range" min={0} max={100} step={1} disabled={off}
|
||||
value={view.mic_gain ?? 0}
|
||||
onChange={(e) => put({ mic_gain: Number(e.target.value) }, () => SetKenwoodMicGain(Number(e.target.value)))}
|
||||
className="flex-1 accent-primary" />
|
||||
<span className="w-12 text-right font-mono tabular-nums">{view.mic_gain ?? 0}</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<span className="w-16 shrink-0 text-muted-foreground">{t('k3.squelch')}</span>
|
||||
<input type="range" min={0} max={100} step={1} disabled={off}
|
||||
value={view.squelch ?? 0}
|
||||
onChange={(e) => put({ squelch: Number(e.target.value) }, () => SetKenwoodSquelch(Number(e.target.value)))}
|
||||
className="flex-1 accent-primary" />
|
||||
<span className="w-12 text-right font-mono tabular-nums">{view.squelch ?? 0}</span>
|
||||
</label>
|
||||
|
||||
{/* 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. */}
|
||||
<div className="flex flex-wrap items-center gap-1.5 pt-1">
|
||||
<Toggle label="PRE" on={view.preamp} off={off} onClick={() => SetKenwoodPreamp(!view.preamp).catch(setErrMsg)} />
|
||||
<Toggle label="ATT" on={view.att} off={off} onClick={() => SetKenwoodAtt(!view.att).catch(setErrMsg)} />
|
||||
<Toggle label="NB" on={view.nb} off={off} onClick={() => SetKenwoodNB(!view.nb).catch(setErrMsg)} />
|
||||
<Toggle label="NR" on={view.nr} off={off} onClick={() => SetKenwoodNR(!view.nr).catch(setErrMsg)} />
|
||||
<span className="w-2" />
|
||||
{['OFF', 'SLOW', 'FAST'].map((a) => (
|
||||
<Toggle key={a} label={a} on={(view.agc || '').toUpperCase() === a} off={off}
|
||||
onClick={() => SetKenwoodAGC(a).catch(setErrMsg)} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* RIT / XIT. Clear zeroes both offsets at once — the radio's own RC,
|
||||
and what an operator means by "clear it". */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<Toggle label="RIT" on={view.rit} off={off} onClick={() => SetKenwoodRIT(!view.rit).catch(setErrMsg)} />
|
||||
<Toggle label="XIT" on={view.xit} off={off} onClick={() => SetKenwoodXIT(!view.xit).catch(setErrMsg)} />
|
||||
<Toggle label={t('k3.clear')} on={false} off={off} onClick={() => 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 && (<>
|
||||
<span className="w-2" />
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('k3.antenna')}</span>
|
||||
{[1, 2].map((n) => (
|
||||
<Toggle key={n} label={'ANT' + n} on={view.antenna === n} off={off}
|
||||
onClick={() => SetKenwoodAntenna(n).catch(setErrMsg)} />
|
||||
))}
|
||||
</>)}
|
||||
</div>
|
||||
|
||||
{/* Filter width */}
|
||||
<div className="flex flex-wrap items-center gap-1.5">
|
||||
<span className="text-[10px] uppercase tracking-wider text-muted-foreground">{t('k3.filter')}</span>
|
||||
{FILTERS.map((hz) => (
|
||||
<Toggle key={hz} label={hz >= 1000 ? (hz / 1000).toFixed(1) + 'k' : String(hz)}
|
||||
on={view.filter_hz === hz} off={off}
|
||||
onClick={() => SetKenwoodFilter(hz).catch(setErrMsg)} />
|
||||
))}
|
||||
{view.filter_hz > 0 && (
|
||||
<span className="text-[10px] font-mono text-muted-foreground">{view.filter_hz} Hz</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* CW keyer speed — the radio's own keyer, the one the K3 sends with. */}
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<span className="w-16 shrink-0 text-muted-foreground">{t('k3.keySpeed')}</span>
|
||||
<input type="range" min={8} max={50} step={1} disabled={off}
|
||||
value={view.key_speed || 20}
|
||||
onChange={(e) => put({ key_speed: Number(e.target.value) }, () => SetKenwoodKeySpeed(Number(e.target.value)))}
|
||||
className="flex-1 accent-primary" />
|
||||
<span className="w-12 text-right font-mono tabular-nums">{view.key_speed || 0} wpm</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user