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:
2026-07-28 14:35:56 +02:00
parent 5a6db2b656
commit 386fb7f030
3 changed files with 68 additions and 8 deletions
+62 -4
View File
@@ -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">