import { useRef, useState } from 'react'; import { Flame, ChevronDown } from 'lucide-react'; import { cn } from '@/lib/utils'; import { MeterBar } from '@/components/MeterBar'; import { AmpOperate, AmpPower, AmpPowerLevel, AmpFanMode, FlexAmpOperate } from '../../wailsjs/go/main/App'; // AmpCard renders the amplifier card exactly like the one in the FlexRadio panel, // so Station Control (and anywhere else that needs it) shows the SAME card instead // of a stripped-down variant. It handles all three amp families: // • SPE Expert / ACOM — driven by OpsLog's own serial/TCP link (amp.spe/amp.acom) // • PowerGenius XL — OPERATE + meters come from the Flex (which reports the // amp), fan mode from the direct GSCP link (amp.pgxl) // Controls use the multi-amp API (AmpOperate/AmpPower/… by amp id) so several amps // can each get their own card. 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}
}
); } // speMaxW / powerLevelLabel — same helpers the Flex panel uses. function speMaxW(model?: string): number { const m = (model || '').toUpperCase(); if (m.includes('20K') || m.includes('2K')) return 2000; if (m.includes('15K')) return 1500; return 1300; } function powerLevelLabel(pl?: string): string { switch ((pl || '').trim().toUpperCase()) { case 'L': return 'Low'; case 'M': return 'Mid'; case 'H': return 'High'; default: return pl || ''; } } type Amp = { id: string; name: string; type?: string; spe?: any; acom?: any; pgxl?: any }; export function AmpCard({ amp, flex, t }: { amp: Amp; flex: any; t: (k: string, v?: any) => string }) { // Peak-hold so the jittery VITA-49 meters read steadily (own ref per card). // One second: the same window as the docked widget, so the two never show // different figures for the same amplifier, and neither trails the end of a // transmission long enough to look like lag. 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 > 1000) { peak.current[key] = { v: val, t: now }; return val; } return p.v; }; const isSPE = !!amp.spe; const isACOM = !!amp.acom; if (isSPE) { const spe = amp.spe; return (
{/* Power ON pulses RTS then DTR — it must stay clickable while the amp is off (no status = not "connected"), and serial only. */}
{(['L', 'M', 'H'] as const).map((lvl, i) => { const active = (spe.power_level || '').trim().toUpperCase() === lvl; return ( ); })}
{spe.connected ? (spe.tx ? 'TX' : 'RX') : t('flxp.speOffline')} {spe.connected && ( {spe.band ? `${spe.band} · ` : ''}{spe.output_w}W · SWR {Number(spe.swr_ant ?? 0).toFixed(1)} · {spe.temp_c}°C · {powerLevelLabel(spe.power_level)} )}
{(spe.warnings || spe.alarms) && ( ⚠ {spe.warnings} {spe.alarms} )}
{spe.connected && ( (f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} /> )} ); } if (isACOM) { const acom = amp.acom; return (
{acom.connected ? acom.state : t('flxp.acomOffline')} {acom.connected && ( {acom.band ? `${acom.band} · ` : ''}{acom.fwd_w}W · SWR {Number(acom.swr ?? 0).toFixed(1)} · {acom.temp_c}°C · Fan {acom.fan} )}
{acom.err_text && ( ⚠ {acom.err_text} )}
{acom.connected && ( (f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} /> )} ); } // PowerGenius XL — OPERATE + meters ride on the Flex; fan mode on the GSCP link. const pg = amp.pgxl || {}; const viaFlex = !!flex?.amp_available; const operate = viaFlex ? !!flex?.amp_operate : !!pg.operate; const connected = !!pg.connected || viaFlex; const fault = flex?.amp_fault; // Meters built from the amplifier's own GSCP status frame, for when the radio // is not feeding a meter stream. // // Whether there is power is the amp's state field; how much is the plain // forward figure. NOT "peakfwd" — that is a latched maximum which is never // reset and survives in the last-known status after the amp disconnects, so it // once claimed 1350 W from an old transmission while 10 W was going out. Same // reason peak_id is left alone. Both readings are gated on transmit so they // fall back to zero between overs instead of freezing on the last one. const pgxlMeters = () => { if (!pg.connected) return null; const txing = typeof flex?.transmitting === 'boolean' ? flex.transmitting : /TRANSMIT/i.test(pg.state || ''); const fwdW = peakHold('pgfwd', txing ? Number(pg.fwd_w) || 0 : 0); const idA = peakHold('pgid', txing ? Number(pg.id) || 0 : 0); const swr = peakHold('pgswr', txing ? Number(pg.vswr) || 0 : 0); const tempC = Number(pg.temperature) || 0; // Two columns, not four. The card sits beside a tall neighbour in Station // Control, so a single row of four leaves the height empty and squeezes each // bar into a quarter width — two rows of two use the room that is already // there and give every bar twice the resolution. return (
(f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} /> {/* Below 1:1 the reading is meaningless, so an idle amp shows a flat bar rather than a zero that looks like a perfect match. */} = 1 ? swr : 1} lo={1} hi={3} display={swr >= 1 ? swr.toFixed(1) : '—'} segColor={(f) => (f > 0.75 ? '#dc2626' : f > 0.4 ? '#f59e0b' : '#16a34a')} /> 0 ? `${Math.round(tempC)} °C` : '—'} segColor={(f) => (f > 0.8 ? '#dc2626' : f > 0.6 ? '#f59e0b' : '#ea580c')} />
); }; return (
{(pg.host || pg.connected) && ( )}
{fault && fault !== 'NONE' && ( {t('flxp.fault')}: {fault} )}
{/* Amplifier meters (FWD / ID / TEMP …). The FlexRadio UDP stream is the preferred source — it is fast and reads the same as SmartSDR. When there is no Flex, or it is not streaming, the amplifier's OWN link carries the same figures; falling back to them is what the docked widget already does. Without that fallback this card showed an operator on a Kenwood nothing but OPERATE and the fan mode, while the amplifier was reporting power, current and temperature all along. */} {(() => { const meters = (flex?.meters as any[]) || []; const dbmToW = (d: number) => Math.pow(10, (d - 30) / 10); const amps = meters.filter((m) => (m.src || '').toUpperCase().includes('AMP') && !/^(RL|DRV)$/i.test((m.name || '').trim())); if (!viaFlex || amps.length === 0) return pgxlMeters(); // Power comes from the radio's meter stream and nothing else. The // amplifier also reports a "peakfwd", and using it was a mistake twice // over: it is a latched maximum that is never reset, and it survives in // the last-known status after the amp disconnects — so it claimed // 1350 W from an old transmission while the radio was putting out 10. return (
{amps.map((m) => { if (/fwd|pwr/i.test(m.name || '') && /dbm/i.test(m.unit || '')) { return ; } const acc = /temp|degc|degf/i.test(`${m.unit}${m.name}`) ? '#ea580c' : /volt/i.test(m.unit || '') ? '#2563eb' : '#16a34a'; let lo = m.lo, hi = m.hi; if (/amp/i.test(m.unit || '') || /^ID$|current/i.test(m.name || '')) { lo = 0; hi = m.hi >= 25 ? m.hi : 25; } return ; })}
); })()} ); }