feat(elecraft): a K3/K4 console — power, volume, S-meter, SWR, MOX, tune
The Elecraft backend already existed; what was missing was somewhere to operate the radio from. This adds the panel, on the Kenwood-dialect client the K3 already speaks, in a tab of its own beside the Yaesu and Icom consoles. Scope is the six controls asked for and nothing else. Every extra command added without a radio to test it against is a control that may or may not do what its label says, and a K3 exposes dozens. No K3 was available while writing this, so the two halves are treated differently. The setters are the commands the reference documents unambiguously and whose effect is visible and reversible (PC, AG, TX/RX). The meters are the opposite: their scaling differs by model and firmware, so the raw answers are logged next to the power setting, the panel says the scaling is provisional, and an unmeasured SWR shows as '—' rather than as a perfect 1.0 — a good match on an antenna nobody measured is the reading that costs a radio. ATU tune sends SWT20 (the K3 front-panel tap) and logs exactly what it sent, so a wrong mapping names itself instead of leaving an operator guessing which button OpsLog pressed.
This commit is contained in:
+18
-2
@@ -78,6 +78,7 @@ import { WorldMap, LocatorMap } from '@/components/MainMap';
|
||||
import { FlexPanel } from '@/components/FlexPanel';
|
||||
import { IcomPanel } from '@/components/IcomPanel';
|
||||
import { YaesuPanel } from '@/components/YaesuPanel';
|
||||
import { ElecraftPanel } from '@/components/ElecraftPanel';
|
||||
import { AntGeniusPanel, type AGStatus } from '@/components/AntGeniusPanel';
|
||||
import { MotorAntennaWidget, type AntStatus } from '@/components/MotorAntennaWidget';
|
||||
import { TunerGeniusPanel, type TGStatus } from '@/components/TunerGeniusPanel';
|
||||
@@ -1861,7 +1862,7 @@ export default function App() {
|
||||
// so it's loaded async on mount and re-read on profile:changed below.
|
||||
// 'none' is only ever stored for the third and fourth panes: the first two are
|
||||
// the Main view, and a layout with no panes at all is not a layout.
|
||||
type MainPaneKind = 'map1' | 'map2' | 'cluster' | 'worked' | 'flex' | 'recent' | 'icom' | 'yaesu' | 'netcontrol' | 'decodes' | 'none';
|
||||
type MainPaneKind = 'map1' | 'map2' | 'cluster' | 'worked' | 'flex' | 'recent' | 'icom' | 'yaesu' | 'elecraft' | 'netcontrol' | 'decodes' | 'none';
|
||||
const [mapZoomSignal, setMapZoomSignal] = useState(0); // bump → world map auto-zooms now
|
||||
const [mainPaneLeft, setMainPaneLeft] = useState<MainPaneKind>('map1');
|
||||
const [mainPaneRight, setMainPaneRight] = useState<MainPaneKind>('map2');
|
||||
@@ -1872,7 +1873,7 @@ export default function App() {
|
||||
// quarter-width map is unreadable.
|
||||
const [mainLayout4, setMainLayout4] = useState<'cols' | 'quad'>('quad');
|
||||
const loadMainPanes = useCallback(async () => {
|
||||
const valid = (v: string): v is MainPaneKind => v === 'map1' || v === 'map2' || v === 'cluster' || v === 'worked' || v === 'flex' || v === 'recent' || v === 'icom' || v === 'yaesu' || v === 'netcontrol' || v === 'decodes';
|
||||
const valid = (v: string): v is MainPaneKind => v === 'map1' || v === 'map2' || v === 'cluster' || v === 'worked' || v === 'flex' || v === 'recent' || v === 'icom' || v === 'yaesu' || v === 'elecraft' || v === 'netcontrol' || v === 'decodes';
|
||||
const [l, r, p3, p4, lay] = await Promise.all([
|
||||
GetUIPref('mainPaneLeft').catch(() => ''),
|
||||
GetUIPref('mainPaneRight').catch(() => ''),
|
||||
@@ -6037,6 +6038,12 @@ export default function App() {
|
||||
<YaesuPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} onKeySpeed={setCWSpeedEverywhere} />
|
||||
</div>
|
||||
);
|
||||
case 'elecraft':
|
||||
return (
|
||||
<div className="h-full w-full min-h-0 rounded-lg overflow-hidden border border-border">
|
||||
<ElecraftPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||
</div>
|
||||
);
|
||||
case 'icom':
|
||||
return (
|
||||
<div className="h-full w-full min-h-0 rounded-lg overflow-hidden border border-border">
|
||||
@@ -7303,6 +7310,7 @@ export default function App() {
|
||||
{catState.backend === 'flex' && <TabsTrigger value="flex">Flex Console</TabsTrigger>}
|
||||
{catState.backend === 'icom' && <TabsTrigger value="icom">Icom Console</TabsTrigger>}
|
||||
{catState.backend === 'yaesu' && <TabsTrigger value="yaesu">Yaesu Console</TabsTrigger>}
|
||||
{(catState.backend === 'elecraft' || catState.backend === 'kenwood') && <TabsTrigger value="elecraft">{t('k3.console')}</TabsTrigger>}
|
||||
{statsTabOpen && (
|
||||
<TabsTrigger value="stats" className="gap-1.5">
|
||||
{t('stats.tab')}
|
||||
@@ -7982,6 +7990,14 @@ export default function App() {
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{/* Elecraft K3/K4 console. Shown for the Kenwood backend too: the
|
||||
K3 speaks that dialect and the panel reads whatever answers. */}
|
||||
{(catState.backend === 'elecraft' || catState.backend === 'kenwood') && (
|
||||
<TabsContent value="elecraft" className="flex-1 min-h-0 p-0">
|
||||
<ElecraftPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||
</TabsContent>
|
||||
)}
|
||||
|
||||
{catState.backend === 'icom' && (
|
||||
<TabsContent value="icom" className="flex-1 min-h-0 p-0">
|
||||
<IcomPanel isNetwork={catBackend === 'icom-net'} onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Radio, Power, Activity, AudioLines, SlidersHorizontal } from 'lucide-react';
|
||||
import {
|
||||
GetKenwoodState, RefreshKenwood, SetKenwoodPower, SetKenwoodAFGain, SetKenwoodTX, TuneKenwoodATU,
|
||||
GetCATState,
|
||||
} from '../../wailsjs/go/main/App';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useI18n } from '@/lib/i18n';
|
||||
import { sMeterRST } from '@/lib/rst';
|
||||
import { MeterBar } from '@/components/MeterBar';
|
||||
|
||||
type KenwoodState = {
|
||||
available: boolean; model?: string; elecraft: boolean; mode?: string;
|
||||
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;
|
||||
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,
|
||||
};
|
||||
|
||||
// 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
|
||||
// it rather than this comment.
|
||||
const S9_RAW = 9;
|
||||
const DB_PER_RAW = 6;
|
||||
function sParts(rawV: number): { s: number; over: number; label: string } {
|
||||
if (rawV >= S9_RAW) {
|
||||
const over = Math.max(0, Math.round((rawV - S9_RAW) * DB_PER_RAW));
|
||||
return { s: 9, over, label: over > 0 ? `S9+${over}` : 'S9' };
|
||||
}
|
||||
const s = Math.max(0, Math.min(9, rawV));
|
||||
return { s, over: 0, label: `S${s}` };
|
||||
}
|
||||
|
||||
// Segment colour, printed the way a radio's own meter is: green up to S9, amber
|
||||
// through the S9+ range, red once the signal would be reported as 59+20 or more.
|
||||
function sSegColor(frac: number) {
|
||||
if (frac > 0.78) return '#dc2626';
|
||||
if (frac > 0.55) return '#f59e0b';
|
||||
return '#16a34a';
|
||||
}
|
||||
|
||||
export function ElecraftPanel({ onReportRST }: { onReportRST?: (rst: string) => void }) {
|
||||
const { t } = useI18n();
|
||||
const [st, setSt] = useState<KenwoodState>(ZERO);
|
||||
const [freqHz, setFreqHz] = useState(0);
|
||||
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 localAtRef = useRef(0);
|
||||
|
||||
useEffect(() => {
|
||||
let alive = true;
|
||||
const tick = async () => {
|
||||
try {
|
||||
const s = (await GetKenwoodState()) as KenwoodState;
|
||||
const c = (await GetCATState()) as any;
|
||||
if (!alive) return;
|
||||
setSt(s);
|
||||
setFreqHz(c?.split && c?.freq_rx_hz > 0 ? c.freq_rx_hz : (c?.freq_hz ?? 0));
|
||||
if (Date.now() - localAtRef.current > 1200) setLocal({});
|
||||
setErr('');
|
||||
} catch (e: any) {
|
||||
if (alive) setErr(String(e?.message ?? e));
|
||||
}
|
||||
};
|
||||
tick();
|
||||
const id = window.setInterval(tick, 500);
|
||||
return () => { alive = false; window.clearInterval(id); };
|
||||
}, []);
|
||||
|
||||
const view = { ...st, ...local };
|
||||
const off = !st.available;
|
||||
|
||||
const put = (patch: typeof local, run: () => Promise<any>) => {
|
||||
setLocal((p) => ({ ...p, ...patch }));
|
||||
localAtRef.current = Date.now();
|
||||
run().catch((e) => setErr(String(e?.message ?? e)));
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="flex flex-col gap-2 h-full min-h-0 bg-card overflow-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 bg-muted/40 border-b border-border shrink-0">
|
||||
<Radio className="size-4 text-primary shrink-0" />
|
||||
<span className="text-xs font-semibold uppercase tracking-wider text-muted-foreground">
|
||||
{st.elecraft ? 'Elecraft' : 'Kenwood'}
|
||||
</span>
|
||||
<span className="text-xs font-mono text-foreground/80">{st.model}</span>
|
||||
<span className={cn('size-2 rounded-full', off ? 'bg-muted-foreground/40' : 'bg-success')} />
|
||||
<div className="flex-1" />
|
||||
<span className="font-mono text-sm tabular-nums">{freqHz > 0 ? (freqHz / 1e6).toFixed(6) : '—'}</span>
|
||||
<button type="button" className="text-[11px] text-muted-foreground hover:text-foreground"
|
||||
onClick={() => RefreshKenwood().catch(() => {})} title={t('k3.refreshHint')}>
|
||||
<SlidersHorizontal className="size-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{off && <div className="px-3 text-xs text-muted-foreground">{t('k3.waiting')}</div>}
|
||||
{!!err && <div className="px-3 text-[11px] text-danger">{err}</div>}
|
||||
|
||||
{/* Meters */}
|
||||
<div className="grid grid-cols-3 gap-2 px-3">
|
||||
<MeterBar label="S-METER" value={view.transmitting ? 0 : view.s_meter} lo={0} hi={100}
|
||||
accent="#16a34a" segColor={sSegColor}
|
||||
display={view.transmitting ? '—' : sParts(view.s_meter_raw).label}
|
||||
onClick={() => {
|
||||
if (view.transmitting || !onReportRST) return;
|
||||
const sp = sParts(view.s_meter_raw);
|
||||
onReportRST(sMeterRST(sp.s, sp.over, view.mode));
|
||||
}}
|
||||
title={t('k3.sMeterHint', { raw: String(view.s_meter_raw) })} />
|
||||
<MeterBar label="PWR" value={view.transmitting ? view.power_meter : 0} lo={0} hi={100} accent="#0ea5e9" />
|
||||
{/* 0 means "not measured", and it must not render as a perfect 1.0:
|
||||
a match that looks ideal on an antenna nobody has measured is the one
|
||||
reading that can cost a radio. */}
|
||||
<MeterBar label="SWR" value={view.transmitting && view.swr > 0 ? view.swr : 1} lo={1} hi={4}
|
||||
accent="#f59e0b"
|
||||
display={view.transmitting && view.swr > 0 ? view.swr.toFixed(1) : '—'} />
|
||||
</div>
|
||||
|
||||
{view.meters_provisional && (
|
||||
<p className="px-3 text-[10px] text-muted-foreground">{t('k3.provisional')}</p>
|
||||
)}
|
||||
|
||||
{/* MOX + TUNE */}
|
||||
<div className="flex items-center gap-2 px-3">
|
||||
<button type="button" disabled={off}
|
||||
onClick={() => SetKenwoodTX(!view.transmitting).catch((e) => setErr(String(e?.message ?? e)))}
|
||||
className={cn('flex-1 px-3 py-2.5 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30',
|
||||
view.transmitting
|
||||
? 'bg-danger text-danger-foreground border-danger shadow-[0_0_14px] shadow-danger/50'
|
||||
: 'bg-card text-danger border-danger hover:bg-danger-muted')}>
|
||||
<Power className="size-4 inline mr-1 -mt-0.5" /> MOX
|
||||
</button>
|
||||
<button type="button" disabled={off}
|
||||
onClick={() => TuneKenwoodATU().catch((e) => setErr(String(e?.message ?? e)))}
|
||||
title={t('k3.tuneHint')}
|
||||
className="flex-1 px-3 py-2.5 rounded-lg text-sm font-extrabold tracking-wide border-2 border-warning text-warning bg-card hover:bg-warning-muted transition-all disabled:opacity-30">
|
||||
<Activity className="size-4 inline mr-1 -mt-0.5" /> TUNE
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Power + volume */}
|
||||
<div className="px-3 pb-3 space-y-2">
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<span className="w-16 shrink-0 text-muted-foreground">{t('k3.power')}</span>
|
||||
<input type="range" min={0} max={110} step={1} disabled={off}
|
||||
value={view.rf_power ?? 0}
|
||||
onChange={(e) => put({ rf_power: Number(e.target.value) }, () => SetKenwoodPower(Number(e.target.value)))}
|
||||
className="flex-1 accent-primary" />
|
||||
<span className="w-12 text-right font-mono tabular-nums">{view.rf_power ?? 0} W</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 text-xs">
|
||||
<span className="w-16 shrink-0 text-muted-foreground flex items-center gap-1">
|
||||
<AudioLines className="size-3.5" /> {t('k3.volume')}
|
||||
</span>
|
||||
<input type="range" min={0} max={100} step={1} disabled={off}
|
||||
value={view.af_gain ?? 0}
|
||||
onChange={(e) => put({ af_gain: Number(e.target.value) }, () => SetKenwoodAFGain(Number(e.target.value)))}
|
||||
className="flex-1 accent-primary" />
|
||||
<span className="w-12 text-right font-mono tabular-nums">{view.af_gain ?? 0}</span>
|
||||
</label>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -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', '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.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', '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.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',
|
||||
|
||||
Reference in New Issue
Block a user