From b9079fe4e2d4b032d93a34b85553589ead73c9a1 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 9 Jul 2026 15:30:24 +0200 Subject: [PATCH] fix: bug where scope is now showing on IC7300 --- frontend/src/App.tsx | 10 ++++- frontend/src/components/IcomPanel.tsx | 45 +++++++++++++++++------ frontend/src/components/SettingsModal.tsx | 42 ++++++++++++++++++++- frontend/src/lib/i18n.tsx | 4 +- internal/cat/icomserial.go | 23 ++++++++++-- 5 files changed, 102 insertions(+), 22 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 4694dc5..ad54df2 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -366,6 +366,11 @@ export default function App() { // CAT — receives live rig state via Wails events. const [catState, setCatState] = useState({ 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 [ubStatus, setUbStatus] = useState<{ enabled: boolean; connected: boolean; direction: number; moving: boolean }>({ enabled: false, connected: false, direction: 0, moving: false }); const [agStatus, setAgStatus] = useState({ connected: false, port_a: 0, port_b: 0, antennas: [] }); @@ -1352,6 +1357,7 @@ export default function App() { try { const c = await GetCATSettings(); if (c.digital_default) digitalDefaultRef.current = c.digital_default; + setCatBackend(c.backend ?? ''); } catch {} }, []); const loadLists = useCallback(async () => { @@ -3154,7 +3160,7 @@ export default function App() { case 'icom': return (
- { setRstSent(r); rstUserEditedRef.current = true; }} /> + { setRstSent(r); rstUserEditedRef.current = true; }} />
); case 'netcontrol': @@ -4245,7 +4251,7 @@ export default function App() { is an Icom. */} {catState.backend === 'icom' && ( - { setRstSent(r); rstUserEditedRef.current = true; }} /> + { setRstSent(r); rstUserEditedRef.current = true; }} /> )} diff --git a/frontend/src/components/IcomPanel.tsx b/frontend/src/components/IcomPanel.tsx index 6de30cb..8c633c0 100644 --- a/frontend/src/components/IcomPanel.tsx +++ b/frontend/src/components/IcomPanel.tsx @@ -58,6 +58,21 @@ const BANDS: { l: string; hz: number }[] = [ // SSB by frequency and the rig's data variant for digital modes. 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: // MHz "." 3-digit-kHz "." 2-digit-(10 Hz). 21032000 → "21.032.00". 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 // read every cache cycle; DSP set-controls are optimistic and reconcile on the // 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 [st, setSt] = useState(ZERO); const [cat, setCat] = useState(null); // RigState (freq/mode/split) for the VFO display @@ -637,16 +652,22 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void {st.split ? Split : null}
- {/* Radio power ON / OFF. Manual by design — the app never wakes the rig - on connect; ON sends the wake preamble then the rig boots ~15 s. */} - - + {/* Radio power ON / OFF — NETWORK only. Over USB the CI-V interface is + 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 && ( + <> + + + + )}
@@ -804,7 +825,7 @@ export function IcomPanel({ onReportRST }: { onReportRST?: (rst: string) => void onChange={(v) => set({ preamp: parseInt(v) }, () => IcomSetPreamp(parseInt(v)))} /> - set({ att: parseInt(v) }, () => IcomSetAtt(parseInt(v)))} /> diff --git a/frontend/src/components/SettingsModal.tsx b/frontend/src/components/SettingsModal.tsx index aea693c..e97488c 100644 --- a/frontend/src/components/SettingsModal.tsx +++ b/frontend/src/components/SettingsModal.tsx @@ -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) { const { t } = useI18n(); const [selected, setSelected] = useState((initialSection as SectionId) || 'station'); @@ -2054,12 +2068,24 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan -
+
+ + +
+
{ 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 })); }} /> -

{t('cat.civHint')}

+

{t('cat.civHint')}

)} {catCfg.backend === 'icom-net' && ( @@ -2069,6 +2095,18 @@ export function SettingsModal({ onClose, onSaved, initialSection, onMainPaneChan setCatCfg((s) => ({ ...s, icom_net_host: e.target.value }))} />
+
+ + +
= 2 && d.Data[0] == 0x00 }); err == nil { 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 - // selector byte; single-scope rigs (IC-7300…) do not. - b.dualScope = b.rigAddr == 0x98 || b.rigAddr == 0xA2 + // selector byte; single-scope rigs (IC-7300…) do not. Decide from the + // 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 // 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 @@ -590,7 +598,7 @@ func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) { } continue } - if rawN < 4 { + if rawN < 24 { rawN++ 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 // routes every 0x27 frame to scopeLoop. 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 // continue rather than abort the second command. if err := b.exec(civ.CmdScope, civ.SubScopeOnOff, boolByte(on)); err != nil {