fix(logging): say why the antenna stopped and what the ADIF port is receiving

Two problems from one operator's log, both of them the log's fault.

SteppIR. The last line about it is "status query failed, reconnecting: Port
has been closed" and then nothing — for the rest of the session. The poll loop
does retry every two seconds, but a failed open was a bare `continue`: no line,
ever. And startUltrabeam had three silent ways out — settings unreadable, the
antenna turned off, no port or host configured — so a cleared field and a lost
adapter produced exactly the same evidence, which is none. Both now say what
happened, with the port or host named. Reopen failures are reported three
times and then throttled (a port that is gone stays gone), and the recovery
says how many attempts it took.

FLDIGI. Four hundred "ADIF payload ignored" lines and nothing else legible.
The cause is in the same log: two inbound listeners on the SAME multicast
group and port, 239.255.0.1:2237 — one WSJT (MSHV), one ADIF (FLDIGI). Every
WSJT-X packet is delivered to both, and an FT8 cycle is dozens of decodes
every fifteen seconds; the bursts in the log are exactly 15 s apart. The ADIF
listener now checks for the WSJT-X magic and says so once, with what to do
about it, instead of rejecting each packet in writing. Anything else is
described a few times with its payload and then goes quiet.
This commit is contained in:
2026-08-14 16:24:06 +02:00
parent 61006d8155
commit 2cd1274975
5 changed files with 151 additions and 5 deletions
+44
View File
@@ -88,6 +88,10 @@ type Client struct {
connMu sync.Mutex
conn io.ReadWriteCloser
// openFails counts consecutive failures to reopen the port, so the retry
// reports the first one and the recovery, and stays quiet in between.
// Guarded by connMu.
openFails int
// ioMu serialises EVERY exchange on the shared connection — a status query
// (write "?A" then read 11 bytes) and a command write must never interleave,
@@ -187,6 +191,35 @@ func (c *Client) open() (io.ReadWriteCloser, error) {
}
}
// openFailQuiet is how many consecutive failed reopens are reported before the
// loop goes quiet about them. A port that has gone (adapter unplugged, another
// program holding it) stays gone, and one line every two seconds would be the
// entire log.
const openFailQuiet = 3
// noteOpenFailure logs a failure to reopen the port, throttled. Caller must NOT
// hold connMu — it is taken here.
func (c *Client) noteOpenFailure(err error) {
c.connMu.Lock()
c.openFails++
n := c.openFails
c.connMu.Unlock()
switch {
case n <= openFailQuiet:
log.Printf("steppir: cannot open %s: %v (attempt %d)", c.target(), err, n)
case n == openFailQuiet+1:
log.Printf("steppir: still cannot open %s — retrying every 2 s, further attempts will not be logged until it comes back", c.target())
}
}
// target names what the client is trying to reach, for the log.
func (c *Client) target() string {
if c.tr.Mode == "serial" {
return fmt.Sprintf("%s @ %d baud", c.tr.COM, c.tr.Baud)
}
return fmt.Sprintf("%s:%d", c.tr.Host, c.tr.Port)
}
func (c *Client) pollLoop() {
ticker := time.NewTicker(2 * time.Second)
defer ticker.Stop()
@@ -201,8 +234,19 @@ func (c *Client) pollLoop() {
if err != nil {
c.connMu.Unlock()
c.setDisconnected()
// SAY SO. This retried every two seconds in complete silence,
// so an antenna that lost its port stopped answering and the
// log had nothing at all after the disconnection — which is
// exactly what an operator reports as "it worked for a while
// and then it didn't". Throttled, because a port that is gone
// stays gone and this would otherwise be the whole log.
c.noteOpenFailure(err)
continue
}
if c.openFails > 0 {
log.Printf("steppir: reconnected after %d failed attempt(s)", c.openFails)
c.openFails = 0
}
c.conn = conn
}
c.connMu.Unlock()