fix(icom): ask the radio which shape its scope commands take

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.
This commit is contained in:
2026-08-27 16:14:53 +02:00
parent ee8fd32bf7
commit 9cf1984fb2
2 changed files with 43 additions and 18 deletions
+4 -2
View File
@@ -4,11 +4,13 @@
"date": "", "date": "",
"en": [ "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.", "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 radios own answer instead of a list of models."
], ],
"fr": [ "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.", "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."
] ]
}, },
{ {
+39 -16
View File
@@ -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 // 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 // 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), // 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 // 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 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) 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.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) applog.Printf("icom scope: output on=%v ack: %v", on, err)
} }
b.scopeMu.Lock() b.scopeMu.Lock()
@@ -1041,13 +1075,7 @@ func (b *IcomSerial) scopeReadCfg() {
// makes the scope follow the VFO, so tuning pans the view left/right. // makes the scope follow the VFO, so tuning pans the view left/right.
func (b *IcomSerial) SetScopeMode(fixed bool) error { func (b *IcomSerial) SetScopeMode(fixed bool) error {
mode := boolByte(fixed) // 0 = center, 1 = fixed (verify on rig via the cfg log) mode := boolByte(fixed) // 0 = center, 1 = fixed (verify on rig via the cfg log)
var payload []byte if err := b.execScope("set mode", civ.SubScopeMode, mode); err != nil {
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 {
applog.Printf("icom scope: set mode fixed=%v ack: %v", fixed, err) applog.Printf("icom scope: set mode fixed=%v ack: %v", fixed, err)
} }
b.scopeMu.Lock() b.scopeMu.Lock()
@@ -1093,13 +1121,8 @@ func (b *IcomSerial) SetScopeEdges(low, high int64) error {
if rangeID == 0 { if rangeID == 0 {
return fmt.Errorf("icom scope: freq out of range") return fmt.Errorf("icom scope: freq out of range")
} }
if b.dualScope { _ = b.execScope("fixed mode", civ.SubScopeMode, 0x01)
_ = b.exec(civ.CmdScope, civ.SubScopeMode, 0x00, 0x01) // fixed mode (main) _ = b.execScope("edge set 1", civ.SubScopeEdge, 0x01)
_ = 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)
}
payload := append([]byte{civ.CmdScope, civ.SubScopeFixEdge, rangeID, 0x01}, civ.FreqToBCD(low)...) payload := append([]byte{civ.CmdScope, civ.SubScopeFixEdge, rangeID, 0x01}, civ.FreqToBCD(low)...)
payload = append(payload, civ.FreqToBCD(high)...) payload = append(payload, civ.FreqToBCD(high)...)
b.scopeMu.Lock() b.scopeMu.Lock()