feat: Tuner Genius XL — A/B channels, plus FlexRadio panel + Station Control cards

Push the tuner control further so it mirrors the native 4O3A app and the way the
PowerGenius XL is surfaced.

Backend (internal/tunergenius):
- Status now carries both RF channels A and B (source/mode, band, frequency,
  bound Flex nickname, per-channel bypass, antenna, PTT), the active channel,
  the C1/L/C2 relay-network positions, and the 3-way-vs-SO2R hardware variant
  (learned once from the `info` reply). Flat freq/antenna still mirror the active
  channel for the compact widget.
- New TunerGeniusActivate(ch) binding → `activate ch=N` (or `ant=N` on 3-way).

Frontend:
- New shared TunerCard, styled exactly like AmpCard (PWR/SWR meter bars,
  A/B channel selector with source+freq+antenna, Tune/Bypass/Operate). Used in
  BOTH the FlexRadio panel (its own card, like the PGXL) and Station Control.
- Docked TunerGeniusPanel widget now shows the two channels A/B with their
  source/frequency/antenna and lets you click to make one active — the missing
  A/B state the user flagged.
- i18n EN/FR for the new labels (channels, antenna, in-line/bypassed, title).

Still UNTESTED on hardware — verify the per-channel field names/units and the
activate behaviour on the real box.
This commit is contained in:
2026-07-25 10:47:11 +02:00
parent 933d601c03
commit 9b677c6b35
12 changed files with 439 additions and 28 deletions
+49 -3
View File
@@ -2,11 +2,17 @@ import { Zap, X, Power, Radio } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useI18n } from '@/lib/i18n';
export type TGChannel = {
ptt?: boolean; band?: number; mode?: number; mode_str?: string; flex?: string;
freq_mhz?: number; bypass?: boolean; antenna?: number;
};
export type TGStatus = {
connected: boolean; host?: string; last_error?: string;
fwd_dbm?: number; fwd_w?: number; swr_db?: number; vswr?: number; freq_mhz?: number;
operate?: boolean; bypass?: boolean; tuning?: boolean; active?: number;
antenna?: number; three_way?: boolean; message?: string;
a?: TGChannel; b?: TGChannel;
relay_c1?: number; relay_l?: number; relay_c2?: number;
};
// swrColour picks a status colour for the VSWR readout: green ≤1.5, amber ≤2.0,
@@ -18,17 +24,51 @@ function swrColour(vswr?: number): string {
return 'text-danger';
}
// TunerGeniusPanel — control widget for a 4O3A Tuner Genius XL ATU. Shows the
// live SWR / forward power and offers Tune (autotune), Bypass and Operate/Standby.
export function TunerGeniusPanel({ status, onTune, onBypass, onOperate, onClose }: {
// ChanRow — a compact A/B channel line in the docked widget. Highlights the
// active channel (green) or TX (red), shows the source + frequency + antenna,
// and clicking it makes that channel active.
function ChanRow({ letter, ch, active, onSelect, t }: {
letter: 'A' | 'B'; ch: TGChannel; active: boolean; onSelect: () => void;
t: (k: string, v?: any) => string;
}) {
const ptt = !!ch.ptt;
const cls = ptt
? 'bg-gradient-to-r from-red-500 to-rose-600 text-white border-red-400/40'
: active
? 'bg-gradient-to-r from-emerald-500 to-emerald-600 text-white border-emerald-400/40'
: 'bg-card/70 text-foreground/80 border-border hover:bg-muted/60';
const src = ch.mode_str || '—';
const freq = ch.freq_mhz && ch.freq_mhz > 0 ? `${ch.freq_mhz.toFixed(3)}` : '—';
return (
<button type="button" onClick={onSelect}
title={active ? t('tgp.chActive', { letter }) : t('tgp.chSelect', { letter })}
className={cn('w-full flex items-center gap-1.5 rounded-lg border px-2 py-1 text-left transition-all active:scale-[0.98]', cls)}>
<span className="font-extrabold text-xs w-3 shrink-0">{letter}</span>
<span className="text-[10px] font-semibold truncate opacity-90 w-12 shrink-0">{src}</span>
<span className="font-mono text-[10px] tabular-nums truncate flex-1">{freq}</span>
{ch.antenna != null && ch.antenna > 0 && (
<span className="text-[9px] font-semibold whitespace-nowrap opacity-90 shrink-0">{t('tgp.ant')}{ch.antenna}</span>
)}
{ptt && <span className="text-[9px] font-bold uppercase shrink-0">TX</span>}
</button>
);
}
// TunerGeniusPanel — compact docked widget for a 4O3A Tuner Genius XL ATU. Shows
// the live SWR / forward power, the two RF channels (A / B, click to activate),
// and Tune / Bypass / Operate. A fuller card (TunerCard) is shown in the FlexRadio
// panel and Station Control.
export function TunerGeniusPanel({ status, onTune, onBypass, onOperate, onActivate, onClose }: {
status: TGStatus;
onTune: () => void;
onBypass: (on: boolean) => void;
onOperate: (on: boolean) => void;
onActivate: (ch: number) => void;
onClose: () => void;
}) {
const { t } = useI18n();
const vswr = status.vswr && status.vswr > 0 ? status.vswr : undefined;
const active = status.active ?? 1;
return (
<div className="h-full flex flex-col rounded-xl border border-border bg-gradient-to-b from-card to-muted/30 shadow-sm overflow-hidden">
@@ -68,6 +108,12 @@ export function TunerGeniusPanel({ status, onTune, onBypass, onOperate, onClose
</div>
</div>
{/* Channel A / B — click to activate */}
<div className="space-y-1">
<ChanRow letter="A" ch={status.a ?? {}} active={active === 1} onSelect={() => onActivate(1)} t={t} />
<ChanRow letter="B" ch={status.b ?? {}} active={active === 2} onSelect={() => onActivate(2)} t={t} />
</div>
{status.message && (
<div className="rounded-lg border border-warning-border bg-warning-muted text-warning-muted-foreground text-[10px] px-2 py-1 text-center break-words">
{status.message}