fix: bug where scope is now showing on IC7300

This commit is contained in:
2026-07-09 15:30:24 +02:00
parent 206a6bff09
commit b9079fe4e2
5 changed files with 102 additions and 22 deletions
+8 -2
View File
@@ -366,6 +366,11 @@ export default function App() {
// CAT — receives live rig state via Wails events. // CAT — receives live rig state via Wails events.
const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any); const [catState, setCatState] = useState<CATState>({ enabled: false, connected: false } as any);
// Configured CAT backend ('icom' USB vs 'icom-net'): the live catState.backend
// is "icom" for BOTH, so we track the settings value to tell them apart (used to
// hide the rig ON/OFF buttons on USB, where the interface is unpowered when the
// rig is off so power-ON can't work).
const [catBackend, setCatBackend] = useState('');
const [rotatorHeading, setRotatorHeading] = useState<{ enabled: boolean; ok: boolean; azimuth: number }>({ enabled: false, ok: false, azimuth: 0 }); const [rotatorHeading, setRotatorHeading] = useState<{ enabled: boolean; ok: boolean; azimuth: number }>({ enabled: false, ok: false, azimuth: 0 });
const [ubStatus, setUbStatus] = useState<{ enabled: boolean; connected: boolean; direction: number; moving: boolean }>({ enabled: false, connected: false, direction: 0, moving: false }); const [ubStatus, setUbStatus] = useState<{ enabled: boolean; connected: boolean; direction: number; moving: boolean }>({ enabled: false, connected: false, direction: 0, moving: false });
const [agStatus, setAgStatus] = useState<AGStatus>({ connected: false, port_a: 0, port_b: 0, antennas: [] }); const [agStatus, setAgStatus] = useState<AGStatus>({ connected: false, port_a: 0, port_b: 0, antennas: [] });
@@ -1352,6 +1357,7 @@ export default function App() {
try { try {
const c = await GetCATSettings(); const c = await GetCATSettings();
if (c.digital_default) digitalDefaultRef.current = c.digital_default; if (c.digital_default) digitalDefaultRef.current = c.digital_default;
setCatBackend(c.backend ?? '');
} catch {} } catch {}
}, []); }, []);
const loadLists = useCallback(async () => { const loadLists = useCallback(async () => {
@@ -3154,7 +3160,7 @@ export default function App() {
case 'icom': case 'icom':
return ( return (
<div className="h-full w-full min-h-0 rounded-lg overflow-hidden border border-border"> <div className="h-full w-full min-h-0 rounded-lg overflow-hidden border border-border">
<IcomPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} /> <IcomPanel isNetwork={catBackend === 'icom-net'} onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
</div> </div>
); );
case 'netcontrol': case 'netcontrol':
@@ -4245,7 +4251,7 @@ export default function App() {
is an Icom. */} is an Icom. */}
{catState.backend === 'icom' && ( {catState.backend === 'icom' && (
<TabsContent value="icom" className="flex-1 min-h-0 p-0"> <TabsContent value="icom" className="flex-1 min-h-0 p-0">
<IcomPanel onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} /> <IcomPanel isNetwork={catBackend === 'icom-net'} onReportRST={(r) => { setRstSent(r); rstUserEditedRef.current = true; }} />
</TabsContent> </TabsContent>
)} )}
+25 -4
View File
@@ -58,6 +58,21 @@ const BANDS: { l: string; hz: number }[] = [
// SSB by frequency and the rig's data variant for digital modes. // 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'];
// 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
// dB stepped attenuator; the IC-7300/705/7100 have a single 20 dB attenuator; the
// IC-9700 a single 10 dB. Offering the wrong steps = a dead button (the rig NAKs
// e.g. 6 dB on a 7300). Default to the common single 20 dB for unknown models.
function attOptions(model?: string): { v: string; l: string }[] {
const m = (model ?? '').toUpperCase();
const OFF = { v: '0', l: 'OFF' };
if (/(7610|7700|7800|7850|7851)/.test(m)) {
return [OFF, { v: '6', l: '6dB' }, { v: '12', l: '12dB' }, { v: '18', l: '18dB' }];
}
if (m.includes('9700')) return [OFF, { v: '10', l: '10dB' }];
return [OFF, { v: '20', l: '20dB' }]; // IC-7300 / IC-705 / IC-7100 / default
}
// fmtVFO renders a Hz frequency the way an Icom front panel does: // fmtVFO renders a Hz frequency the way an Icom front panel does:
// MHz "." 3-digit-kHz "." 2-digit-(10 Hz). 21032000 → "21.032.00". // MHz "." 3-digit-kHz "." 2-digit-(10 Hz). 21032000 → "21.032.00".
function fmtVFO(hz?: number): string { function fmtVFO(hz?: number): string {
@@ -534,7 +549,7 @@ function ScopePanadapter() {
// Unlike the Flex (which pushes state), the Icom is polled: meters/TX state are // Unlike the Flex (which pushes state), the Icom is polled: meters/TX state are
// read every cache cycle; DSP set-controls are optimistic and reconcile on the // read every cache cycle; DSP set-controls are optimistic and reconcile on the
// next poll. Front-panel knob changes for DSP show after ↻ Refresh. // next poll. Front-panel knob changes for DSP show after ↻ Refresh.
export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void } = {}) { export function IcomPanel({ onReportRST, isNetwork = false }: { onReportRST?: (rst: string) => void; isNetwork?: boolean } = {}) {
const { t } = useI18n(); const { t } = useI18n();
const [st, setSt] = useState<IcomState>(ZERO); const [st, setSt] = useState<IcomState>(ZERO);
const [cat, setCat] = useState<any>(null); // RigState (freq/mode/split) for the VFO display const [cat, setCat] = useState<any>(null); // RigState (freq/mode/split) for the VFO display
@@ -637,8 +652,12 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void
{st.split ? <span className="rounded-md bg-warning/20 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-warning">Split</span> : null} {st.split ? <span className="rounded-md bg-warning/20 px-1.5 py-0.5 text-[10px] font-bold uppercase tracking-wider text-warning">Split</span> : null}
</div> </div>
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{/* Radio power ON / OFF. Manual by design — the app never wakes the rig {/* Radio power ON / OFF — NETWORK only. Over USB the CI-V interface is
on connect; ON sends the wake preamble then the rig boots ~15 s. */} unpowered while the rig is off, so power-ON can't reach it (OFF works
but ON doesn't); hiding both avoids a dead button. On the network the
rig's LAN server stays alive in standby, so both work. */}
{isNetwork && (
<>
<button type="button" onClick={() => IcomSetPower(true).catch(() => {})} title={t('icmp.powerOnHint')} <button type="button" onClick={() => IcomSetPower(true).catch(() => {})} title={t('icmp.powerOnHint')}
className="inline-flex items-center gap-1 rounded-md border border-success/60 bg-success/10 px-2 py-1 text-xs font-bold text-success hover:bg-success/20"> className="inline-flex items-center gap-1 rounded-md border border-success/60 bg-success/10 px-2 py-1 text-xs font-bold text-success hover:bg-success/20">
<Power className="size-3.5" /> ON <Power className="size-3.5" /> ON
@@ -647,6 +666,8 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void
className="inline-flex items-center gap-1 rounded-md border border-destructive/60 bg-destructive/10 px-2 py-1 text-xs font-bold text-destructive hover:bg-destructive/20"> className="inline-flex items-center gap-1 rounded-md border border-destructive/60 bg-destructive/10 px-2 py-1 text-xs font-bold text-destructive hover:bg-destructive/20">
<Power className="size-3.5" /> OFF <Power className="size-3.5" /> OFF
</button> </button>
</>
)}
</div> </div>
</div> </div>
@@ -804,7 +825,7 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void
onChange={(v) => set({ preamp: parseInt(v) }, () => IcomSetPreamp(parseInt(v)))} /> onChange={(v) => set({ preamp: parseInt(v) }, () => IcomSetPreamp(parseInt(v)))} />
</Row> </Row>
<Row label="Att"> <Row label="Att">
<Segmented value={String(st.att)} options={[{ v: '0', l: 'OFF' }, { v: '6', l: '6dB' }, { v: '12', l: '12dB' }, { v: '18', l: '18dB' }]} <Segmented value={String(st.att)} options={attOptions(cat?.rig)}
onChange={(v) => set({ att: parseInt(v) }, () => IcomSetAtt(parseInt(v)))} /> onChange={(v) => set({ att: parseInt(v) }, () => IcomSetAtt(parseInt(v)))} />
</Row> </Row>
<Row label={t('icmp.filter')}> <Row label={t('icmp.filter')}>
+40 -2
View File
@@ -761,6 +761,20 @@ function FlexBandAntennasPanel({ bands }: { bands: string[] }) {
); );
} }
// Known Icom models paired with their FACTORY default CI-V address. Picking a
// model sets icom_addr so the backend identifies it (civ.ModelName) and the UI
// adapts (e.g. the attenuator steps differ by model). Keep in lockstep with
// civ.ModelName in internal/cat/civ/civ.go. `addr` is decimal.
const ICOM_MODELS: { name: string; addr: number }[] = [
{ name: 'IC-705', addr: 0xA4 },
{ name: 'IC-7300', addr: 0x94 },
{ name: 'IC-7610', addr: 0x98 },
{ name: 'IC-7700', addr: 0x88 },
{ name: 'IC-7800', addr: 0x80 },
{ name: 'IC-9100', addr: 0x7C },
{ name: 'IC-9700', addr: 0xA2 },
];
export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable }: Props) { export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChanged, flexAvailable, icomAvailable }: Props) {
const { t } = useI18n(); const { t } = useI18n();
const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station'); const [selected, setSelected] = useState<SectionId>((initialSection as SectionId) || 'station');
@@ -2054,12 +2068,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="space-y-1 col-span-2"> <div className="space-y-1">
<Label>{t('cat.icomModel')}</Label>
<Select
value={ICOM_MODELS.find((m) => m.addr === (catCfg.icom_addr ?? 0x98))?.name ?? '_custom'}
onValueChange={(v) => { const m = ICOM_MODELS.find((x) => x.name === v); if (m) setCatCfg((s) => ({ ...s, icom_addr: m.addr })); }}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{ICOM_MODELS.map((m) => <SelectItem key={m.name} value={m.name}>{m.name}</SelectItem>)}
<SelectItem value="_custom">{t('cat.icomModelOther')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1">
<Label>{t('cat.civAddr')}</Label> <Label>{t('cat.civAddr')}</Label>
<Input value={(catCfg.icom_addr ?? 0x98).toString(16).toUpperCase().padStart(2, '0')} <Input value={(catCfg.icom_addr ?? 0x98).toString(16).toUpperCase().padStart(2, '0')}
onChange={(e) => { const n = parseInt(e.target.value.replace(/[^0-9a-fA-F]/g, ''), 16); setCatCfg((s) => ({ ...s, icom_addr: (n >= 0 && n <= 0xFF) ? n : s.icom_addr })); }} /> onChange={(e) => { const n = parseInt(e.target.value.replace(/[^0-9a-fA-F]/g, ''), 16); setCatCfg((s) => ({ ...s, icom_addr: (n >= 0 && n <= 0xFF) ? n : s.icom_addr })); }} />
<p className="text-xs text-muted-foreground">{t('cat.civHint')}</p>
</div> </div>
<p className="col-span-2 text-xs text-muted-foreground">{t('cat.civHint')}</p>
</> </>
)} )}
{catCfg.backend === 'icom-net' && ( {catCfg.backend === 'icom-net' && (
@@ -2069,6 +2095,18 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan
<Input placeholder="192.168.1.60" value={catCfg.icom_net_host ?? ''} <Input placeholder="192.168.1.60" value={catCfg.icom_net_host ?? ''}
onChange={(e) => setCatCfg((s) => ({ ...s, icom_net_host: e.target.value }))} /> onChange={(e) => setCatCfg((s) => ({ ...s, icom_net_host: e.target.value }))} />
</div> </div>
<div className="space-y-1">
<Label>{t('cat.icomModel')}</Label>
<Select
value={ICOM_MODELS.find((m) => m.addr === (catCfg.icom_addr ?? 0x98))?.name ?? '_custom'}
onValueChange={(v) => { const m = ICOM_MODELS.find((x) => x.name === v); if (m) setCatCfg((s) => ({ ...s, icom_addr: m.addr })); }}>
<SelectTrigger><SelectValue /></SelectTrigger>
<SelectContent>
{ICOM_MODELS.map((m) => <SelectItem key={m.name} value={m.name}>{m.name}</SelectItem>)}
<SelectItem value="_custom">{t('cat.icomModelOther')}</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-1"> <div className="space-y-1">
<Label>{t('cat.civAddr')}</Label> <Label>{t('cat.civAddr')}</Label>
<Input value={(catCfg.icom_addr ?? 0x98).toString(16).toUpperCase().padStart(2, '0')} <Input value={(catCfg.icom_addr ?? 0x98).toString(16).toUpperCase().padStart(2, '0')}
+2 -2
View File
@@ -137,7 +137,7 @@ const en: Dict = {
'cat.icomNetAudio': 'Stream RX audio over the network (experimental)', 'cat.icomNetAudio': 'Stream RX audio over the network (experimental)',
'cat.icomNetAudioHint': 'Play the rigs received audio through your Listening device (Settings → Audio) over the 50003 stream. Experimental — the audio framing is pending on-rig verification; leave off if control misbehaves.', 'cat.icomNetAudioHint': 'Play the rigs received audio through your Listening device (Settings → Audio) over the 50003 stream. Experimental — the audio framing is pending on-rig verification; leave off if control misbehaves.',
'cat.omnirigRig': 'OmniRig rig slot', 'cat.flexIp': 'FlexRadio IP', 'cat.port': 'Port', 'cat.flexSpots': 'Show cluster spots on the panadapter', 'cat.flexSpotsHint': "(spots from OpsLog's DX cluster appear on the radio, auto-expire after 30 min)", 'cat.omnirigRig': 'OmniRig rig slot', 'cat.flexIp': 'FlexRadio IP', 'cat.port': 'Port', 'cat.flexSpots': 'Show cluster spots on the panadapter', 'cat.flexSpotsHint': "(spots from OpsLog's DX cluster appear on the radio, auto-expire after 30 min)",
'cat.icomPort': 'Icom CI-V port', 'cat.selectCom': 'Select COM port', 'cat.noPorts': 'No ports found', 'cat.baud': 'Baud rate', 'cat.civAddr': 'CI-V address (hex)', 'cat.civHint': 'IC-7610 = 98, IC-7300 = 94, IC-9700 = A2, IC-705 = A4. Set "CI-V USB Echo Back" OFF and CI-V baud to match on the rig.', 'cat.icomPort': 'Icom CI-V port', 'cat.selectCom': 'Select COM port', 'cat.noPorts': 'No ports found', 'cat.baud': 'Baud rate', 'cat.icomModel': 'Rig model', 'cat.icomModelOther': 'Other (custom address)', 'cat.civAddr': 'CI-V address (hex)', 'cat.civHint': 'Pick your model to set the CI-V address automatically (or choose "Other" and type it). Set "CI-V USB Echo Back" OFF and CI-V baud to match on the rig.',
'cat.tciHost': 'TCI host', 'cat.tciHint': 'Enable the TCI server in ExpertSDR2/EESDR (Options → TCI). Default port 40001. Use 127.0.0.1 when OpsLog runs on the same PC.', 'cat.tciSpots': 'Show cluster spots on the panorama', 'cat.tciSpotsHint': "(spots from OpsLog's DX cluster appear on the SDR panadapter)", 'cat.tciHost': 'TCI host', 'cat.tciHint': 'Enable the TCI server in ExpertSDR2/EESDR (Options → TCI). Default port 40001. Use 127.0.0.1 when OpsLog runs on the same PC.', 'cat.tciSpots': 'Show cluster spots on the panorama', 'cat.tciSpotsHint': "(spots from OpsLog's DX cluster appear on the SDR panadapter)",
'cat.pollMs': 'Poll interval (ms)', 'cat.delayMs': 'CAT delay (ms)', 'cat.digitalDefault': 'Default digital mode (when rig reports DIG)', 'cat.modeBeforeFreq': 'Set mode before frequency', 'cat.modeBeforeFreqHint': '(older rigs that drop the mode after a band change)', 'cat.pollMs': 'Poll interval (ms)', 'cat.delayMs': 'CAT delay (ms)', 'cat.digitalDefault': 'Default digital mode (when rig reports DIG)', 'cat.modeBeforeFreq': 'Set mode before frequency', 'cat.modeBeforeFreqHint': '(older rigs that drop the mode after a band change)',
'cat.omnirigHint': 'Configure your rig (COM port, baud rate, model) in OmniRig\'s own settings GUI first. OpsLog will read whichever Rig slot you select here. Set CAT delay above 0 if your rig drops commands sent back-to-back (some older Kenwood/Yaesu). OmniRig only reports generic "DIG" for digital modes — Default digital mode is the specific mode OpsLog will surface (and log).', 'cat.omnirigHint': 'Configure your rig (COM port, baud rate, model) in OmniRig\'s own settings GUI first. OpsLog will read whichever Rig slot you select here. Set CAT delay above 0 if your rig drops commands sent back-to-back (some older Kenwood/Yaesu). OmniRig only reports generic "DIG" for digital modes — Default digital mode is the specific mode OpsLog will surface (and log).',
@@ -332,7 +332,7 @@ const fr: Dict = {
'cat.icomNetAudio': 'Diffuser laudio RX par le réseau (expérimental)', 'cat.icomNetAudio': 'Diffuser laudio RX par le réseau (expérimental)',
'cat.icomNetAudioHint': 'Écoute laudio reçu du poste sur ton périphérique d’écoute (Réglages → Audio) via le flux 50003. Expérimental — le format audio reste à vérifier sur le poste ; laisse désactivé si le contrôle se comporte mal.', 'cat.icomNetAudioHint': 'Écoute laudio reçu du poste sur ton périphérique d’écoute (Réglages → Audio) via le flux 50003. Expérimental — le format audio reste à vérifier sur le poste ; laisse désactivé si le contrôle se comporte mal.',
'cat.omnirigRig': 'Slot OmniRig', 'cat.flexIp': 'IP FlexRadio', 'cat.port': 'Port', 'cat.flexSpots': 'Afficher les spots cluster sur le panadapter', 'cat.flexSpotsHint': "(les spots du cluster DX d'OpsLog apparaissent sur la radio, expirent après 30 min)", 'cat.omnirigRig': 'Slot OmniRig', 'cat.flexIp': 'IP FlexRadio', 'cat.port': 'Port', 'cat.flexSpots': 'Afficher les spots cluster sur le panadapter', 'cat.flexSpotsHint': "(les spots du cluster DX d'OpsLog apparaissent sur la radio, expirent après 30 min)",
'cat.icomPort': 'Port CI-V Icom', 'cat.selectCom': 'Choisir un port COM', 'cat.noPorts': 'Aucun port trouvé', 'cat.baud': 'Débit (baud)', 'cat.civAddr': 'Adresse CI-V (hex)', 'cat.civHint': 'IC-7610 = 98, IC-7300 = 94, IC-9700 = A2, IC-705 = A4. Mets « CI-V USB Echo Back » sur OFF et fais correspondre le débit CI-V sur le poste.', 'cat.icomPort': 'Port CI-V Icom', 'cat.selectCom': 'Choisir un port COM', 'cat.noPorts': 'Aucun port trouvé', 'cat.baud': 'Débit (baud)', 'cat.icomModel': 'Modèle de poste', 'cat.icomModelOther': 'Autre (adresse perso)', 'cat.civAddr': 'Adresse CI-V (hex)', 'cat.civHint': 'Choisis ton modèle pour fixer ladresse CI-V automatiquement (ou « Autre » et saisis-la). Mets « CI-V USB Echo Back » sur OFF et fais correspondre le débit CI-V sur le poste.',
'cat.tciHost': 'Hôte TCI', 'cat.tciHint': 'Active le serveur TCI dans ExpertSDR2/EESDR (Options → TCI). Port par défaut 40001. Utilise 127.0.0.1 si OpsLog tourne sur le même PC.', 'cat.tciSpots': 'Afficher les spots cluster sur le panorama', 'cat.tciSpotsHint': "(les spots du cluster DX d'OpsLog apparaissent sur le panadapter SDR)", 'cat.tciHost': 'Hôte TCI', 'cat.tciHint': 'Active le serveur TCI dans ExpertSDR2/EESDR (Options → TCI). Port par défaut 40001. Utilise 127.0.0.1 si OpsLog tourne sur le même PC.', 'cat.tciSpots': 'Afficher les spots cluster sur le panorama', 'cat.tciSpotsHint': "(les spots du cluster DX d'OpsLog apparaissent sur le panadapter SDR)",
'cat.pollMs': 'Intervalle de poll (ms)', 'cat.delayMs': 'Délai CAT (ms)', 'cat.digitalDefault': 'Mode numérique par défaut (quand le poste indique DIG)', 'cat.modeBeforeFreq': 'Régler le mode avant la fréquence', 'cat.modeBeforeFreqHint': '(anciens postes qui perdent le mode après un changement de bande)', 'cat.pollMs': 'Intervalle de poll (ms)', 'cat.delayMs': 'Délai CAT (ms)', 'cat.digitalDefault': 'Mode numérique par défaut (quand le poste indique DIG)', 'cat.modeBeforeFreq': 'Régler le mode avant la fréquence', 'cat.modeBeforeFreqHint': '(anciens postes qui perdent le mode après un changement de bande)',
'cat.omnirigHint': "Configure d'abord ton poste (port COM, débit, modèle) dans l'interface de réglages d'OmniRig. OpsLog lira le slot Rig que tu choisis ici. Mets le délai CAT au-dessus de 0 si ton poste perd des commandes envoyées coup sur coup (certains anciens Kenwood/Yaesu). OmniRig ne rapporte qu'un « DIG » générique pour les modes numériques — le mode numérique par défaut est le mode précis qu'OpsLog affichera (et loggera).", 'cat.omnirigHint': "Configure d'abord ton poste (port COM, débit, modèle) dans l'interface de réglages d'OmniRig. OpsLog lira le slot Rig que tu choisis ici. Mets le délai CAT au-dessus de 0 si ton poste perd des commandes envoyées coup sur coup (certains anciens Kenwood/Yaesu). OmniRig ne rapporte qu'un « DIG » générique pour les modes numériques — le mode numérique par défaut est le mode précis qu'OpsLog affichera (et loggera).",
+19 -4
View File
@@ -210,17 +210,25 @@ func (b *IcomSerial) Connect() error {
go b.netScopeFeeder(sc.ScopeChan(), b.readerDone) go b.netScopeFeeder(sc.ScopeChan(), b.readerDone)
} }
// Best-effort model identification: ask the rig for its own CI-V address. // Best-effort model identification: ask the rig for its own CI-V address. The
// 0x19 ID read returns the rig's FACTORY default address (e.g. 0x94 for an
// IC-7300) regardless of the address it's currently OPERATING on — so it's the
// reliable model signal even when the user runs the rig at a non-default CI-V
// address (a common trick to make CAT "just work" at 0x98).
idAddr := b.rigAddr // fallback: the configured address if the ID read fails
if err := b.write(civ.CmdReadID, civ.SubPTT); err == nil { if err := b.write(civ.CmdReadID, civ.SubPTT); err == nil {
if f, err := b.recv(icomReadTimeout, func(d civ.Decoded) bool { if f, err := b.recv(icomReadTimeout, func(d civ.Decoded) bool {
return d.Cmd == civ.CmdReadID && len(d.Data) >= 2 && d.Data[0] == 0x00 return d.Cmd == civ.CmdReadID && len(d.Data) >= 2 && d.Data[0] == 0x00
}); err == nil { }); err == nil {
b.model = civ.ModelName(f.Data[1]) b.model = civ.ModelName(f.Data[1])
idAddr = f.Data[1]
} }
} }
// Dual-scope rigs (IC-7610/9700) prefix each waveform frame with a main/sub // Dual-scope rigs (IC-7610/9700) prefix each waveform frame with a main/sub
// selector byte; single-scope rigs (IC-7300…) do not. // selector byte; single-scope rigs (IC-7300…) do not. Decide from the
b.dualScope = b.rigAddr == 0x98 || b.rigAddr == 0xA2 // IDENTIFIED model, NOT the configured address: an IC-7300 run at 0x98 must
// still parse single-scope frames (this was the "scope blank on the 7300" bug).
b.dualScope = idAddr == 0x98 || idAddr == 0xA2
// Defer the DSP snapshot until the rig actually answers CI-V. Over the network // Defer the DSP snapshot until the rig actually answers CI-V. Over the network
// the rig may still be booting (or off) at Connect, so an immediate readDSP // the rig may still be booting (or off) at Connect, so an immediate readDSP
// would time out and leave every control at 0 / off with no retry. ReadState // would time out and leave every control at 0 / off with no retry. ReadState
@@ -590,7 +598,7 @@ func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) {
} }
continue continue
} }
if rawN < 4 { if rawN < 24 {
rawN++ rawN++
applog.Printf("icom scope raw #%d: len=%d data=[% X]", rawN, len(f.Data), f.Data) applog.Printf("icom scope raw #%d: len=%d data=[% X]", rawN, len(f.Data), f.Data)
} }
@@ -696,6 +704,13 @@ func (b *IcomSerial) assembleSweep(regions map[byte][]byte, total byte) {
// and 0x27 0x11 turns the waveform data OUTPUT over CI-V on. While on, the reader // and 0x27 0x11 turns the waveform data OUTPUT over CI-V on. While on, the reader
// routes every 0x27 frame to scopeLoop. // routes every 0x27 frame to scopeLoop.
func (b *IcomSerial) SetScope(on bool) error { func (b *IcomSerial) SetScope(on bool) error {
if on {
// Context for the scope-diagnostic log: which rig + whether we expect the
// dual-scope (main/sub) frame layout. If the IC-7300 (single scope) streams
// but nothing shows, compare its `icom scope raw` frames against this.
applog.Printf("icom scope: enable on rig=%q addr=0x%02X dualScope=%v (expect %s frame layout)",
b.model, b.rigAddr, b.dualScope, map[bool]string{true: "27 00 [MS] [seq] [total] …", false: "27 00 [seq] [total] …"}[b.dualScope])
}
// Some firmwares don't ack 0x27 sets; a timeout here isn't fatal, so log and // Some firmwares don't ack 0x27 sets; a timeout here isn't fatal, so log and
// continue rather than abort the second command. // continue rather than abort the second command.
if err := b.exec(civ.CmdScope, civ.SubScopeOnOff, boolByte(on)); err != nil { if err := b.exec(civ.CmdScope, civ.SubScopeOnOff, boolByte(on)); err != nil {