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.
93 lines
3.5 KiB
Go
93 lines
3.5 KiB
Go
package cat
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// The rule that decides whether a silent-but-alive Icom network session is
|
|
// tolerated or torn down. It was "always tolerate", and that is what froze the
|
|
// frequency display for ever when another program took the CI-V session.
|
|
//
|
|
// The decision is pinned here rather than left implicit, because the two cases
|
|
// it separates pull in opposite directions: a rig in standby must NOT be
|
|
// reconnected every few seconds (the panel and its ON button would blink away),
|
|
// while a hijacked session must recover without restarting OpsLog.
|
|
func TestIcomSilenceTolerance(t *testing.T) {
|
|
// tolerate mirrors the condition in ReadState.
|
|
tolerate := func(alive, readerGone bool, lastGood time.Time, silentFor, grace time.Duration) bool {
|
|
return alive && !readerGone && (lastGood.IsZero() || silentFor < grace)
|
|
}
|
|
never := time.Time{}
|
|
some := time.Now()
|
|
|
|
cases := []struct {
|
|
name string
|
|
alive bool
|
|
readerGone bool
|
|
lastGood time.Time
|
|
silentFor time.Duration
|
|
want bool
|
|
}{
|
|
{"never answered since connect — rig is off, keep the panel", true, false, never, time.Hour, true},
|
|
{"brief silence during a band change", true, false, some, 2 * time.Second, true},
|
|
{"silence past the grace — reconnect", true, false, some, icomSilentGrace + time.Second, false},
|
|
{"reader goroutine gone — dead however alive the link looks", true, true, some, time.Second, false},
|
|
{"control link itself dead", false, false, some, time.Second, false},
|
|
}
|
|
for _, c := range cases {
|
|
got := tolerate(c.alive, c.readerGone, c.lastGood, c.silentFor, icomSilentGrace)
|
|
if got != c.want {
|
|
t.Errorf("%s: tolerate = %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The backoff must actually converge on the ceiling: a rig left switched off is
|
|
// silent for hours, and a window that stayed at 30 s would re-tear its session
|
|
// about a hundred times an hour.
|
|
func TestIcomSilenceBackoff(t *testing.T) {
|
|
g := icomSilentGrace
|
|
for i := 0; i < 20; i++ {
|
|
if g < icomSilentGraceMax {
|
|
g *= 2
|
|
}
|
|
}
|
|
if g < icomSilentGraceMax {
|
|
t.Fatalf("backoff never reached the ceiling: %s < %s", g, icomSilentGraceMax)
|
|
}
|
|
if g > 2*icomSilentGraceMax {
|
|
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")
|
|
}
|
|
}
|