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:
2026-08-14 17:37:12 +02:00
parent 7470ebf47e
commit c5c9d355ec
5 changed files with 141 additions and 3 deletions
+55
View File
@@ -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.