import { useRef } from 'react';
import { Flame } from 'lucide-react';
import { cn } from '@/lib/utils';
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.
const METER_SEGMENTS = 26;
function MeterBar({ label, value, unit, lo, hi, accent = '#16a34a', display, segColor, compact }: {
label: string; value: number; unit?: string; lo: number; hi: number; accent?: string; display?: string;
segColor?: (frac: number) => string; compact?: boolean;
}) {
const span = hi - lo;
const pct = span > 0 ? Math.max(0, Math.min(100, ((value - lo) / span) * 100)) : 0;
const lit = Math.round((pct / 100) * METER_SEGMENTS);
return (
{label}
{display !== undefined ? display : (
<>{Math.abs(value) >= 100 ? value.toFixed(0) : value.toFixed(1)}{unit} >
)}
{Array.from({ length: METER_SEGMENTS }).map((_, i) => {
const on = i < lit;
const frac = i / METER_SEGMENTS;
const col = segColor ? segColor(frac) : (frac > 0.82 ? '#dc2626' : accent);
return (
);
})}
);
}
function Card({ icon: Icon, title, accent, children }: { icon: any; title: string; accent?: string; children: React.ReactNode }) {
return (
);
}
// 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).
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 isSPE = !!amp.spe;
const isACOM = !!amp.acom;
if (isSPE) {
const spe = amp.spe;
return (
AmpOperate(amp.id, !spe.operate).catch(() => {})}
className={cn('px-4 py-2 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30',
spe.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
{spe.operate ? 'OPERATE' : 'STANDBY'}
AmpPower(amp.id, true).catch(() => {})}
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON
AmpPower(amp.id, false).catch(() => {})}
className="px-3 py-2 text-sm font-bold bg-card text-danger border-l-2 border-success/70 hover:bg-danger/15 disabled:opacity-30">OFF
{(['L', 'M', 'H'] as const).map((lvl, i) => {
const active = (spe.power_level || '').trim().toUpperCase() === lvl;
return (
AmpPowerLevel(amp.id, lvl).catch(() => {})}
className={cn('px-3 py-2 text-sm font-bold disabled:opacity-30', i > 0 && 'border-l border-border',
active ? 'bg-primary text-primary-foreground' : 'bg-card text-muted-foreground hover:bg-muted')}>
{powerLevelLabel(lvl)}
);
})}
{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 (
AmpOperate(amp.id, !acom.operate).catch(() => {})}
className={cn('px-4 py-2 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30',
acom.operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
{acom.operate ? 'OPERATE' : 'STANDBY'}
AmpPower(amp.id, true).catch(() => {})}
title={acom.transport === 'serial' ? 'Power on (DTR/RTS pulse — needs the power-on pins wired)' : 'Power-on needs the serial DTR/RTS lines — not available over a network bridge'}
className="px-3 py-2 text-sm font-bold bg-card text-success hover:bg-success/15 disabled:opacity-30">ON
AmpPower(amp.id, false).catch(() => {})}
className="px-3 py-2 text-sm font-bold bg-card text-danger border-l-2 border-success/70 hover:bg-danger/15 disabled:opacity-30">OFF
{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;
return (
(viaFlex ? FlexAmpOperate(!operate) : AmpOperate(amp.id, !operate)).catch(() => {})}
className={cn('px-4 py-2 rounded-lg text-sm font-extrabold tracking-wide border-2 transition-all disabled:opacity-30',
operate ? 'bg-warning text-warning-foreground border-warning shadow-[0_0_14px] shadow-warning/50' : 'bg-card text-warning border-warning hover:bg-warning-muted')}>
{operate ? 'OPERATE' : 'STANDBY'}
{operate ? t('flxp.ampInLine') : t('flxp.ampBypassed')}
{(pg.host || pg.connected) && (
{t('flxp.fan')}
AmpFanMode(amp.id, e.target.value).catch(() => {})}
className="h-8 rounded-md border border-warning-border bg-card px-2 text-xs font-semibold text-warning outline-none focus:border-warning disabled:opacity-40"
>
{t('flxp.fanStandard')}
{t('flxp.fanContest')}
{t('flxp.fanBroadcast')}
)}
{fault && fault !== 'NONE' && (
{t('flxp.fault')}: {fault}
)}
{/* Amplifier meters (FWD / ID / TEMP …) from the FlexRadio UDP stream. */}
{viaFlex && (() => {
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 (amps.length === 0) return null;
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 ;
})}
);
})()}
);
}