feat(icom): a DATA button, native PSK, real watts — and the audio codec

The console gains a DATA mode button (USB-D, what FT8 and friends ride on);
the PSK button now maps to the rigs' native PSK mode (0x12) instead of erroring
as an unknown mode — rigs without one NAK it, exactly as RS-BA1's button does
there. The power meter shows real watts on the IC-7760, whose meter face runs
to 250 W where a 100 W rig's percentage was the watts. The clickable S-meter
gets a pointer cursor so it looks like what it is.

Network audio: rxcodec 0x04 — the experiments on the real 7760 settle the
codec table (0x10 = 16-bit stereo, 0x02 = 8-bit mono, both heard as garble),
and 0x04 is the 16-bit mono the playback path actually plays.
This commit is contained in:
2026-08-29 15:48:26 +02:00
parent bd2edf6624
commit ffbbff80d6
5 changed files with 34 additions and 12 deletions
+12 -4
View File
@@ -79,7 +79,7 @@ function bandsFor(model?: string): Band[] {
// Mode buttons for the console (like RS-BA1's row). SetCATMode picks USB/LSB for
// SSB by frequency and the rig's data variant for digital modes.
const MODES = ['SSB', 'CW', 'RTTY', 'PSK', 'AM', 'FM'];
const MODES = ['SSB', 'CW', 'RTTY', 'PSK', 'AM', 'FM', 'DATA'];
// Attenuator steps are MODEL-dependent even though the CI-V command (0x11) is the
// same: the value byte is the dB. The IC-7610 (and 7700/7800/7851) have a 6/12/18
@@ -132,6 +132,10 @@ function fmtVFO(hz?: number): string {
function modeMatches(btn: string, cur?: string): boolean {
if (!cur) return false;
if (btn === 'SSB') return cur === 'SSB' || cur === 'USB' || cur === 'LSB';
// The backend surfaces USB-D as the operator's digital default (FT8…), or as
// plain DATA — either way it is the DATA button that should light.
if (btn === 'DATA') return ['DATA', 'FT8', 'FT4', 'JS8', 'JT65', 'JT9', 'MFSK', 'OLIVIA'].includes(cur);
if (btn === 'PSK') return cur === 'PSK' || cur === 'PSK31';
return btn === cur;
}
@@ -246,7 +250,7 @@ function Meter({ label, value, accent, scale, onClick, title }: { label: string;
</>
);
if (onClick) {
return <button type="button" onClick={onClick} title={title} className="flex items-center gap-2 w-full rounded hover:bg-muted/50 -mx-1 px-1 py-0.5">{body}</button>;
return <button type="button" onClick={onClick} title={title} className="flex items-center gap-2 w-full rounded cursor-pointer hover:bg-muted/50 -mx-1 px-1 py-0.5">{body}</button>;
}
return <div className="flex items-center gap-2">{body}</div>;
}
@@ -718,7 +722,7 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
</div>
</div>
{/* Mode selector row (RS-BA1's SSB/CW/RTTY/PSK/AM/FM). */}
<div className="grid grid-cols-6 border-t border-border/60 divide-x divide-border/60">
<div className="grid grid-cols-7 border-t border-border/60 divide-x divide-border/60">
{MODES.map((m) => {
const on = modeMatches(m, curMode);
return (
@@ -739,7 +743,11 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
title={onReportRST ? t('rst.clickToFill') : undefined}
onClick={onReportRST ? () => onReportRST(sMeterRST(sp.s, sp.over, st.mode)) : undefined} />
); })()}
<Meter label="Po" value={st.power_meter} accent="#ef4444" scale={`${st.power_meter} W`} />
{/* The meter reads 0-100% of the rig's own scale. On a 100 W rig the
percentage IS the watts; the IC-7760's meter runs to 250 W (the rig
makes 200), so its percentage is worth 2.5 W a point — same face as
the radio's. */}
<Meter label="Po" value={st.power_meter} accent="#ef4444" scale={`${Math.round(st.power_meter * (((st.model ?? '').includes('7760')) ? 2.5 : 1))} W`} />
<Meter label="SWR" value={st.swr_meter} accent="#f59e0b" scale={st.swr_meter > 0 ? `${(1 + st.swr_meter / 33.3).toFixed(1)}` : '1.0'} />
</div>