feat: FlexRadio TX power per band and mode class

Settings → FlexRadio, beside the per-band antennas and applied the same way:
when the band or the mode changes. 1.5 kW that is fine on SSB has no business
going into an FT8 signal on the same band.

Three classes rather than one row per mode, because what decides the power is
duty cycle and what the amplifier will take of it, not the mode's name — FT8 and
RTTY punish an amplifier the same way, SSB and AM do not. The classification is
pinned by a test: a digital mode sorted as phone would deliver exactly the
1.5 kW this exists to prevent.

An empty box means "leave the power alone", never zero watts, and an unknown
mode changes nothing — setting a transmit power from a name we do not recognise
is not a good guess.

Unlike the antennas, a locked band does not skip it: the lock means "do not let
the rig drag my log entry around", not "let the amplifier keep whatever the last
mode left".
This commit is contained in:
2026-07-31 11:30:46 +02:00
parent bd2a8524dc
commit 62b6fe0a3a
8 changed files with 250 additions and 7 deletions
+81 -2
View File
@@ -47,7 +47,7 @@ import {
DownloadULSCounties, ULSStatus, BackfillUSCounties,
ComputeStationInfo,
GetUIPref, SetUIPref,
GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas,
GetFlexState, GetFlexBandAntennas, SaveFlexBandAntennas, GetFlexBandPower, SaveFlexBandPower,
GetADIFMonitor, SaveADIFMonitor, PickADIFMonitorFile,
GetRelayAuto, SaveRelayAuto, GetStationDevices,
} from '../../wailsjs/go/main/App';
@@ -953,6 +953,79 @@ function ComingSoon({ id, icon: Icon }: { id: SectionId; icon?: any }) {
);
}
// FlexBandPowerPanel — TX power per band and per class of mode.
//
// Three classes, not one row per mode: what decides the power is duty cycle and
// what the amplifier will take of it, not the mode's name. Blank leaves the
// power alone, which is why the boxes are empty rather than showing 0.
function FlexBandPowerPanel({ bands }: { bands: string[] }) {
const { t } = useI18n();
const [map, setMap] = useState<Record<string, { phone: number; cw: number; digi: number }>>({});
const [msg, setMsg] = useState('');
useEffect(() => { GetFlexBandPower().then((m: any) => setMap(m ?? {})).catch(() => {}); }, []);
const set = (band: string, kind: 'phone' | 'cw' | 'digi', v: string) => {
const key = band.toUpperCase();
const n = Math.max(0, Math.min(100, parseInt(v, 10) || 0));
setMap((m) => {
const cur = m[key] ?? { phone: 0, cw: 0, digi: 0 };
const next = { ...m, [key]: { ...cur, [kind]: n } };
SaveFlexBandPower(next as any)
.then(() => { setMsg(t('flxpw.saved')); window.setTimeout(() => setMsg(''), 1200); })
.catch((e: any) => setMsg(String(e?.message ?? e)));
return next;
});
};
const cell = (b: string, kind: 'phone' | 'cw' | 'digi') => {
const e = map[b.toUpperCase()] ?? { phone: 0, cw: 0, digi: 0 };
return (
<td className="px-2 py-1">
<Input
type="number" min={0} max={100}
className="h-7 w-20 text-sm"
value={e[kind] ? String(e[kind]) : ''}
placeholder="—"
onChange={(ev) => set(b, kind, ev.target.value)}
/>
</td>
);
};
return (
<div className="space-y-3">
<div>
<h3 className="text-base font-semibold text-foreground">{t('flxpw.title')}</h3>
<p className="text-xs text-muted-foreground">{t('flxpw.hint')}</p>
</div>
{bands.length === 0 ? (
<p className="text-xs text-muted-foreground">{t('flxpw.noBands')}</p>
) : (
<div className="rounded-md border border-border overflow-hidden max-w-xl">
<table className="w-full text-sm">
<thead className="bg-muted/40 text-muted-foreground text-xs">
<tr>
<th className="text-left px-3 py-1.5 font-semibold">{t('flxpw.band')}</th>
<th className="text-left px-2 py-1.5 font-semibold">{t('flxpw.phone')}</th>
<th className="text-left px-2 py-1.5 font-semibold">CW</th>
<th className="text-left px-2 py-1.5 font-semibold">{t('flxpw.digi')}</th>
</tr>
</thead>
<tbody>
{bands.map((b) => (
<tr key={b} className="border-t border-border/60">
<td className="px-3 py-1 font-mono">{b}</td>
{cell(b, 'phone')}
{cell(b, 'cw')}
{cell(b, 'digi')}
</tr>
))}
</tbody>
</table>
</div>
)}
{msg && <span className="text-xs text-muted-foreground">{msg}</span>}
</div>
);
}
// FlexBandAntennasPanel — pick the RX/TX antenna per band. Applied automatically
// when the band changes (frequency change / spot click). Antennas come live from
// the connected FlexRadio; bands come from Lists → Bands.
@@ -5591,7 +5664,13 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
antgenius: AntGeniusPanelSettings,
tunergenius: TunerGeniusPanelSettings,
pgxl: PGXLPanelSettings,
flex: () => <FlexBandAntennasPanel bands={lists.bands ?? []} />,
flex: () => (
<>
<FlexBandAntennasPanel bands={lists.bands ?? []} />
<div className="h-6" />
<FlexBandPowerPanel bands={lists.bands ?? []} />
</>
),
audio: AudioPanel,
};