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
+58 -4
View File
@@ -4430,6 +4430,32 @@ export namespace spe {
export namespace tunergenius {
export class Channel {
ptt: boolean;
band: number;
mode: number;
mode_str: string;
flex: string;
freq_mhz: number;
bypass: boolean;
antenna: number;
static createFrom(source: any = {}) {
return new Channel(source);
}
constructor(source: any = {}) {
if ('string' === typeof source) source = JSON.parse(source);
this.ptt = source["ptt"];
this.band = source["band"];
this.mode = source["mode"];
this.mode_str = source["mode_str"];
this.flex = source["flex"];
this.freq_mhz = source["freq_mhz"];
this.bypass = source["bypass"];
this.antenna = source["antenna"];
}
}
export class Status {
connected: boolean;
host?: string;
@@ -4438,13 +4464,18 @@ export namespace tunergenius {
fwd_w: number;
swr_db: number;
vswr: number;
freq_mhz: number;
operate: boolean;
bypass: boolean;
tuning: boolean;
active: number;
antenna: number;
three_way: boolean;
a: Channel;
b: Channel;
relay_c1: number;
relay_l: number;
relay_c2: number;
freq_mhz: number;
antenna: number;
message?: string;
static createFrom(source: any = {}) {
@@ -4460,15 +4491,38 @@ export namespace tunergenius {
this.fwd_w = source["fwd_w"];
this.swr_db = source["swr_db"];
this.vswr = source["vswr"];
this.freq_mhz = source["freq_mhz"];
this.operate = source["operate"];
this.bypass = source["bypass"];
this.tuning = source["tuning"];
this.active = source["active"];
this.antenna = source["antenna"];
this.three_way = source["three_way"];
this.a = this.convertValues(source["a"], Channel);
this.b = this.convertValues(source["b"], Channel);
this.relay_c1 = source["relay_c1"];
this.relay_l = source["relay_l"];
this.relay_c2 = source["relay_c2"];
this.freq_mhz = source["freq_mhz"];
this.antenna = source["antenna"];
this.message = source["message"];
}
convertValues(a: any, classs: any, asMap: boolean = false): any {
if (!a) {
return a;
}
if (a.slice && a.map) {
return (a as any[]).map(elem => this.convertValues(elem, classs));
} else if ("object" === typeof a) {
if (asMap) {
for (const key of Object.keys(a)) {
a[key] = new classs(a[key]);
}
return a;
}
return new classs(a);
}
return a;
}
}
}