import { useEffect, useRef, useState } from 'react'; import { Radio, AudioLines, Mic, Activity, SlidersHorizontal, Antenna, Filter, Power, Volume2, VolumeX } from 'lucide-react'; import { GetIcomState, IcomRefresh, IcomSetAFGain, IcomSetRFGain, IcomSetNB, IcomSetNBLevel, IcomSetNR, IcomSetNRLevel, IcomSetANF, IcomSetAPF, IcomSetAGC, IcomSetPreamp, IcomSetAtt, IcomSetFilter, AudioMonitorActive, AudioStartMonitor, AudioStopMonitor, IcomSetRFPower, IcomSetMicGain, IcomSetSplit, IcomTune, IcomSetATU, IcomConsolePTT, GetCATState, SetCATFrequency, SetCATMode, IcomSetRIT, IcomSetRITOn, IcomSetXITOn, IcomSetAntenna, IcomSetPBTInner, IcomSetPBTOuter, IcomSetManualNotch, IcomSetNotchPos, IcomSetSquelch, IcomSetComp, IcomSetCompLevel, IcomSetMonitor, IcomSetMonLevel, IcomSetVOX, IcomSetVOXGain, IcomSetAntiVOX, IcomSetPower, } 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'; import { ShiftRow } from '@/components/ShiftRow'; type IcomState = { available: boolean; model?: string; mode?: string; transmitting: boolean; split: boolean; sub_hz?: number; atu_on?: boolean; s_meter: number; power_meter: number; swr_meter: number; rf_power: number; mic_gain: number; af_gain: number; rf_gain: number; nb: boolean; nb_level: number; nr: boolean; nr_level: number; anf: boolean; apf: boolean; agc?: string; preamp: number; att: number; filter: number; rit_hz: number; rit_on: boolean; xit_on: boolean; antenna: number; pbt_inner: number; pbt_outer: number; manual_notch: boolean; notch_pos: number; squelch: number; comp: boolean; comp_level: number; monitor: boolean; mon_level: number; vox: boolean; vox_gain: number; anti_vox: number; }; const ZERO: IcomState = { available: false, transmitting: false, split: false, s_meter: 0, power_meter: 0, swr_meter: 0, rf_power: 0, mic_gain: 0, af_gain: 0, rf_gain: 0, nb: false, nb_level: 0, nr: false, nr_level: 0, anf: false, apf: false, preamp: 0, att: 0, filter: 1, rit_hz: 0, rit_on: false, xit_on: false, antenna: 1, pbt_inner: 50, pbt_outer: 50, manual_notch: false, notch_pos: 50, squelch: 0, comp: false, comp_level: 0, monitor: false, mon_level: 0, vox: false, vox_gain: 0, anti_vox: 0, }; // Band buttons jump the VFO to a sensible default frequency (SSB/CW mix) using // the plain SetFrequency command — no band-stacking codes needed. Hz values. type Band = { l: string; hz: number }; const HF_BANDS: Band[] = [ { l: '160', hz: 1_840_000 }, { l: '80', hz: 3_750_000 }, { l: '40', hz: 7_100_000 }, { l: '30', hz: 10_130_000 }, { l: '20', hz: 14_150_000 }, { l: '17', hz: 18_130_000 }, { l: '15', hz: 21_250_000 }, { l: '12', hz: 24_950_000 }, { l: '10', hz: 28_400_000 }, ]; const B6 = { l: '6', hz: 50_150_000 }; const B2 = { l: '2', hz: 144_300_000 }; // SSB calling const B70 = { l: '70cm', hz: 432_200_000 }; // SSB calling const B23 = { l: '23cm', hz: 1_296_200_000 }; // SSB calling // Band buttons jump the VFO to a sensible default frequency (SSB/CW mix) using // the plain SetFrequency command — no band-stacking codes needed. // // Which buttons to OFFER depends on the radio, exactly as the attenuator steps // do below. An IC-9700 has no HF at all, yet the console was showing it 160 // through 6 and none of the bands it actually covers: ten dead buttons, and no // way to change band from here on the only rig where you would want to. function bandsFor(model?: string): Band[] { const m = (model ?? '').toUpperCase(); if (m.includes('9700')) return [B2, B70, B23]; // VHF/UHF/SHF only if (m.includes('705')) return [...HF_BANDS, B6, B2, B70]; if (m.includes('9100')) return [...HF_BANDS, B6, B2, B70, B23]; // 7300 / 7610 / 7100 / unknown: HF + 6 m, the historical list. return [...HF_BANDS, B6]; } // Mode buttons for the console (like RS-BA1's row). SetCATMode picks USB/LSB for // SSB by frequency and the rig's data variant for digital modes. const MODES = ['SSB', 'CW', 'RTTY', 'PSK', 'AM', 'FM', 'DATA']; // Attenuator steps are MODEL-dependent even though the CI-V command (0x11) is the // same: the value byte is the dB. The IC-7610 (and 7700/7800/7851) have a 6/12/18 // dB stepped attenuator, and the IC-7760 the same (confirmed on a real one); the // IC-7300/705/7100 have a single 20 dB attenuator; the // IC-9700 a single 10 dB. Offering the wrong steps = a dead button (the rig NAKs // e.g. 6 dB on a 7300). Default to the common single 20 dB for unknown models. function attOptions(model?: string): { v: string; l: string }[] { const m = (model ?? '').toUpperCase(); const OFF = { v: '0', l: 'OFF' }; if (/(7610|7700|7760|7800|7850|7851)/.test(m)) { return [OFF, { v: '6', l: '6dB' }, { v: '12', l: '12dB' }, { v: '18', l: '18dB' }]; } if (m.includes('9700')) return [OFF, { v: '10', l: '10dB' }]; return [OFF, { v: '20', l: '20dB' }]; // IC-7300 / IC-705 / IC-7100 / default } // bandOfHz names the amateur band a frequency falls in, so the band row can show // where the rig actually is. The buttons only ever SENT a frequency and carried // no active state at all, so nothing was highlighted whatever the rig reported. // Edges are the ITU/IARU band limits, wide enough to cover regional differences — // out-of-band (transverter IF, general coverage RX) matches nothing, as it should. function bandOfHz(hz?: number): string { if (!hz || hz <= 0) return ''; const mhz = hz / 1_000_000; const bands: [string, number, number][] = [ ['160', 1.8, 2.0], ['80', 3.5, 4.0], ['60', 5.25, 5.45], ['40', 7.0, 7.3], ['30', 10.1, 10.15], ['20', 14.0, 14.35], ['17', 18.068, 18.168], ['15', 21.0, 21.45], ['12', 24.89, 24.99], ['10', 28.0, 29.7], // Labels must match the band buttons' exactly — this is only used to light // the button for the band the rig is on, and '70' never matched '70cm'. ['6', 50.0, 54.0], ['4', 70.0, 70.5], ['2', 144.0, 148.0], ['70cm', 430.0, 450.0], ['23cm', 1240.0, 1300.0], ]; for (const [name, lo, hi] of bands) if (mhz >= lo && mhz <= hi) return name; return ''; } // fmtVFO renders a Hz frequency the way an Icom front panel does: // MHz "." 3-digit-kHz "." 2-digit-(10 Hz). 21032000 → "21.032.00". function fmtVFO(hz?: number): string { if (!hz || hz <= 0) return '––.–––.––'; const mhz = Math.floor(hz / 1_000_000); const khz = Math.floor((hz % 1_000_000) / 1000); const h2 = Math.floor((hz % 1000) / 10); return `${mhz}.${String(khz).padStart(3, '0')}.${String(h2).padStart(2, '0')}`; } // modeMatches marks a mode button active, folding the rig's USB/LSB into SSB. // icomWatts turns the backend's 0-100 meter percentage back into watts on the // IC-7760's own meter face. The backend value is linear in the RAW meter byte // (0-255 → 0-100), but Icom's calibration is not: raw 143 is half deflection // and raw 213 is full scale. On a real 7760 a measured 100 W sits at half // deflection of the 250 W face — the linear ×2.5 first tried showed 140 W for // it. Below half scale watts run 0→100, above it 100→250. // The anchors are MEASURED on the real radio, not derived: a known 50 W read // raw ≈89 and a known 100 W read raw 143 (Icom's documented half-deflection), // with raw 213 = full scale = 250 W. The face is not linear in watts at the // bottom — a two-segment guess showed 50 W as 62 — so watts interpolate // between the measured anchors, and a new measurement just adds a row. // Third measured anchor (2026-08-30): a real 200 W read full deflection — // raw 213 is 200 W on this rig, not the 250 the printed face suggests. const ICOM_7760_PO: [number, number][] = [[0, 0], [89, 50], [143, 100], [213, 200]]; function icomWatts(pct: number): { w: number; defl: number } { const raw = Math.max(0, pct * 2.55); const defl = raw <= 143 ? (raw / 143) * 50 : Math.min(100, 50 + ((raw - 143) / 70) * 50); let w = 200; for (let i = 1; i < ICOM_7760_PO.length; i++) { const [r0, w0] = ICOM_7760_PO[i - 1], [r1, w1] = ICOM_7760_PO[i]; if (raw <= r1) { w = w0 + ((raw - r0) / (r1 - r0)) * (w1 - w0); break; } } return { w: Math.round(w), defl }; } function modeMatches(btn: string, cur?: string): boolean { if (!cur) return false; if (btn === 'SSB') return cur === 'SSB' || cur === 'USB' || cur === 'LSB'; // The backend surfaces USB-D as the operator's digital default (FT8…), or as // plain DATA — either way it is the DATA button that should light. if (btn === 'DATA') return ['DATA', 'FT8', 'FT4', 'JS8', 'JT65', 'JT9', 'MFSK', 'OLIVIA'].includes(cur); if (btn === 'PSK') return cur === 'PSK' || cur === 'PSK31'; return btn === cur; } function Slider({ value, onChange, disabled, accent = '#2563eb', step = 1 }: { value: number; onChange: (v: number) => void; disabled?: boolean; accent?: string; step?: number; }) { const v = Math.max(0, Math.min(100, value)); const ref = useRef(null); // Mouse-wheel adjusts the slider. React's onWheel is passive (preventDefault // is ignored), so attach a non-passive native listener; read live values via // refs to avoid stale closures. const valRef = useRef(value); valRef.current = value; const cbRef = useRef(onChange); cbRef.current = onChange; const disRef = useRef(disabled); disRef.current = disabled; const stepRef = useRef(step); stepRef.current = step; useEffect(() => { const el = ref.current; if (!el) return; const onWheel = (e: WheelEvent) => { if (disRef.current) return; e.preventDefault(); const d = e.deltaY < 0 ? stepRef.current : -stepRef.current; const nv = Math.max(0, Math.min(100, valRef.current + d)); if (nv !== valRef.current) cbRef.current(nv); }; el.addEventListener('wheel', onWheel, { passive: false }); return () => el.removeEventListener('wheel', onWheel); }, []); return ( onChange(parseInt(e.target.value, 10))} className={cn('flex-1 h-1.5 rounded-full appearance-none cursor-pointer disabled:opacity-30 disabled:cursor-default', '[&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:size-3.5 [&::-webkit-slider-thumb]:rounded-full', '[&::-webkit-slider-thumb]:bg-card [&::-webkit-slider-thumb]:border-2 [&::-webkit-slider-thumb]:shadow-sm')} style={{ background: `linear-gradient(to right, ${accent} ${v}%, #d8cfb8 ${v}%)`, borderColor: accent }} /> ); } function Segmented({ value, options, onChange }: { value: string; options: { v: string; l: string }[]; onChange: (v: string) => void; }) { return (
{options.map((o) => ( ))}
); } function Chip({ on, onClick, label }: { on: boolean; onClick: () => void; label: string }) { return ( ); } function LevelRow({ label, on, onToggle, value, onLevel }: { label: string; on: boolean; onToggle: () => void; value: number; onLevel: (v: number) => void; }) { return (
{value}
); } function Card({ icon: Icon, title, accent, children }: { icon: any; title: string; accent?: string; children: React.ReactNode }) { return (
{title}
{children}
); } function Row({ label, children }: { label: string; children: React.ReactNode }) { return (
{label} {children}
); } // Meter — a thin horizontal bar for a live 0-100 reading (S / Po / SWR). // Optional onClick makes the row a button (used to send the S reading to RST tx). function Meter({ label, value, accent, scale, onClick, title }: { label: string; value: number; accent: string; scale?: string; onClick?: () => void; title?: string }) { const v = Math.max(0, Math.min(100, value)); const body = ( <> {label}
{scale ?? v} ); if (onClick) { return ; } return
{body}
; } // sParts turns the raw 0-100 S-meter into S-unit + dB-over-S9 (S9 ≈ 47% on the // CI-V 0-255 scale, +60 dB near full scale). Used for both the display label and // the RST-tx value on click. // Green to S9, amber through +20, red above — the Elecraft console's scale. function sSegColor(frac: number) { if (frac > 0.78) return '#dc2626'; if (frac > 0.55) return '#f59e0b'; return '#16a34a'; } function sParts(v: number): { s: number; over: number; label: string } { if (v >= 47) { const over = Math.max(0, Math.round((v - 47) * 60 / 47)); return { s: 9, over, label: over > 0 ? `S9+${over}` : 'S9' }; } const s = Math.max(0, Math.min(9, Math.round(v / 5.2))); return { s, over: 0, label: `S${s}` }; } // wfColor maps a 0-1 amplitude to a classic waterfall colour ramp // (near-black → blue → cyan → green → amber → red). const WF_STOPS: [number, [number, number, number]][] = [ [0.0, [8, 12, 28]], [0.22, [26, 58, 138]], [0.42, [0, 150, 190]], [0.62, [46, 200, 120]], [0.80, [240, 210, 70]], [1.0, [244, 63, 60]], ]; function wfColor(v: number): [number, number, number] { v = Math.max(0, Math.min(1, Math.pow(v, 0.7))); // gamma-lift so the noise floor still has hue for (let i = 1; i < WF_STOPS.length; i++) { if (v <= WF_STOPS[i][0]) { const [a, ca] = WF_STOPS[i - 1], [b, cb] = WF_STOPS[i]; const f = (v - a) / (b - a || 1); return [Math.round(ca[0] + (cb[0] - ca[0]) * f), Math.round(ca[1] + (cb[1] - ca[1]) * f), Math.round(ca[2] + (cb[2] - ca[2]) * f)]; } } return WF_STOPS[WF_STOPS.length - 1][1]; } // The spectrum scope is GONE, deliberately. Every Icom streams its waveform // differently — the IC-7851 controls a scope it never streams, and a real // IC-7760 stops answering CI-V altogether a few frames in, taking CAT and // audio down with it — and chasing a per-model frame layout for a decoration // is not worth a console that drops the link. The radio has a better scope // on its own front panel. export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (rst: string) => void; isNetwork?: boolean } = {}) { // The speaker toggle lives HERE, next to ON/OFF, because that is where the // operator is looking — burying "stop listening" behind Settings → Audio // meant a trip through two panels to mute a radio sitting in the same room. const [listening, setListening] = useState(false); useEffect(() => { if (!isNetwork) return; let alive = true; const ask = () => AudioMonitorActive().then((v) => { if (alive) setListening(!!v); }).catch(() => {}); ask(); const id = window.setInterval(ask, 2000); return () => { alive = false; window.clearInterval(id); }; }, [isNetwork]); const toggleListening = () => { const next = !listening; setListening(next); (next ? AudioStartMonitor() : Promise.resolve(AudioStopMonitor())).catch(() => setListening(!next)); }; const { t } = useI18n(); const [st, setSt] = useState(ZERO); const [cat, setCat] = useState(null); // RigState (freq/mode/split) for the VFO display const [tuning, setTuning] = useState(false); const txRef = useRef(false); const stRef = useRef(ZERO); stRef.current = st; const load = () => { GetIcomState().then((s) => setSt((s ?? ZERO) as IcomState)).catch(() => {}); GetCATState().then((c) => setCat(c ?? null)).catch(() => {}); }; const setMode = (m: string) => { setCat((c: any) => (c ? { ...c, mode: m } : c)); SetCATMode(m).catch(() => {}); }; // Initial one-shot read of the rig's DSP snapshot on mount (the 500ms poll only // re-reads the cache; the backend also loads DSP on the first responsive read). const refresh = async () => { try { await IcomRefresh(); } catch {} await load(); }; useEffect(() => { refresh(); const id = window.setInterval(load, 500); // fast poll so meters/TX feel live return () => window.clearInterval(id); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); // Optimistic local update + fire the command; the cache poll reconciles. const set = (patch: Partial, fn: () => Promise) => { setSt((s) => ({ ...s, ...patch })); fn().catch(() => {}); }; const toggleMox = () => { const next = !txRef.current; txRef.current = next; // Through the console binding: on a network station the PC microphone // rides with the PTT (silence otherwise); on USB it keys and nothing more. set({ transmitting: next }, () => IcomConsolePTT(next)); }; const tune = async () => { setTuning(true); try { await IcomTune(); } catch {} window.setTimeout(() => setTuning(false), 4000); }; // RIT/ΔTX offset (signed Hz, clamped ±9999). Optimistic like the DSP controls. const setRit = (hz: number) => { const v = Math.max(-9999, Math.min(9999, hz)); set({ rit_hz: v }, () => IcomSetRIT(v)); }; // Ctrl+Left/Right shifts the RIT by ±10 Hz while RIT is active — a keyboard // clarifier for zero-beating a caller without touching the mouse. useEffect(() => { const onKey = (e: KeyboardEvent) => { if (!e.ctrlKey || (e.key !== 'ArrowLeft' && e.key !== 'ArrowRight')) return; const s = stRef.current; if (!s.available || !s.rit_on) return; e.preventDefault(); setRit(s.rit_hz + (e.key === 'ArrowRight' ? 10 : -10)); }; window.addEventListener('keydown', onKey); return () => window.removeEventListener('keydown', onKey); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); if (!st.available) { return (
{t('icmp.notConnected')}
); } const tx = st.transmitting; // VFO readout. In split the active/listening VFO is RX (freq_rx_hz) and the // other is TX (freq_hz); otherwise there's a single VFO (freq_hz). const split = !!cat?.split; const mainHz: number = split ? (cat?.freq_rx_hz || 0) : (cat?.freq_hz || 0); // The sub receiver's dial is worth seeing whether or not split is on — // in split the CAT state's TX freq is the authority, otherwise the panel's // own sub_hz read. const subHz: number = split ? (cat?.freq_hz || 0) : (st.sub_hz || 0); // The panel's own mode first: it carries the sideband (USB/LSB) where the // CAT state folds both into ADIF's SSB. const curMode: string = st.mode || cat?.mode || ''; // Mode-dependent controls: VOX / speech-comp / mic are voice-only (hidden on // CW and data); APF (audio peak filter) is CW-only. Fold USB/LSB into phone. const um = curMode.toUpperCase(); const isCW = um === 'CW' || um === 'CWR'; const isPhone = um === 'SSB' || um === 'USB' || um === 'LSB' || um === 'AM' || um === 'FM'; return (
{/* Header strip: model + mode + live RX/TX indicator + split badge. */}
{tx ? 'TX' : 'RX'} {st.model || 'Icom'} {st.mode ? {st.mode} : null} {st.split ? Split : null}
{/* Radio power ON / OFF — NETWORK only. Over USB the CI-V interface is unpowered while the rig is off, so power-ON can't reach it (OFF works but ON doesn't); hiding both avoids a dead button. On the network the rig's LAN server stays alive in standby, so both work. */} {isNetwork && ( <> )}
{/* VFO readout — the RS-BA1-style twin display: MAIN (active) + SUB, the big tabular frequency, mode badge, band, and the RIT/ΔTX offset. */}
{/* MAIN VFO */}
{tx ? 'Main · TX' : 'Main'} {curMode ? {curMode} : null}
{fmtVFO(mainHz)}
{cat?.band || (mainHz ? '' : '—')} {st.rit_on ? RIT {st.rit_hz > 0 ? '+' : st.rit_hz < 0 ? '−' : ''}{Math.abs(st.rit_hz)} : null} {st.xit_on ? ΔTX : null}
{/* SUB VFO (populated in split; dimmed otherwise) */}
Sub {split ? SPLIT : null}
{fmtVFO(subHz)}
{split ? 'TX' : ''}
{/* Mode selector row (RS-BA1's SSB/CW/RTTY/PSK/AM/FM). */}
{MODES.map((m) => { const on = modeMatches(m, curMode); return ( ); })}
{/* Live meters — the SAME LED MeterBar every other console uses (Flex, Elecraft, the amp cards): one instrument look across the app, per the operator's "les consoles doivent se ressembler". S is clickable → RST. */}
{(() => { const sp = sParts(st.s_meter); return ( { if (!st.transmitting) onReportRST(sMeterRST(sp.s, sp.over, st.mode)); } : undefined} /> ); })()} {(() => { if ((st.model ?? '').includes('7760')) { const { w, defl } = icomWatts(st.power_meter); return ; } return ; })()} 0 ? 1 + st.swr_meter / 33.3 : 0} lo={1} hi={4} accent="#f59e0b" display={st.swr_meter > 0 ? (1 + st.swr_meter / 33.3).toFixed(1) : '—'} />
{/* Band buttons + antenna selection. */}
{bandsFor(st.model).map((b) => { const here = bandOfHz(mainHz) === b.l; return ( ); })}
set({ antenna: parseInt(v) }, () => IcomSetAntenna(parseInt(v)))} />
{/* Clarifiers: RIT & ΔTX (XIT) — wheel or ± to shift, Ctrl+←/→ shifts RIT. */} set({ rit_on: !st.rit_on }, () => IcomSetRITOn(!st.rit_on))} onSet={setRit} /> set({ xit_on: !st.xit_on }, () => IcomSetXITOn(!st.xit_on))} onSet={setRit} />

