feat: SPE Expert amplifier card in the Flex panel (OPERATE/STANDBY + live status)
When the configured amplifier is an SPE Expert (not the PowerGenius), the Flex panel now shows an SPE control card — the Flex doesn't report SPE amps, so it's driven by OpsLog's own SPE link. The PowerGenius fan-mode control is hidden when the amp type isn't PowerGenius.
This commit is contained in:
@@ -4,7 +4,7 @@ import {
|
|||||||
GetFlexState, FlexSetPower, FlexSetTunePower, FlexTune, FlexSetVox, FlexSetVoxLevel, FlexSetVoxDelay,
|
GetFlexState, FlexSetPower, FlexSetTunePower, FlexTune, FlexSetVox, FlexSetVoxLevel, FlexSetVoxDelay,
|
||||||
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
||||||
FlexMox, FlexAmpOperate,
|
FlexMox, FlexAmpOperate,
|
||||||
GetPGXLStatus, PGXLSetFanMode,
|
GetPGXLStatus, PGXLSetFanMode, GetPGXLSettings, GetSPEStatus, SPESetOperate,
|
||||||
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice,
|
FlexSetAGCMode, FlexSetAGCThreshold, FlexSetAudioLevel, FlexSetMute, FlexSetRXAntenna, FlexSetTXAntenna, FlexSetSplit, FlexSetActiveSlice, FlexSetTXSlice,
|
||||||
FlexSetRIT, FlexSetRITFreq, FlexSetXIT, FlexSetXITFreq,
|
FlexSetRIT, FlexSetRITFreq, FlexSetXIT, FlexSetXITFreq,
|
||||||
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
|
FlexSetNB, FlexSetNBLevel, FlexSetNR, FlexSetNRLevel, FlexSetANF, FlexSetANFLevel,
|
||||||
@@ -310,6 +310,29 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
|||||||
return () => { alive = false; window.clearInterval(id); };
|
return () => { alive = false; window.clearInterval(id); };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// Configured amplifier type (pgxl / spe*), so we show the SPE control when the
|
||||||
|
// operator runs an SPE Expert instead of the PowerGenius.
|
||||||
|
const [ampType, setAmpType] = useState<string>('pgxl');
|
||||||
|
const [ampEnabled, setAmpEnabled] = useState(false);
|
||||||
|
useEffect(() => {
|
||||||
|
let alive = true;
|
||||||
|
const load = () => GetPGXLSettings().then((s: any) => { if (alive) { setAmpType(s?.type || 'pgxl'); setAmpEnabled(!!s?.enabled); } }).catch(() => {});
|
||||||
|
load();
|
||||||
|
const id = window.setInterval(load, 5000);
|
||||||
|
return () => { alive = false; window.clearInterval(id); };
|
||||||
|
}, []);
|
||||||
|
const isSPE = ampEnabled && ampType !== 'pgxl';
|
||||||
|
// SPE Expert live status (only polled when an SPE amp is configured).
|
||||||
|
const [spe, setSpe] = useState<any>({ connected: false });
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isSPE) return;
|
||||||
|
let alive = true;
|
||||||
|
const tick = () => GetSPEStatus().then((s: any) => alive && setSpe(s || { connected: false })).catch(() => {});
|
||||||
|
tick();
|
||||||
|
const id = window.setInterval(tick, 1500);
|
||||||
|
return () => { alive = false; window.clearInterval(id); };
|
||||||
|
}, [isSPE]);
|
||||||
|
|
||||||
const change = (key: keyof FlexState, val: number | boolean | string, send: () => Promise<any>) => {
|
const change = (key: keyof FlexState, val: number | boolean | string, send: () => Promise<any>) => {
|
||||||
hold.current[key] = Date.now() + 900;
|
hold.current[key] = Date.now() + 900;
|
||||||
setSt((p) => ({ ...p, [key]: val }));
|
setSt((p) => ({ ...p, [key]: val }));
|
||||||
@@ -760,6 +783,35 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
|||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* SPE Expert amplifier (serial/TCP) — shown when it's the configured amp.
|
||||||
|
The Flex doesn't report SPE amps, so this card is driven by OpsLog's own
|
||||||
|
SPE link rather than the Flex amplifier object. */}
|
||||||
|
{isSPE && (
|
||||||
|
<Card icon={Flame} title={`${t('flxp.amplifier')} · SPE${spe.model ? ' ' + spe.model : ''}`} accent="#ea580c">
|
||||||
|
<div className="flex items-center gap-3 flex-wrap">
|
||||||
|
<button type="button" disabled={!spe.connected}
|
||||||
|
onClick={() => SPESetOperate(!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'}
|
||||||
|
</button>
|
||||||
|
<span className={cn('inline-flex items-center gap-1.5 text-xs', spe.connected ? 'text-muted-foreground' : 'text-danger')}>
|
||||||
|
<span className={cn('size-1.5 rounded-full', spe.connected ? 'bg-success' : 'bg-danger')} />
|
||||||
|
{spe.connected ? (spe.tx ? 'TX' : 'RX') : t('flxp.pgOffline')}
|
||||||
|
</span>
|
||||||
|
{spe.connected && (
|
||||||
|
<span className="text-xs font-mono text-muted-foreground tabular-nums">
|
||||||
|
{spe.output_w}W · SWR {Number(spe.swr_ant ?? 0).toFixed(1)} · {spe.temp_c}°C · {spe.power_level}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
<div className="flex-1" />
|
||||||
|
{(spe.warnings || spe.alarms) && (
|
||||||
|
<span className="px-2 py-1 rounded bg-danger-muted text-danger-muted-foreground text-xs font-bold">⚠ {spe.warnings} {spe.alarms}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* External amplifier (PowerGenius XL) — only when detected. */}
|
{/* External amplifier (PowerGenius XL) — only when detected. */}
|
||||||
{st.amp_available && (
|
{st.amp_available && (
|
||||||
<Card icon={Flame} title={`${t('flxp.amplifier')}${st.amp_model ? ' · ' + st.amp_model : ''}`} accent="#ea580c">
|
<Card icon={Flame} title={`${t('flxp.amplifier')}${st.amp_model ? ' · ' + st.amp_model : ''}`} accent="#ea580c">
|
||||||
@@ -776,7 +828,7 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
|||||||
{/* Fan mode — shown when the PowerGenius is configured (Settings →
|
{/* Fan mode — shown when the PowerGenius is configured (Settings →
|
||||||
PowerGenius). The dot shows the direct-connection state; the
|
PowerGenius). The dot shows the direct-connection state; the
|
||||||
selector is disabled until connected (hover it for the error). */}
|
selector is disabled until connected (hover it for the error). */}
|
||||||
{(pg.host || pg.connected) && (
|
{ampType === 'pgxl' && (pg.host || pg.connected) && (
|
||||||
<label className="flex items-center gap-1.5 text-xs" title={pg.connected ? t('flxp.pgConnected') : (pg.last_error || t('flxp.pgOffline'))}>
|
<label className="flex items-center gap-1.5 text-xs" title={pg.connected ? t('flxp.pgConnected') : (pg.last_error || t('flxp.pgOffline'))}>
|
||||||
<span className={cn('size-1.5 rounded-full', pg.connected ? 'bg-success shadow-[0_0_6px_rgba(16,185,129,0.8)]' : 'bg-danger')} />
|
<span className={cn('size-1.5 rounded-full', pg.connected ? 'bg-success shadow-[0_0_6px_rgba(16,185,129,0.8)]' : 'bg-danger')} />
|
||||||
<span className="text-muted-foreground">{t('flxp.fan')}</span>
|
<span className="text-muted-foreground">{t('flxp.fan')}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user