From 69d78ba8c0926ea885a1af3e0dd2e1fb3d1e1761 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 27 Aug 2026 16:18:55 +0200 Subject: [PATCH] fix(icom): fall back to 0x27 0x10 when a rig has no 0x27 0x11 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IC-7851 rejects 0x27 0x11 in BOTH shapes: on that radio 0x27 0x10 is the waveform-output switch itself, not a display switch. Rejected both ways, take 0x10 as the output command and switch it off on disable too — there it stops a stream rather than blanking the operator's own scope screen. A mode/span/edge answer also settles the selector question directly: three bytes or more means the rig wants the main/sub form. Worth reading, because a wrong-shaped SET is answered with silence on this firmware, and a timeout is something the rejection retry cannot act on — which is why the fixed/centre sets were timing out. --- internal/cat/icomserial.go | 57 ++++++++++++++++++++++++++++++-------- 1 file changed, 45 insertions(+), 12 deletions(-) diff --git a/internal/cat/icomserial.go b/internal/cat/icomserial.go index f8b5390..7a69484 100644 --- a/internal/cat/icomserial.go +++ b/internal/cat/icomserial.go @@ -93,15 +93,18 @@ type IcomSerial struct { // leading main/sub selector byte (IC-7610/9700). scopeAmp is the latest // reassembled sweep; scopeMu guards it (written by the scope goroutine, read // via ScopeData from the binding goroutine). - dualScope bool - scopeMu sync.Mutex - scopeAmp []byte - scopeLow int64 // spectrum left-edge frequency (from the sweep's header frame) - scopeHigh int64 // spectrum right-edge frequency - scopeSeq int - scopeOn bool - scopeFixed bool // true = fixed-span mode (tracked optimistically) - scopeSeen bool // logged the first sweep's structure once (on-rig verification) + dualScope bool + // Which 0x27 subcommand switches the waveform stream on this rig: 0x11 where + // it exists, 0x10 on the ones that answer 0x11 with a rejection. + scopeOutSub byte + scopeMu sync.Mutex + scopeAmp []byte + scopeLow int64 // spectrum left-edge frequency (from the sweep's header frame) + scopeHigh int64 // spectrum right-edge frequency + scopeSeq int + scopeOn bool + scopeFixed bool // true = fixed-span mode (tracked optimistically) + scopeSeen bool // logged the first sweep's structure once (on-rig verification) curFreq int64 // last frequency read (for sideband choice) curModeByte byte // last raw Icom mode byte (for filter re-send) @@ -848,6 +851,19 @@ func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) { loggedCfg[f.Data[0]] = true applog.Printf("icom scope cfg 0x%02X: data=[% X]", f.Data[0], f.Data) } + // The rig just told us its own layout: a mode/span/edge answer of + // three bytes or more carries the main/sub selector, one of two + // bytes does not. Worth reading, because the SET commands take the + // same shape and several firmwares answer a wrong-shaped set with + // silence rather than a rejection — which is not something the + // retry in execScope can act on. + if f.Data[0] == civ.SubScopeMode && len(f.Data) >= 2 { + if sel := len(f.Data) >= 3; sel != b.dualScope { + applog.Printf("icom scope: the rig answers 0x%02X with %d bytes — using the %s form", + f.Data[0], len(f.Data)-1, map[bool]string{true: "27 xx 00 …", false: "27 xx …"}[sel]) + b.dualScope = sel + } + } continue } if rawN < 24 { @@ -1034,14 +1050,31 @@ func (b *IcomSerial) SetScope(on bool) error { // radio, and closing OpsLog (SetScope(false)) blanking a local IC-7300's // screen is exactly the regression this avoids. Some firmwares don't ack a // 0x27 set; a timeout isn't fatal, so log and continue. - if err := b.execScope("display on", civ.SubScopeOnOff, 0x01); err != nil { - applog.Printf("icom scope: display on ack: %v", err) + if b.scopeOutSub != civ.SubScopeOnOff { + if err := b.execScope("display on", civ.SubScopeOnOff, 0x01); err != nil { + applog.Printf("icom scope: display on ack: %v", err) + } } } // Waveform data OUTPUT over CI-V: enabled with the scope, and — crucially — // the ONLY thing we switch off on disable, so the radio's own scope display is // left exactly as the operator had it. - if err := b.execScope("data output", civ.SubScopeOn, boolByte(on)); err != nil { + // + // scopeOutSub is 0x11 on the radios that have it. The IC-7851 does not: it + // rejects 0x11 in both shapes, because on that radio 0x27 0x10 is itself the + // waveform-output switch rather than a display switch. Fall back to it, and + // from then on switch it off on disable too — on THAT radio doing so stops a + // data stream, not the operator's own scope screen. + if b.scopeOutSub == 0 { + b.scopeOutSub = civ.SubScopeOn + } + err := b.execScope("data output", b.scopeOutSub, boolByte(on)) + if err != nil && b.scopeOutSub == civ.SubScopeOn && strings.Contains(err.Error(), "rejected") { + applog.Printf("icom scope: no 0x27 0x11 on this rig — 0x27 0x10 is its data-output switch") + b.scopeOutSub = civ.SubScopeOnOff + err = b.execScope("data output", b.scopeOutSub, boolByte(on)) + } + if err != nil { applog.Printf("icom scope: output on=%v ack: %v", on, err) } b.scopeMu.Lock()