fix(icom net): a sleeping rig keeps its session, and the console is reachable

From an operator's log: an IC-7760 in standby, and OpsLog dialling and
dropping every forty seconds for as long as it was left there — control
link up, login OK, token renewed, and not one CI-V answer.

lastGoodAt bounds "the link answers but no CI-V comes back". It belongs
to a session and was never cleared when a new one opened, so a rig that
went to standby half an hour ago handed every fresh session a
half-hour-old last good read: past the grace before the first command was
even sent. Torn down at once, redialled twenty seconds later, torn down
again. Cleared on connect, the rule reads as it was written — silent
since connect is a rig in standby, and the session is kept so it can be
woken.

The console it is woken from was missing too. It appeared only once the
live CAT state said "icom", which a sleeping rig never says, so the ON
button was absent at the one moment it exists for. It now follows the
CONFIGURED radio — which also had to start following a radio switched
from the status bar, instead of waiting for a trip through Settings and a
Save that changed nothing.
This commit is contained in:
2026-09-06 17:35:53 +02:00
parent 381a7fdc40
commit 507d1f0882
4 changed files with 79 additions and 7 deletions
+30
View File
@@ -60,3 +60,33 @@ func TestIcomSilenceBackoff(t *testing.T) {
t.Errorf("backoff overshot the ceiling: %s", g)
}
}
// A new session must not be judged on the previous one's silence.
//
// From an operator's log: a rig switched to standby, then a dial-and-drop loop
// every forty seconds for as long as it was left there. lastGoodAt bounds "the
// link answers but no CI-V comes back"; it belongs to a session, and it was
// never cleared when a new one opened, so every fresh session started already
// past the grace — and the Icom console, where the power-ON button lives,
// blinked away on every pass.
func TestAFreshSessionStartsWithACleanSilenceClock(t *testing.T) {
b := &IcomSerial{
lastGoodAt: time.Now().Add(-30 * time.Minute), // a session from before standby
readFails: 9,
silentGrace: icomSilentGraceMax,
}
// What Connect does once the transport is open, before anything is sent.
b.lastGoodAt = time.Time{}
b.readFails = 0
b.silentGrace = icomSilentGrace
if !b.lastGoodAt.IsZero() {
t.Fatal("the previous session's last good read survived into this one")
}
tolerate := func(lastGood time.Time, silentFor, grace time.Duration) bool {
return lastGood.IsZero() || silentFor < grace
}
if !tolerate(b.lastGoodAt, time.Hour, b.silentGrace) {
t.Error("a session silent since connect was torn down — that is a rig in standby, and where the ON button has to work")
}
}
+15
View File
@@ -232,6 +232,21 @@ func (b *IcomSerial) Connect() error {
_ = port.SetRTS(false)
b.port = port
b.model = civ.ModelName(b.rigAddr)
// A NEW SESSION IS NOT JUDGED ON THE OLD ONE'S SILENCE.
//
// lastGoodAt bounds "the control link answers but no CI-V comes back". It
// belongs to a session, and it was never cleared when a new one opened —
// so a rig that went to standby half an hour ago handed every fresh session
// a half-hour-old "last good read", which is past the grace before the first
// command is even sent. The session was torn down at once, redialled twenty
// seconds later, and torn down again: a loop with no way out, and the Icom
// console (with its power-ON button) blinking away on every pass.
//
// Cleared, the rule reads as it was written: silent since connect is a rig in
// standby, and the session is kept so the operator can wake it.
b.lastGoodAt = time.Time{}
b.readFails = 0
b.silentGrace = icomSilentGrace
// Start the reader before any request: recv() now waits on respCh, which only
// the reader feeds. respCh is buffered so a burst (or the scope stream) never