From 9cf1984fb2e96f3b741b3667216c98456e41f7c9 Mon Sep 17 00:00:00 2001 From: rouggy Date: Thu, 27 Aug 2026 16:14:53 +0200 Subject: [PATCH] fix(icom): ask the radio which shape its scope commands take MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The IC-7851 rejects "27 11 01" and wants "27 11 00 01" — the main/sub selector its two scopes need. The shape was picked from a list of CI-V addresses, so an address not on it got the wrong one and the scope stayed blank with a rejection in the log and nothing on screen. A rejection is an unambiguous answer, so a rejected 0x27 set is now re-sent in the other shape and the winner remembered. A TIMEOUT deliberately is not retried: several firmwares never ack a 0x27 set, and treating silence as a wrong guess would flip a rig that was already working. --- changelog.json | 6 +++-- internal/cat/icomserial.go | 55 +++++++++++++++++++++++++++----------- 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/changelog.json b/changelog.json index 41c27fa..6f5d53e 100644 --- a/changelog.json +++ b/changelog.json @@ -4,11 +4,13 @@ "date": "", "en": [ "Each radio carries its own MY_RIG (Settings → CAT), written on every QSO made with it — ahead of the per-band station in Operating conditions, which says what you planned to use rather than which radio is keying. Left empty, nothing changes.", - "Worked-before matrix: a dot in the corner of a cell now means the callsign you are working has already been worked on that slot, whatever colour the entity status gave the cell. Its two colours (worked / confirmed with this callsign) are in Appearance with the other matrix colours." + "Worked-before matrix: a dot in the corner of a cell now means the callsign you are working has already been worked on that slot, whatever colour the entity status gave the cell. Its two colours (worked / confirmed with this callsign) are in Appearance with the other matrix colours.", + "Icom spectrum scope: the IC-7851 (and any radio that wants the main/sub selector on its scope commands) now works over CI-V — the command shape is worked out from the radio’s own answer instead of a list of models." ], "fr": [ "Chaque radio porte son propre MY_RIG (Réglages → CAT), inscrit sur chaque QSO fait avec elle — avant la station par bande des Conditions de trafic, qui dit ce qui était prévu et non quelle radio émet. Laissé vide, rien ne change.", - "Matrice des contacts : un point dans le coin d'une case indique que l'indicatif en cours a déjà été contacté sur ce créneau, quelle que soit la couleur donnée par le statut de l'entité. Ses deux couleurs (contacté / confirmé avec cet indicatif) se règlent dans Apparence avec les autres couleurs de la matrice." + "Matrice des contacts : un point dans le coin d'une case indique que l'indicatif en cours a déjà été contacté sur ce créneau, quelle que soit la couleur donnée par le statut de l'entité. Ses deux couleurs (contacté / confirmé avec cet indicatif) se règlent dans Apparence avec les autres couleurs de la matrice.", + "Scope Icom : l'IC-7851 (et toute radio qui attend le sélecteur main/sub sur les commandes de scope) fonctionne maintenant en CI-V — la forme de la commande est déduite de la réponse de la radio au lieu d'une liste de modèles." ] }, { diff --git a/internal/cat/icomserial.go b/internal/cat/icomserial.go index d44a604..f8b5390 100644 --- a/internal/cat/icomserial.go +++ b/internal/cat/icomserial.go @@ -982,6 +982,40 @@ func (b *IcomSerial) assembleSweep(regions map[byte][]byte, total byte) { } } +// execScope sends a 0x27 SET and, if the rig rejects it, sends it once more in +// the other shape — with or without the leading main/sub selector byte — and +// remembers which one this rig speaks. +// +// The shape used to be decided from the CI-V address, which meant every new +// model was a blank scope until someone reported it: the IC-7851 (0x8E) rejects +// "27 11 01" outright and wants "27 11 00 01", exactly as the IC-7610 does not. +// A rejection is a cheap and unambiguous answer, so ask the rig instead of +// keeping a list. Only the SET commands need this — the waveform parser already +// detects the selector per frame. +func (b *IcomSerial) execScope(what string, sub byte, args ...byte) error { + try := func(sel bool) error { + p := []byte{civ.CmdScope, sub} + if sel { + p = append(p, 0x00) // main scope + } + return b.exec(append(p, args...)...) + } + err := try(b.dualScope) + // Only a REJECTION means "wrong shape". A timeout says nothing (several + // firmwares simply don't ack a 0x27 set), and retrying it in the other shape + // would flip a working rig onto the wrong one. + if err == nil || !strings.Contains(err.Error(), "rejected") { + return err + } + if err2 := try(!b.dualScope); err2 == nil { + b.dualScope = !b.dualScope + applog.Printf("icom scope: %s rejected — this rig wants the %s form (selector=%v)", + what, map[bool]string{true: "27 xx 00 …", false: "27 xx …"}[b.dualScope], b.dualScope) + return nil + } + return err +} + // SetScope enables or disables the spectrum scope. Two commands are needed and // RS-BA1 sends both: 0x27 0x10 turns the scope DISPLAY on (without it the rig // streams nothing — the case when we're remote and can't touch the front panel), @@ -1000,14 +1034,14 @@ 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.exec(civ.CmdScope, 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) } } // 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.exec(civ.CmdScope, civ.SubScopeOn, boolByte(on)); err != nil { + if err := b.execScope("data output", civ.SubScopeOn, boolByte(on)); err != nil { applog.Printf("icom scope: output on=%v ack: %v", on, err) } b.scopeMu.Lock() @@ -1041,13 +1075,7 @@ func (b *IcomSerial) scopeReadCfg() { // makes the scope follow the VFO, so tuning pans the view left/right. func (b *IcomSerial) SetScopeMode(fixed bool) error { mode := boolByte(fixed) // 0 = center, 1 = fixed (verify on rig via the cfg log) - var payload []byte - if b.dualScope { - payload = []byte{civ.CmdScope, civ.SubScopeMode, 0x00, mode} - } else { - payload = []byte{civ.CmdScope, civ.SubScopeMode, mode} - } - if err := b.exec(payload...); err != nil { + if err := b.execScope("set mode", civ.SubScopeMode, mode); err != nil { applog.Printf("icom scope: set mode fixed=%v ack: %v", fixed, err) } b.scopeMu.Lock() @@ -1093,13 +1121,8 @@ func (b *IcomSerial) SetScopeEdges(low, high int64) error { if rangeID == 0 { return fmt.Errorf("icom scope: freq out of range") } - if b.dualScope { - _ = b.exec(civ.CmdScope, civ.SubScopeMode, 0x00, 0x01) // fixed mode (main) - _ = b.exec(civ.CmdScope, civ.SubScopeEdge, 0x00, 0x01) // activate edge set 1 - } else { - _ = b.exec(civ.CmdScope, civ.SubScopeMode, 0x01) - _ = b.exec(civ.CmdScope, civ.SubScopeEdge, 0x01) - } + _ = b.execScope("fixed mode", civ.SubScopeMode, 0x01) + _ = b.execScope("edge set 1", civ.SubScopeEdge, 0x01) payload := append([]byte{civ.CmdScope, civ.SubScopeFixEdge, rangeID, 0x01}, civ.FreqToBCD(low)...) payload = append(payload, civ.FreqToBCD(high)...) b.scopeMu.Lock()