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
@@ -9,12 +9,15 @@ import { useI18n } from '@/lib/i18n';
import { writeUiPref } from '@/lib/uiPref';
import { RotorCompass } from '@/components/RotorCompass';
import { AmpCard } from '@/components/AmpCard';
import { TunerCard } from '@/components/TunerCard';
import type { TGStatus } from '@/components/TunerGeniusPanel';
import {
GetStationDevices, SaveStationDevices, GetStationStatus, StationSetRelay,
GetRotatorHeading, RotatorGoTo, RotatorStop,
GetUltrabeamStatus, SetUltrabeamDirection, UltrabeamRetract, MotorSetElement, MotorReadElements,
ListDenkoviDevices, ListSerialPorts, TestStationDevice,
GetAmpStatuses, GetFlexState,
GetTunerGeniusStatus, GetTunerGeniusSettings,
} from '../../wailsjs/go/main/App';
type RotatorProps = { centerLat?: number | null; centerLon?: number | null; bearing?: number | null };
@@ -283,6 +286,21 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
const id = window.setInterval(load, 1500);
return () => { alive = false; window.clearInterval(id); };
}, []);
// Tuner Genius XL (4O3A): a card here too, so operators without a FlexRadio
// panel still get the controls. Re-read the enabled flag so it appears/hides
// without a restart.
const [tg, setTg] = useState<TGStatus>({ connected: false });
const [tgEnabled, setTgEnabled] = useState(false);
useEffect(() => {
let alive = true;
const load = async () => {
try { const en: any = await GetTunerGeniusSettings(); if (alive) setTgEnabled(!!en?.enabled); } catch {}
try { const s: any = await GetTunerGeniusStatus(); if (alive && s) setTg(s as TGStatus); } catch {}
};
load();
const id = window.setInterval(load, 1500);
return () => { alive = false; window.clearInterval(id); };
}, []);
const loadDevices = useCallback(async () => {
try { setDevices(((await GetStationDevices()) ?? []) as Device[]); } catch { /* db not ready */ }
@@ -406,13 +424,15 @@ export function StationControlPanel({ centerLat, centerLon, bearing }: RotatorPr
}
// One card per configured amplifier (identical to the Flex panel's card).
for (const amp of amps) widgets.push({ id: `amp:${amp.id}`, node: <AmpCard amp={amp} flex={flexState} t={t} /> });
// Tuner Genius XL card (identical to the Flex panel's).
if (tgEnabled) widgets.push({ id: 'tuner', node: <TunerCard status={tg} t={t} /> });
for (const dev of devices) widgets.push({ id: dev.id, node: deviceCard(dev) });
const rank = (id: string) => { const i = order.indexOf(id); return i < 0 ? 1e6 : i; };
const ordered = widgets.map((w, i) => ({ ...w, i })).sort((a, b) => (rank(a.id) - rank(b.id)) || (a.i - b.i));
const widgetIds = ordered.map((w) => w.id);
const noDevices = devices.length === 0 && !rot.enabled && !ant.enabled && amps.length === 0;
const noDevices = devices.length === 0 && !rot.enabled && !ant.enabled && amps.length === 0 && !tgEnabled;
return (
<div className="flex-1 min-h-0 overflow-auto p-4">