The network backend treated "control link alive but no CI-V reply" as the rig being in standby, and tolerated it WITHOUT BOUND. When another program takes the CI-V session — WSJT-X through OmniRig, or the Remote Utility — the rig goes on answering pings on the control stream while sending us nothing at all. Alive() stayed true, so ReadState returned the cached frequency with err == nil, the Manager never saw a failure, never reconnected, and re-published a frozen number with a fresh timestamp on every poll. Only restarting OpsLog cleared it. Bounded now, on the last SUCCESSFUL read. Past the grace the error is reported so the Manager tears the session down and reconnects, which re-takes the CI-V stream. Also fatal immediately: the CI-V reader goroutine having exited — no read can ever succeed after that, however healthy the control link looks. The grace backs off to minutes when the silence persists, because the two cases pull opposite ways: a stolen session recovers on the first attempt, while a rig switched OFF is silent for hours and re-tearing its session every 30 s would blink the panel — and its ON button — away continuously. A good read resets it. The decision table is pinned by a test; the standby case (never answered since connect) keeps the old tolerate-for-ever behaviour.
63 lines
2.2 KiB
Go
63 lines
2.2 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)
|
|
}
|
|
}
|