feat: ATU controls in the FlexRadio panel
The backend already parsed the atu object and exposed ATUStart / ATUBypass / SetATUMemories through the Flex controller and the App bindings — only the UI was missing, so the radio's own tuner was the one thing on the panel you still had to reach SmartSDR for. Placed under TUNE, which is the order of operations: the tuner needs a carrier to measure against. The status is decoded into words rather than shown raw. That is the point of the block: TUNE_FAIL and "never tuned" leave the radio looking identical on its own front panel, and the operator transmits into a bad match either way. Failed is red, tuning amber, tuned green.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { Radio, Zap, Power, AudioLines, Flame, Gauge, Volume2, VolumeX, ChevronDown, SlidersHorizontal } from 'lucide-react';
|
||||
import {
|
||||
GetFlexState, FlexSetPower, FlexSetTunePower, FlexTune, FlexSetVox, FlexSetVoxLevel, FlexSetVoxDelay,
|
||||
GetFlexState, FlexATUStart, FlexATUBypass, FlexSetATUMemories, FlexSetPower, FlexSetTunePower, FlexTune, FlexSetVox, FlexSetVoxLevel, FlexSetVoxDelay,
|
||||
FlexSetProcessor, FlexSetProcessorLevel, FlexSetMon, FlexSetMonLevel, FlexSetMic,
|
||||
FlexMox, FlexAmpOperate,
|
||||
GetPGXLStatus, PGXLSetFanMode,
|
||||
@@ -125,8 +125,8 @@ function Segmented({ value, options, onChange, disabled }: {
|
||||
}
|
||||
|
||||
// Chip — a compact on/off pill (NB/NR/ANF/VOX/MON…).
|
||||
function Chip({ on, onClick, label, disabled, accent = 'emerald' }: {
|
||||
on: boolean; onClick: () => void; label: string; disabled?: boolean; accent?: 'emerald' | 'violet' | 'cyan' | 'amber';
|
||||
function Chip({ on, onClick, label, disabled, accent = 'emerald', title }: {
|
||||
on: boolean; onClick: () => void; label: string; disabled?: boolean; accent?: 'emerald' | 'violet' | 'cyan' | 'amber'; title?: string;
|
||||
}) {
|
||||
const onCls = {
|
||||
emerald: 'bg-success border-success text-success-foreground',
|
||||
@@ -135,7 +135,7 @@ function Chip({ on, onClick, label, disabled, accent = 'emerald' }: {
|
||||
amber: 'bg-warning border-warning text-warning-foreground',
|
||||
}[accent];
|
||||
return (
|
||||
<button type="button" onClick={onClick} disabled={disabled}
|
||||
<button type="button" onClick={onClick} disabled={disabled} title={title}
|
||||
className={cn(
|
||||
// min width (not fixed) so a longer label like STONE keeps symmetric
|
||||
// padding instead of overflowing; short labels (NB/NR/APF) stay aligned.
|
||||
@@ -146,6 +146,37 @@ function Chip({ on, onClick, label, disabled, accent = 'emerald' }: {
|
||||
);
|
||||
}
|
||||
|
||||
// SmartSDR reports the tuner as an enum. It is worth decoding rather than
|
||||
// showing raw: TUNE_FAIL and "never tuned" both leave the radio looking normal,
|
||||
// and the operator transmits into a bad match either way.
|
||||
function atuLabel(status: string | undefined, t: (k: string) => string): string {
|
||||
switch ((status || '').toUpperCase()) {
|
||||
case 'TUNE_IN_PROGRESS': return t('flxp.atuTuning');
|
||||
case 'TUNE_SUCCESSFUL':
|
||||
case 'TUNE_OK': return t('flxp.atuOk');
|
||||
case 'TUNE_FAIL':
|
||||
case 'TUNE_FAIL_BYPASS': return t('flxp.atuFail');
|
||||
case 'TUNE_BYPASS':
|
||||
case 'TUNE_MANUAL_BYPASS': return t('flxp.atuBypassed');
|
||||
case 'TUNE_ABORTED': return t('flxp.atuAborted');
|
||||
case 'TUNE_NOT_STARTED':
|
||||
case 'NONE':
|
||||
case '': return t('flxp.atuIdle');
|
||||
default: return status || '';
|
||||
}
|
||||
}
|
||||
|
||||
function atuTone(status: string | undefined): string {
|
||||
switch ((status || '').toUpperCase()) {
|
||||
case 'TUNE_SUCCESSFUL':
|
||||
case 'TUNE_OK': return 'text-success';
|
||||
case 'TUNE_FAIL':
|
||||
case 'TUNE_FAIL_BYPASS': return 'text-danger';
|
||||
case 'TUNE_IN_PROGRESS': return 'text-warning';
|
||||
default: return 'text-muted-foreground';
|
||||
}
|
||||
}
|
||||
|
||||
function LevelRow({ label, on, onToggle, value, onLevel, disabled, accent, sliderAccent }: {
|
||||
label: string; on: boolean; onToggle: () => void; value: number; onLevel: (v: number) => void;
|
||||
disabled?: boolean; accent?: 'emerald' | 'violet' | 'cyan' | 'amber'; sliderAccent?: string;
|
||||
@@ -624,6 +655,33 @@ export function FlexPanel({ onCWSpeed, onReportRST }: { onCWSpeed?: (wpm: number
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* ATU — the radio's built-in tuner. Sits under TUNE because that is
|
||||
the order of operations: the tuner needs a carrier to measure
|
||||
against, so TUNE first, then start a tuning cycle. */}
|
||||
<div className="flex items-center gap-2 border-t border-border/60 pt-3">
|
||||
<span className="w-14 shrink-0 text-[11px] font-bold text-muted-foreground">ATU</span>
|
||||
<button type="button" disabled={off}
|
||||
title={t('flxp.atuTuneHint')}
|
||||
onClick={() => FlexATUStart().catch(() => {})}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-extrabold tracking-wide border-2 border-warning text-warning bg-card hover:bg-warning-muted transition-all disabled:opacity-30">
|
||||
{t('flxp.atuTune')}
|
||||
</button>
|
||||
<button type="button" disabled={off}
|
||||
title={t('flxp.atuBypassHint')}
|
||||
onClick={() => FlexATUBypass().catch(() => {})}
|
||||
className="px-3 py-1.5 rounded-lg text-xs font-extrabold tracking-wide border-2 border-border text-muted-foreground bg-card hover:bg-muted transition-all disabled:opacity-30">
|
||||
{t('flxp.atuBypass')}
|
||||
</button>
|
||||
<Chip on={st.atu_memories} disabled={off} label={t('flxp.atuMem')} accent="cyan"
|
||||
title={t('flxp.atuMemHint')}
|
||||
onClick={() => change('atu_memories', !st.atu_memories, () => FlexSetATUMemories(!st.atu_memories))} />
|
||||
{/* The status is the whole point: a tune that FAILED and one that
|
||||
never ran look identical on the radio's own front panel. */}
|
||||
<span className={cn('ml-auto text-[11px] font-mono font-semibold whitespace-nowrap', atuTone(st.atu_status))}>
|
||||
{atuLabel(st.atu_status, t)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
|
||||
{!isCW ? (
|
||||
<div className="border-t border-border/60 pt-3 space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user