fix(icom): fall back to 0x27 0x10 when a rig has no 0x27 0x11

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.
This commit is contained in:
2026-08-27 16:18:55 +02:00
parent 9cf1984fb2
commit 69d78ba8c0
+34 -1
View File
@@ -94,6 +94,9 @@ type IcomSerial struct {
// reassembled sweep; scopeMu guards it (written by the scope goroutine, read // reassembled sweep; scopeMu guards it (written by the scope goroutine, read
// via ScopeData from the binding goroutine). // via ScopeData from the binding goroutine).
dualScope bool 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 scopeMu sync.Mutex
scopeAmp []byte scopeAmp []byte
scopeLow int64 // spectrum left-edge frequency (from the sweep's header frame) scopeLow int64 // spectrum left-edge frequency (from the sweep's header frame)
@@ -848,6 +851,19 @@ func (b *IcomSerial) scopeLoop(spec chan civ.Decoded, done chan struct{}) {
loggedCfg[f.Data[0]] = true loggedCfg[f.Data[0]] = true
applog.Printf("icom scope cfg 0x%02X: data=[% X]", f.Data[0], f.Data) 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 continue
} }
if rawN < 24 { 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 // 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 // screen is exactly the regression this avoids. Some firmwares don't ack a
// 0x27 set; a timeout isn't fatal, so log and continue. // 0x27 set; a timeout isn't fatal, so log and continue.
if b.scopeOutSub != civ.SubScopeOnOff {
if err := b.execScope("display on", civ.SubScopeOnOff, 0x01); err != nil { if err := b.execScope("display on", civ.SubScopeOnOff, 0x01); err != nil {
applog.Printf("icom scope: display on ack: %v", err) applog.Printf("icom scope: display on ack: %v", err)
} }
} }
}
// Waveform data OUTPUT over CI-V: enabled with the scope, and — crucially — // 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 // the ONLY thing we switch off on disable, so the radio's own scope display is
// left exactly as the operator had it. // 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) applog.Printf("icom scope: output on=%v ack: %v", on, err)
} }
b.scopeMu.Lock() b.scopeMu.Lock()