fix(logging): name the antenna disagreement, and confirm an outbound send
Two silences from one operator's log. The SteppIR is not blocked — it is being overruled. Every tune is commanded, acknowledged on the next poll with the frequency asked for, then replaced on the poll after by a different one: 21075 asked, 21075 confirmed, 21050 reported, again and again, with the operator tuning once more each time. Two minutes of that reads as a dead link. It now says what it is, once, when the motors have stopped and the reported frequency is more than 10 kHz from the commanded one — a controller quantising to its own grid is not a fault, one sitting somewhere else entirely is. UDP outbound had no positive signal at all: only failures were logged. When an operator sets up an ADIF message to a second logger and nothing arrives, "OpsLog never sent it" and "the other program did not take it" looked exactly the same from here — and the first is the common case, because a row created as an inbound ADIF listener instead of an outbound ADIF message matches nothing and emits nothing. Successful sends are logged with their destination and service, and a QSO logged with no outbound row says so once a session.
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
package steppir
|
||||
|
||||
import "testing"
|
||||
|
||||
// An operator's log read as "the antenna stops responding": every tune was
|
||||
// commanded, acknowledged with the requested frequency on the next poll, then
|
||||
// replaced on the poll after by a different one — 21075 asked, 21075 confirmed,
|
||||
// 21050 reported — with the operator tuning again each time. Nothing in the log
|
||||
// named that, so it looked like a dead link.
|
||||
func TestCheckDriftReportsOnceAndRecovers(t *testing.T) {
|
||||
c := &Client{}
|
||||
c.lastSetKHz = 21075
|
||||
|
||||
// Where it was asked to go: nothing to say.
|
||||
c.checkDrift(&Status{Frequency: 21075})
|
||||
if c.lastDriftKHz != 0 {
|
||||
t.Errorf("a controller on frequency was reported as drifting (%d)", c.lastDriftKHz)
|
||||
}
|
||||
// Its own grid is not a fault.
|
||||
c.checkDrift(&Status{Frequency: 21070})
|
||||
if c.lastDriftKHz != 0 {
|
||||
t.Errorf("%d kHz is within tolerance and was reported", 21070)
|
||||
}
|
||||
// Somewhere else entirely: recorded, so it is said once.
|
||||
c.checkDrift(&Status{Frequency: 21050})
|
||||
if c.lastDriftKHz != 21050 {
|
||||
t.Errorf("lastDriftKHz = %d, want 21050", c.lastDriftKHz)
|
||||
}
|
||||
// Still there on the next poll: nothing new to say, and the marker stands.
|
||||
c.checkDrift(&Status{Frequency: 21050})
|
||||
if c.lastDriftKHz != 21050 {
|
||||
t.Errorf("lastDriftKHz = %d after a repeat, want it unchanged", c.lastDriftKHz)
|
||||
}
|
||||
// Back where it belongs: the marker clears, so the next divergence is said.
|
||||
c.checkDrift(&Status{Frequency: 21075})
|
||||
if c.lastDriftKHz != 0 {
|
||||
t.Errorf("lastDriftKHz = %d after recovery, want 0", c.lastDriftKHz)
|
||||
}
|
||||
}
|
||||
|
||||
// A frequency read while the elements are travelling is not a disagreement, it
|
||||
// is an antenna on its way — reporting it would cry wolf on every single tune.
|
||||
func TestCheckDriftIgnoresMovingMotors(t *testing.T) {
|
||||
c := &Client{}
|
||||
c.lastSetKHz = 21075
|
||||
c.checkDrift(&Status{Frequency: 21000, MotorsMoving: 1})
|
||||
if c.lastDriftKHz != 0 {
|
||||
t.Errorf("a moving antenna was reported as drifting (%d)", c.lastDriftKHz)
|
||||
}
|
||||
}
|
||||
@@ -102,6 +102,10 @@ type Client struct {
|
||||
statusMu sync.RWMutex
|
||||
lastStatus *Status
|
||||
lastSetKHz int
|
||||
// lastDriftKHz is the frequency last reported for a controller that had gone
|
||||
// somewhere other than where it was told, so the disagreement is stated once
|
||||
// and not on every poll. Zero when it is where it should be.
|
||||
lastDriftKHz int
|
||||
|
||||
// lastRaw holds the previous raw status frame so we only log a status line
|
||||
// when the controller's reply actually changes — enough to diagnose a stuck
|
||||
@@ -269,10 +273,61 @@ func (c *Client) pollLoop() {
|
||||
c.applyPendingDir(st)
|
||||
c.lastStatus = st
|
||||
c.statusMu.Unlock()
|
||||
c.checkDrift(st)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// driftToleranceKHz is how far the controller's reported frequency may sit from
|
||||
// the one we commanded before it is worth saying so. A SteppIR quantises to its
|
||||
// own grid, so a few kHz is normal and must not be reported as a fault.
|
||||
const driftToleranceKHz = 10
|
||||
|
||||
// checkDrift reports a controller that settled somewhere other than where it was
|
||||
// told to go.
|
||||
//
|
||||
// This exists because of a log that read as "the antenna stops responding": every
|
||||
// tune was commanded, acknowledged on the next poll with the requested frequency,
|
||||
// and then REPLACED on the poll after by a different one — 21075 asked, 21075
|
||||
// confirmed, 21050 reported, over and over, with the operator tuning again each
|
||||
// time. Nothing in the log named that, so it looked like a dead link rather than
|
||||
// a controller with a mind of its own (its own radio interface tracking the rig,
|
||||
// or a front-panel mode that overrides the host).
|
||||
//
|
||||
// Only when the motors have stopped: a frequency read mid-travel is not a
|
||||
// disagreement, it is an antenna on its way. And only when the value changes, so
|
||||
// a controller parked somewhere else does not fill the log.
|
||||
func (c *Client) checkDrift(st *Status) {
|
||||
if st.MotorsMoving != 0 {
|
||||
return
|
||||
}
|
||||
c.statusMu.Lock()
|
||||
want := c.lastSetKHz
|
||||
last := c.lastDriftKHz
|
||||
if want <= 0 || st.Frequency <= 0 {
|
||||
c.statusMu.Unlock()
|
||||
return
|
||||
}
|
||||
diff := st.Frequency - want
|
||||
if diff < 0 {
|
||||
diff = -diff
|
||||
}
|
||||
if diff <= driftToleranceKHz {
|
||||
c.lastDriftKHz = 0 // back where it was asked to be
|
||||
c.statusMu.Unlock()
|
||||
return
|
||||
}
|
||||
if last == st.Frequency {
|
||||
c.statusMu.Unlock()
|
||||
return // already said, and it has not moved since
|
||||
}
|
||||
c.lastDriftKHz = st.Frequency
|
||||
c.statusMu.Unlock()
|
||||
log.Printf("steppir: commanded %d kHz but the controller settled on %d kHz — "+
|
||||
"something else is driving it (its own radio interface, or a front-panel mode overriding the host)",
|
||||
want, st.Frequency)
|
||||
}
|
||||
|
||||
// applyPendingDir replaces a freshly polled direction with the one the operator
|
||||
// last commanded, until the controller confirms it (or the hold expires). The
|
||||
// caller holds statusMu.
|
||||
|
||||
Reference in New Issue
Block a user