{t('icmp.ritHint')}

{/* Transmit controls. */} set({ rf_power: v }, () => IcomSetRFPower(v))} /> {/* PC is a percentage of the rig's rated power; on a 200 W rig the operator thinks in watts, so say it in watts there. */} {(st.model ?? '').includes('7760') ? `${st.rf_power * 2} W` : st.rf_power} {isPhone && ( set({ mic_gain: v }, () => IcomSetMicGain(v))} /> {st.mic_gain} )}
set({ split: !st.split }, () => IcomSetSplit(!st.split))} /> {/* Tuner IN/OUT — TUNE below starts a cycle but could never take the tuner back out of line. */} set({ atu_on: !st.atu_on } as any, () => IcomSetATU(!st.atu_on))} />
{/* Monitor (all modes) + speech processor / VOX (voice modes only — they don't exist on CW or data). */}
set({ monitor: !st.monitor }, () => IcomSetMonitor(!st.monitor))} onLevel={(v) => set({ mon_level: v }, () => IcomSetMonLevel(v))} /> {isPhone && ( <> set({ comp: !st.comp }, () => IcomSetComp(!st.comp))} onLevel={(v) => set({ comp_level: v }, () => IcomSetCompLevel(v))} /> set({ vox: !st.vox }, () => IcomSetVOX(!st.vox))} onLevel={(v) => set({ vox_gain: v }, () => IcomSetVOXGain(v))} /> set({ anti_vox: v }, () => IcomSetAntiVOX(v))} /> {st.anti_vox} )}
set({ af_gain: v }, () => IcomSetAFGain(v))} /> {st.af_gain} set({ rf_gain: v }, () => IcomSetRFGain(v))} /> {st.rf_gain} set({ squelch: v }, () => IcomSetSquelch(v))} /> {st.squelch} set({ agc: v }, () => IcomSetAGC(v))} /> set({ preamp: parseInt(v) }, () => IcomSetPreamp(parseInt(v)))} /> set({ att: parseInt(v) }, () => IcomSetAtt(parseInt(v)))} /> set({ filter: parseInt(v) }, () => IcomSetFilter(parseInt(v)))} /> {/* Twin PBT + manual notch. Sliders are 0-100 with 50 = centre. */} set({ pbt_inner: v }, () => IcomSetPBTInner(v))} /> {st.pbt_inner - 50 > 0 ? '+' : ''}{st.pbt_inner - 50} set({ pbt_outer: v }, () => IcomSetPBTOuter(v))} /> {st.pbt_outer - 50 > 0 ? '+' : ''}{st.pbt_outer - 50}
set({ manual_notch: !st.manual_notch }, () => IcomSetManualNotch(!st.manual_notch))} /> set({ notch_pos: v }, () => IcomSetNotchPos(v))} /> {st.notch_pos}

{t('icmp.manualNotch')}

set({ nb: !st.nb }, () => IcomSetNB(!st.nb))} onLevel={(v) => set({ nb_level: v }, () => IcomSetNBLevel(v))} /> set({ nr: !st.nr }, () => IcomSetNR(!st.nr))} onLevel={(v) => set({ nr_level: v }, () => IcomSetNRLevel(v))} />
set({ anf: !st.anf }, () => IcomSetANF(!st.anf))} /> {t('icmp.autoNotch')}
{/* APF (audio peak filter) — CW only: peaks the CW tone. */} {isCW && (
set({ apf: !st.apf }, () => IcomSetAPF(!st.apf))} /> {t('icmp.apf')}
)}
); }