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": "",
"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 radios 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."
]
},
{
+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
// 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()