import { useRef } from 'react'; import { Flame, X, Power } from 'lucide-react'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; import { MeterBar } from '@/components/MeterBar'; import { AmpOperate, AmpPower, AmpPowerLevel, AmpFanMode, FlexAmpOperate } from '../../wailsjs/go/main/App'; // AmpWidget — the docked amplifier widget, the compact counterpart of AmpCard // (which is the full-size card shown in the FlexRadio panel and Station // Control). It covers all three families, since an operator running an SPE and // a PowerGenius wants one widget, not two: // • SPE Expert — OPERATE, power ON/OFF, L/M/H, live power / SWR / temp // • ACOM — OPERATE, power ON/OFF, live power / SWR / temp // • PowerGenius — OPERATE and meters ride on the FlexRadio, fan mode on GSCP // With several amplifiers configured the caller passes a selection ("all" or an // amp id) chosen from the toolbar icon's dropdown. type Amp = { id: string; name: string; type?: string; spe?: any; acom?: any; pgxl?: any }; // Full-scale watts for the output bar, from the model string. function maxW(model?: string, fallback = 1300): number { const m = (model || '').toUpperCase(); if (m.includes('20K') || m.includes('2K')) return 2000; if (m.includes('15K')) return 1500; if (m.includes('13K')) return 1300; return fallback; } function swrColour(swr?: number): string { if (!swr || swr <= 0) return 'text-muted-foreground'; if (swr <= 1.5) return 'text-success'; if (swr <= 2.0) return 'text-warning'; return 'text-danger'; } // PowerMeter — the output-power LED bar that closes each column, straight under // the controls (no filler above it: pushed to the bottom it left a band of dead // space in the middle of the column). Uses the app-wide MeterBar, compact size, // so it reads exactly like the meters on the rig panels. function PowerMeter({ label, watts, maxWatts }: { label: string; watts: number; maxWatts: number }) { return ( (f > 0.9 ? '#dc2626' : f > 0.75 ? '#f59e0b' : '#ea580c')} /> ); } // usePeakHold keeps the highest reading seen for a short window, so a reading // sampled between two syllables doesn't collapse the meter. `live` is the key // part: pass false the moment the carrier drops and the held value is thrown // away at once — holding it there made releasing the PTT look like a five // second lag, which read as the whole app being slow. function usePeakHold(windowMs = 1000) { const peak = useRef>({}); return (key: string, val: number, live = true) => { if (!live) { delete peak.current[key]; return val; } const now = Date.now(); const p = peak.current[key]; if (!p || val >= p.v || now - p.t > windowMs) { peak.current[key] = { v: val, t: now }; return val; } return p.v; }; } // Stat — one small labelled figure (power, SWR, temperature). function Stat({ label, value, className }: { label: string; value: string; className?: string }) { return (
{label}
{value}
); } // OperateButton — the STANDBY/OPERATE toggle, identical in behaviour across the // three families (only where the state comes from differs). function OperateButton({ operate, disabled, onClick, t }: { operate: boolean; disabled: boolean; onClick: () => void; t: (k: string, v?: any) => string; }) { return ( ); } // AmpBlock — one amplifier inside the widget. function AmpBlock({ amp, flex, showName, t }: { amp: Amp; flex: any; showName: boolean; t: (k: string, v?: any) => string; }) { const spe = amp.spe, acom = amp.acom; const hold = usePeakHold(); if (spe || acom) { const s = spe || acom; // The amp reports zero watts on receive, so its TX flag clears the meter as // soon as the operator lets go — and the radio's own flag, when we have one, // gets there first (the amp is polled on its own slower cycle). const txing = typeof flex?.transmitting === 'boolean' ? flex.transmitting : s.tx !== false; const w = hold('w', Number(spe ? s.output_w : s.fwd_w) || 0, txing); const swr = Number(spe ? s.swr_ant : s.swr) || 0; const hi = spe ? maxW(s.model) : (Number(s.max_w) || 800); // Power ON drives the remote-on control lines, so it stays available while // the amplifier is off and reporting nothing — but only over a serial link. const canPowerOn = spe ? (s.connected || s.transport === 'serial') : (s.port_open && s.transport === 'serial'); return (
{showName && (
{amp.name || (spe ? 'SPE' : 'ACOM')} {s.connected && s.band && {s.band}}
)} AmpOperate(amp.id, !s.operate).catch(() => {})} />
{/* Output level — SPE only; an ACOM has no L/M/H over CAT. */} {spe && (
{(['L', 'M', 'H'] as const).map((lvl) => { const active = (s.power_level || '').trim().toUpperCase() === lvl; return ( ); })}
)} {s.connected ? (
0 ? swr.toFixed(1) : '—'} className={swrColour(swr)} />
) : (
{t('ampw.offline')}
)} {(s.warnings || s.alarms || s.err_text) && (
{s.err_text || `${s.warnings || ''} ${s.alarms || ''}`.trim()}
)}
); } // PowerGenius XL: OPERATE and the meters come from the radio when the Flex // reports the amp; the fan mode rides on our own 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; const meters = ((flex?.meters as any[]) || []).filter((m) => (m.src || '').toUpperCase().includes('AMP')); const meter = (re: RegExp) => meters.find((m) => re.test((m.name || '').trim())); const dbmToW = (d: number) => Math.pow(10, (d - 30) / 10); const fwd = meter(/^fwd|pwr/i); const flexW = fwd ? (/dbm/i.test(fwd.unit || '') ? dbmToW(fwd.value) : fwd.value) : 0; // Whether there is power is the RADIO's transmit flag: it flips the instant // the PTT does, where the amplifier's own status poll answers a second and a // half late and holds TRANSMIT past the carrier. // // How MUCH is the live reading — the radio's meter stream, or the amp's own // forward figure when there is no Flex. NOT the amp's "peakfwd": that is a // latched maximum which is never reset AND survives in the last-known status // after the amp disconnects, so it reported 1350 W from an old transmission // while the radio was putting out 10 W into a disconnected amplifier. const txing = typeof flex?.transmitting === 'boolean' ? flex.transmitting : fwd ? flexW >= 1 : /TRANSMIT/i.test(pg.state || ''); const liveW = flexW > 0 ? flexW : (pg.connected ? Number(pg.fwd_w) || 0 : 0); const fwdW = hold('fwd', txing ? liveW : 0, txing); const id = meter(/^ID$|current/i); // Live drain current, for the same reason: peak_id latches like peak_w. const idA = id ? Number(id.value) : (txing ? Number(pg.id) || undefined : 0); const temp = meter(/temp/i); const tempC = pg.temperature > 0 ? pg.temperature : (temp ? temp.value : undefined); return (
{showName && (
{amp.name || 'PGXL'}
)} (viaFlex ? FlexAmpOperate(!operate) : AmpOperate(amp.id, !operate)).catch(() => {})} /> {connected ? (
) : (
{t('ampw.offline')}
)} {(pg.host || pg.connected) && ( )} {fault && fault !== 'NONE' && (
{t('flxp.fault')}: {fault}
)}
); } export function AmpWidget({ amps, flex, sel, onClose }: { amps: Amp[]; flex: any; sel: string; // "all" or an amp id onClose: () => void; }) { const { t } = useI18n(); // An id that no longer matches (the amp was removed in Settings) falls back to // showing everything rather than an empty widget. const shown = sel === 'all' ? amps : (amps.filter((a) => a.id === sel).length ? amps.filter((a) => a.id === sel) : amps); const anyOnline = shown.some((a) => a.spe?.connected || a.acom?.connected || a.pgxl?.connected || flex?.amp_available); const title = shown.length === 1 ? (shown[0].name || t('ampw.title')) : t('ampw.title'); return (
{title}
{/* Several amplifiers sit SIDE BY SIDE, one column each: stacked, two amps made a widget taller than the dock row and both ended up half hidden. */}
{shown.length === 0 ? (
{t('ampw.none')}
) : (
{shown.map((a, i) => (
0 && 'pl-2.5 border-l border-border/50')}> 1} t={t} />
))}
)}
); }