import { useRef, useState } from 'react'; import { Gauge, Radio, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { MeterBar } from '@/components/MeterBar'; import type { TGStatus, TGChannel } from '@/components/TunerGeniusPanel'; import { TunerGeniusAutotune, TunerGeniusSetBypass, TunerGeniusSetOperate, TunerGeniusActivate } from '../../wailsjs/go/main/App'; // TunerCard renders the 4O3A Tuner Genius XL exactly like the amplifier card // (AmpCard) so Station Control and the FlexRadio panel show the SAME card. It // mirrors the native app's two channels (A / B) with their source, frequency and // antenna, a live PWR / SWR pair, and the Tune / Bypass / Operate actions. It // drives the backend directly (no local state) — the caller's ~1.5s poll // reconciles the display, just like AmpCard. Meters come from the shared MeterBar // so they're the exact same size as the Flex/amp meters. function Card({ icon: Icon, title, accent, children, ckey }: { icon: any; title: string; accent?: string; children: React.ReactNode; ckey?: string }) { // Collapsible with persisted state — same behaviour as the FlexRadio panel's Card. const storeKey = 'opslog.cardOpen.' + (ckey || title); const [open, setOpen] = useState(() => localStorage.getItem(storeKey) !== '0'); const toggle = () => setOpen((o) => { const n = !o; localStorage.setItem(storeKey, n ? '1' : '0'); return n; }); return (
{open &&
{children}
}
); } // ChannelButton — one of the two RF channels (A / B). Clicking it makes that // channel active. Shows the source (RF Sense / Flex / CAT…), frequency and // antenna, matching the two rows of the native 4O3A app. function ChannelButton({ letter, ch, active, ptt, threeWay, onSelect, t }: { letter: 'A' | 'B'; ch: TGChannel; active: boolean; ptt: boolean; threeWay: boolean; onSelect: () => void; t: (k: string, v?: any) => string; }) { const cls = ptt ? 'bg-gradient-to-b from-red-500 to-rose-600 text-white border-red-400/50 shadow-[0_0_10px_rgba(244,63,94,0.5)]' : active ? 'bg-gradient-to-b from-emerald-500 to-emerald-600 text-white border-emerald-400/50 shadow-[0_0_9px_rgba(16,185,129,0.4)]' : 'bg-card text-foreground/80 border-border hover:bg-muted'; const src = ch.mode_str || '—'; const freq = ch.freq_mhz && ch.freq_mhz > 0 ? `${ch.freq_mhz.toFixed(3)} MHz` : '—'; return ( ); } export function TunerCard({ status, t }: { status: TGStatus; t: (k: string, v?: any) => string }) { // Peak-hold, exactly as AmpCard does it. Both cards read the same transmitter, // but the tuner is sampled by a 400 ms TCP poll: on SSB that lands in the gaps // between syllables as often as on a peak, so the raw reading collapses to zero // several times a second mid-transmission and reads as a dropped link. The // amplifier looked steady next to it only because it already smoothed this way. // Hold the highest value seen, decaying after 2 s so it follows a real drop. const peak = useRef>({}); const peakHold = (key: string, val: number) => { const now = Date.now(); const p = peak.current[key]; if (!p || val >= p.v || now - p.t > 2000) { peak.current[key] = { v: val, t: now }; return val; } return p.v; }; const connected = !!status.connected; const rawVswr = status.vswr && status.vswr > 0 ? status.vswr : undefined; const rawFwdW = status.fwd_w && status.fwd_w >= 1 ? status.fwd_w : 0; const fwdW = peakHold('fwd', rawFwdW); // SWR needs RF to mean anything: sampled on receive the device reports a // meaningless 1.00, which would wipe the figure the operator actually wants — // the one measured while transmitting. So show the live value during TX, keep it // briefly afterwards so it can be read, then let it go. // // That expiry is the point: the first version of this held the last TX value // with no timeout at all, so the meter sat frozen on the previous transmission // indefinitely — reading 1.39:1 with the radio plainly in RX and 0 W forward. // Same 2 s window as the power peak above, so the two meters clear together. const swrHold = useRef<{ v: number; t: number } | null>(null); if (rawFwdW >= 1 && rawVswr) swrHold.current = { v: rawVswr, t: Date.now() }; const heldSwr = swrHold.current; const vswr = rawFwdW >= 1 ? rawVswr : heldSwr && Date.now() - heldSwr.t < 2000 ? heldSwr.v : undefined; const active = status.active ?? 1; const a: TGChannel = status.a ?? {}; const b: TGChannel = status.b ?? {}; const title = `${t('tgp.title')}${status.host ? ' · ' + status.host : ''}`; return (
{connected ? (status.bypass ? t('tgp.bypassed') : t('tgp.inLine')) : t('tgp.offline')}
{status.message && ( ⚠ {status.message} )}
{connected && ( <> {/* Channel A / B selector — click to make active (activate ch=1/2). */}
TunerGeniusActivate(1).catch(() => {})} t={t} /> TunerGeniusActivate(2).catch(() => {})} t={t} />
{/* PWR + SWR meters, each taking half the card. The amp card's grid is 3-wide because it shows three meters; copying it here left the tuner's two meters in the first two of three columns, with a third of the card empty to the right of SWR. */}
= 1 ? `${Math.round(fwdW)} W` : '—'} segColor={(f) => (f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} /> (f > 0.5 ? '#dc2626' : f > 0.25 ? '#f59e0b' : '#16a34a')} />
)} ); }