fix(icom): sidebands by name, model-aware controls, address 0x00
From an IC-7300 report, four faults and one addition. USB and LSB could not be commanded at all: modeCode knew 'SSB' — which resolves the sideband from the band — and answered 'unsupported mode' to the sideband names themselves. So an operator wanting USB on 40 m had no way to say it, from the console or from anywhere else. They are separate buttons now, and a rig reporting the folded ADIF 'SSB' still lights the side its frequency implies. The console offered controls the radio does not have: ANT1/ANT2 on a rig with one socket, and a PSK button every non-7610-class Icom NAKs. Both now follow the model, as the band buttons and attenuator steps already did. Mic gain stops being phone-only — on USB-D it still sets what the radio transmits at, so an operator who lives in FT8 had none. CI-V address 0x00 was refused by a 'n > 0' test and silently replaced by the IC-7610 default; an EMPTY setting is what means unconfigured, so the parse error decides now, not the value. Plus the 60 m band button that was missing.
This commit is contained in:
@@ -53,7 +53,13 @@ const ZERO: IcomState = {
|
||||
type Band = { l: string; hz: number };
|
||||
|
||||
const HF_BANDS: Band[] = [
|
||||
{ l: '160', hz: 1_840_000 }, { l: '80', hz: 3_750_000 }, { l: '40', hz: 7_100_000 },
|
||||
{ l: '160', hz: 1_840_000 }, { l: '80', hz: 3_750_000 },
|
||||
// 60 m: the middle of the IARU Region 1 allocation (5351.5-5366.5 kHz), which
|
||||
// every 60 m-capable rig can display. Where the band is channelised (the US)
|
||||
// the operator moves to their channel from here — the button is a way onto
|
||||
// the band, not a claim about what may be transmitted on it.
|
||||
{ l: '60', hz: 5_354_000 },
|
||||
{ l: '40', hz: 7_100_000 },
|
||||
{ l: '30', hz: 10_130_000 }, { l: '20', hz: 14_150_000 }, { l: '17', hz: 18_130_000 },
|
||||
{ l: '15', hz: 21_250_000 }, { l: '12', hz: 24_950_000 }, { l: '10', hz: 28_400_000 },
|
||||
];
|
||||
@@ -78,9 +84,36 @@ function bandsFor(model?: string): Band[] {
|
||||
return [...HF_BANDS, B6];
|
||||
}
|
||||
|
||||
// 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', 'DATA'];
|
||||
// Mode buttons for the console (like RS-BA1's row).
|
||||
//
|
||||
// LSB and USB by NAME, not one "SSB" button that resolves by band: the band
|
||||
// convention is right for a logged mode and useless when the operator means
|
||||
// "put this radio in USB on 40 m", which the console could not express at all.
|
||||
//
|
||||
// PSK is native only on the 7610/7760/7851 class; every other rig NAKs 0x12, so
|
||||
// there the button is dead furniture — see modesFor. Soundcard PSK31 rides on
|
||||
// DATA, which every rig can do.
|
||||
const MODES_BASE = ['LSB', 'USB', 'CW', 'RTTY', 'AM', 'FM', 'DATA'];
|
||||
|
||||
function hasNativePSK(model?: string): boolean {
|
||||
const m = (model ?? '').toUpperCase();
|
||||
return m.includes('7610') || m.includes('7760') || m.includes('7851') ||
|
||||
m.includes('7800') || m.includes('7700');
|
||||
}
|
||||
|
||||
function modesFor(model?: string): string[] {
|
||||
if (!hasNativePSK(model)) return MODES_BASE;
|
||||
return [...MODES_BASE.slice(0, 4), 'PSK', ...MODES_BASE.slice(4)];
|
||||
}
|
||||
|
||||
// Which radios actually have an antenna selector on the CI-V command (0x12).
|
||||
// An IC-7300 has ONE socket: offering it ANT1/ANT2 was two buttons that could
|
||||
// only ever disagree with the front panel.
|
||||
function hasAntennaSelector(model?: string): boolean {
|
||||
const m = (model ?? '').toUpperCase();
|
||||
return m.includes('7610') || m.includes('7760') || m.includes('7851') ||
|
||||
m.includes('7800') || m.includes('7700') || m.includes('9700');
|
||||
}
|
||||
|
||||
// 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
|
||||
@@ -155,9 +188,21 @@ function icomWatts(pct: number): { w: number; defl: number } {
|
||||
return { w: Math.round(w), defl };
|
||||
}
|
||||
|
||||
function modeMatches(btn: string, cur?: string): boolean {
|
||||
// Which sideband a bare "SSB" means at this frequency — the same convention the
|
||||
// backend applies when it resolves the mode for the radio.
|
||||
function sideForHz(hz?: number): string | null {
|
||||
if (!hz || hz <= 0) return null;
|
||||
return hz < 10_000_000 ? 'LSB' : 'USB';
|
||||
}
|
||||
|
||||
function modeMatches(btn: string, cur?: string, hz?: number): boolean {
|
||||
if (!cur) return false;
|
||||
if (btn === 'SSB') return cur === 'SSB' || cur === 'USB' || cur === 'LSB';
|
||||
// A rig that reports the folded ADIF "SSB" still lights the side its
|
||||
// frequency implies, so the row is never blank on a phone contact.
|
||||
if (btn === 'USB' || btn === 'LSB') {
|
||||
if (cur === btn) return true;
|
||||
return cur === 'SSB' && btn === (sideForHz(hz) ?? '');
|
||||
}
|
||||
// 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);
|
||||
@@ -507,9 +552,10 @@ 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-7 border-t border-border/60 divide-x divide-border/60">
|
||||
{MODES.map((m) => {
|
||||
const on = modeMatches(m, curMode);
|
||||
<div className="grid border-t border-border/60 divide-x divide-border/60"
|
||||
style={{ gridTemplateColumns: `repeat(${modesFor(st.model).length}, minmax(0, 1fr))` }}>
|
||||
{modesFor(st.model).map((m) => {
|
||||
const on = modeMatches(m, curMode, mainHz);
|
||||
return (
|
||||
<button key={m} type="button" onClick={() => setMode(m)}
|
||||
className={cn('py-1.5 text-[11px] font-bold tracking-wide transition-colors',
|
||||
@@ -561,10 +607,12 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Row label={t('icmp.antenna')}>
|
||||
<Segmented value={String(st.antenna)} options={[{ v: '1', l: 'ANT1' }, { v: '2', l: 'ANT2' }]}
|
||||
onChange={(v) => set({ antenna: parseInt(v) }, () => IcomSetAntenna(parseInt(v)))} />
|
||||
</Row>
|
||||
{hasAntennaSelector(st.model) && (
|
||||
<Row label={t('icmp.antenna')}>
|
||||
<Segmented value={String(st.antenna)} options={[{ v: '1', l: 'ANT1' }, { v: '2', l: 'ANT2' }]}
|
||||
onChange={(v) => set({ antenna: parseInt(v) }, () => IcomSetAntenna(parseInt(v)))} />
|
||||
</Row>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* Clarifiers: RIT & ΔTX (XIT) — wheel or ± to shift, Ctrl+←/→ shifts RIT. */}
|
||||
@@ -588,7 +636,10 @@ export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (r
|
||||
{(st.model ?? '').includes('7760') ? `${st.rf_power * 2} W` : st.rf_power}
|
||||
</span>
|
||||
</Row>
|
||||
{isPhone && (
|
||||
{/* Not phone-only: on USB-D the same control still sets what the radio
|
||||
transmits at, and hiding it left an operator who lives in FT8 with
|
||||
no mic gain at all. */}
|
||||
{(
|
||||
<Row label={t('icmp.mic')}>
|
||||
<Slider value={st.mic_gain} accent="#ef4444" onChange={(v) => set({ mic_gain: v }, () => IcomSetMicGain(v))} />
|
||||
<span className="w-8 text-right text-xs font-mono tabular-nums text-muted-foreground">{st.mic_gain}</span>
|
||||
|
||||
Reference in New Issue
Block a user