fix: release TX when the elements stop; WSJT-X colours, and worked beats the watch list

The motorised antenna gagged the transmitter for a second or two after it
had finished moving. Three delays were stacked: the antenna polled every
two seconds, the gag held for three after the command whatever the
antenna said, and the widget refreshed every three. The antenna is now
asked four times a second WHILE IT MOVES — an idle one has nothing to say
and stays at two seconds — the gag only bridges the command itself
(900 ms), and the widget follows at half a second while moving.

WSJT-X highlighting:

- A watch-list station already worked on this band and mode is no longer
  painted as one to call. The list is a statement of intent, not of what
  is left to do, and its pink outranked every other verdict including the
  log's, so a worked station stayed pink for the session with nothing to
  tell it from one still needed.
- The four colours are the operator's to choose (Settings → UDP). Only
  the background: the text colour is derived by luma, so a dark blue
  cannot come back as black-on-black in somebody else's window. Changing
  one clears the installed highlights, or the de-duplication would keep
  showing yesterday's colour until a callsign changed verdict.
This commit is contained in:
2026-09-06 18:43:08 +02:00
parent 07ee48e20c
commit d001616767
11 changed files with 306 additions and 31 deletions
+44 -1
View File
@@ -134,6 +134,23 @@ type Client struct {
// clears rather than latching the TX-inhibit on for ever.
const ubMoveOptimisticWindow = 3 * time.Second
// Poll cadences: what an idle antenna is worth, and what a moving one is worth.
// See the poll loop.
const (
ubPollIdle = 2 * time.Second
ubPollMoving = 250 * time.Millisecond
)
// movingOptimistically reports whether a move was commanded so recently that the
// antenna cannot have answered yet — the same window GetStatus reports motion
// for, so the fast poll starts with the movement rather than one interval into
// it.
func (c *Client) movingOptimistically() bool {
c.statusMu.RLock()
defer c.statusMu.RUnlock()
return !c.moveCmdAt.IsZero() && time.Since(c.moveCmdAt) < ubMoveOptimisticWindow
}
// LastSetKHz returns the frequency (kHz) most recently commanded to the antenna,
// or 0 if none yet.
func (c *Client) LastSetKHz() int {
@@ -312,8 +329,22 @@ func (c *Client) pollLoop() {
close(c.done)
}
}()
ticker := time.NewTicker(2 * time.Second) // Increased from 500ms to 2s
// TWO CADENCES, and the fast one is what matters.
//
// An idle antenna has nothing to say, so two seconds is generous. A MOVING
// one has one thing to say and it is urgent: transmit is inhibited while the
// elements travel, so every poll interval between the motors stopping and
// this loop noticing is a second the operator cannot call — with the antenna
// already in place and the rig still gagged. Reported from the air as "it
// stays orange one or two seconds after it has finished".
//
// While the motors run — or a move has just been commanded — the antenna is
// asked four times a second. The controller answers a status query in
// milliseconds; this is nothing to it, and it lasts only as long as the
// movement does.
ticker := time.NewTicker(ubPollIdle)
defer ticker.Stop()
fast := false
pollCount := 0
pollFails := 0 // consecutive failed status polls (transient timeouts tolerated)
@@ -436,6 +467,18 @@ func (c *Client) pollLoop() {
c.lastStatus = status
c.statusMu.Unlock()
// Follow the motors with the poll rate. Changed only when it actually
// changes: Reset on a ticker is cheap but not free, and the antenna is
// polled for hours on end.
if moving := status.MotorsMoving != 0 || c.movingOptimistically(); moving != fast {
fast = moving
if fast {
ticker.Reset(ubPollMoving)
} else {
ticker.Reset(ubPollIdle)
}
}
case <-c.stopChan:
return
}