fix(steppir): the same fast poll, and motion reported at the command
The shortened transmit gag was written for an Ultrabeam that is now polled four times a second while it moves. The SteppIR was still on two seconds and reported nothing at all until its own poll came round, so the gap between the 900 ms grace and the first poll that would have seen the movement was a hole in "block TX while the elements travel" — the transmitter released in the middle of a move. It now reports a commanded move at once (bounded, so an antenna that never answers cannot latch the inhibit on), on a COPY of the cached status so the flag cannot leak into what the poll goroutine owns, and follows the motors with its poll rate exactly as the Ultrabeam does.
This commit is contained in:
+2
-2
@@ -12,7 +12,7 @@
|
||||
"Audio: the Listening device now says when it cannot be opened. A device unplugged, renamed by Windows or unable to run at 16 kHz failed silently while everything upstream reported success — the stream up, the packets arriving, the monitor started — which is the whole of “I turned the sound on and nothing comes out”. The log also says, once, whether the network RX audio is reaching the speakers or arriving with nobody listening.",
|
||||
"Icom network audio starts at once instead of half a minute later. The message that authorises the stream is sent during the login, before the audio socket exists — so the rig was told to send audio to a port nothing was bound to, got a port-unreachable back, and only resumed when its own retry timer came round. It is sent once more as soon as the port is listening.",
|
||||
"Rotor dial: a circular scale and a beam instead of an arrow. The square ring made a marker at 45° sit further from the centre than one at north — a dial is read by angle, so the ring it is read against is now the same distance away all the way round. The antenna is drawn as a sector that fades outwards, which is the shape of the thing it stands for; where the mouse would send it appears in the same shape in orange, and its azimuth in place of the current heading while you aim. Green for where the antenna is, orange for where it would go, yellow for what was ordered — the second lobe of a bidirectional Ultrabeam and the dashed boom are unchanged. Design from EC1KD again.",
|
||||
"Motorised antenna: the transmitter is released as soon as the elements stop. Three delays were stacked between the antenna finishing and the operator being allowed to call — the antenna polled every two seconds, the transmit gag held for three after the command whatever the antenna said, and the screen refreshed every three. The antenna is now polled four times a second WHILE IT MOVES (and left at two seconds when it is still, where nothing changes), the gag only bridges the command itself, and the widget follows at half a second.",
|
||||
"Motorised antenna: the transmitter is released as soon as the elements stop. Three delays were stacked between the antenna finishing and the operator being allowed to call — the antenna polled every two seconds, the transmit gag held for three after the command whatever the antenna said, and the screen refreshed every three. Both the Ultrabeam and the SteppIR are now polled four times a second WHILE IT MOVES (and left at two seconds when it is still, where nothing changes), the gag only bridges the command itself, and the widget follows at half a second. The SteppIR also reports a commanded move at once, as the Ultrabeam already did: it says nothing until its own poll comes round, so the shortened gag would otherwise have released the transmitter in the middle of a move.",
|
||||
"[NEW] The WSJT-X / JTDX highlight colours are yours to choose (Settings → UDP), one per verdict — watch list, new DXCC, new band, worked. Only the background is set: the text colour is worked out from it, so a chosen colour cannot come back unreadable in the decoder’s window. The “grey out stations already worked” switch keeps its purpose — it decides WHETHER dupes are marked, not what colour they are — and is now called “Mark stations already worked”.",
|
||||
"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 everything — including the log — so a station already worked stayed pink for the session with no way to tell it from one still needed.",
|
||||
"Motorised antenna: the indicator turns amber the instant the move is ORDERED, from a button or from an automatic band change with tracking on, instead of waiting for the antenna to say it is moving. The transmit gag already started there; the screen did not, so the two disagreed by a second or more — and on a follow there was no warning at all until a poll landed."
|
||||
@@ -27,7 +27,7 @@
|
||||
"Audio : le périphérique d’écoute signale désormais quand il ne peut pas s’ouvrir. Un périphérique débranché, renommé par Windows ou incapable de fonctionner en 16 kHz échouait en silence pendant que tout en amont annonçait le succès — flux ouvert, paquets reçus, moniteur démarré — ce qui est exactement le « j’ai remis le son et rien ne sort ». Le journal dit aussi, une fois, si l’audio réseau atteint les haut-parleurs ou arrive sans que personne n’écoute.",
|
||||
"L’audio réseau Icom démarre tout de suite au lieu d’une demi-minute plus tard. Le message qui autorise le flux part pendant la connexion, avant que la prise audio n’existe : le poste se voyait donc demander d’émettre vers un port où personne n’écoutait, recevait un « port injoignable » en retour, et ne reprenait qu’au tour suivant de son propre minuteur. Il est renvoyé dès que le port écoute.",
|
||||
"Cadran rotor : échelle circulaire et faisceau au lieu d’une flèche. L’anneau carré plaçait un repère à 45° plus loin du centre qu’un repère au nord — un cadran se lit par l’angle, donc l’anneau qui sert de référence est désormais à la même distance tout autour. L’antenne est dessinée comme un secteur qui s’estompe vers l’extérieur, ce qui est la forme de ce qu’il représente ; là où la souris l’enverrait apparaît dans la même forme en orange, et son azimut à la place du cap courant pendant qu’on vise. Vert pour où l’antenne est, orange pour où elle irait, jaune pour ce qui a été demandé — le deuxième lobe d’un Ultrabeam bidirectionnel et le boom en pointillés sont inchangés. Design d’EC1KD, encore.",
|
||||
"Antenne motorisée : l’émission est rendue dès que les éléments s’arrêtent. Trois délais s’ajoutaient entre la fin du mouvement et le droit d’appeler — l’antenne interrogée toutes les deux secondes, le blocage d’émission maintenu trois secondes après la commande quoi qu’en dise l’antenne, et l’écran rafraîchi toutes les trois. L’antenne est désormais interrogée quatre fois par seconde PENDANT qu’elle bouge (et laissée à deux secondes à l’arrêt, où rien ne change), le blocage ne couvre plus que la commande elle-même, et le widget suit à la demi-seconde.",
|
||||
"Antenne motorisée : l’émission est rendue dès que les éléments s’arrêtent. Trois délais s’ajoutaient entre la fin du mouvement et le droit d’appeler — l’antenne interrogée toutes les deux secondes, le blocage d’émission maintenu trois secondes après la commande quoi qu’en dise l’antenne, et l’écran rafraîchi toutes les trois. L’Ultrabeam comme la SteppIR sont désormais interrogées quatre fois par seconde PENDANT qu’elle bouge (et laissée à deux secondes à l’arrêt, où rien ne change), le blocage ne couvre plus que la commande elle-même, et le widget suit à la demi-seconde. La SteppIR signale aussi un mouvement dès qu’il est commandé, comme le faisait déjà l’Ultrabeam : elle ne dit rien avant sa propre lecture, et le blocage raccourci aurait sinon rendu l’émission en plein mouvement.",
|
||||
"[NEW] Les couleurs de mise en évidence WSJT-X / JTDX sont au choix (Réglages → UDP), une par verdict — watchlist, nouveau DXCC, nouvelle bande, contactée. Seul le fond se règle : la couleur du texte en est déduite, pour qu’une couleur choisie ne revienne jamais illisible dans la fenêtre du décodeur. L’option « griser les stations déjà contactées » garde sa raison d’être — elle décide SI les doublons sont marqués, pas de quelle couleur — et s’appelle désormais « Marquer les stations déjà contactées ».",
|
||||
"Mise en évidence WSJT-X : une station de la watchlist déjà contactée sur cette bande et ce mode n’est plus peinte comme une station à appeler. La liste dit une intention, pas ce qu’il reste à faire, et son rose passait devant tout — y compris le carnet — si bien qu’une station déjà faite restait rose toute la session, sans moyen de la distinguer d’une station encore à faire.",
|
||||
"Antenne motorisée : l’indicateur passe à l’ambre à l’instant où le mouvement est COMMANDÉ, par un bouton comme par un changement de bande automatique quand le suivi est actif, au lieu d’attendre que l’antenne dise qu’elle bouge. Le blocage d’émission démarrait déjà là ; l’écran non, et les deux se contredisaient d’une seconde ou plus — sur un suivi, il n’y avait aucun signe avant l’arrivée d’une lecture."
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package steppir
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The controller says nothing until its own poll comes round, so a move has to
|
||||
// be reported from the moment it is COMMANDED. Without it the app's "block TX
|
||||
// while the elements travel" had a hole: the grace covering the command ran out
|
||||
// before the first poll that would have seen the movement, and the transmitter
|
||||
// was released in the middle of a move.
|
||||
func TestAJustCommandedMoveReportsMotion(t *testing.T) {
|
||||
c := &Client{}
|
||||
c.lastStatus = &Status{Connected: true} // idle, as the controller last said
|
||||
|
||||
if st, _ := c.GetStatus(); st.MotorsMoving != 0 {
|
||||
t.Fatal("an idle antenna reports motion")
|
||||
}
|
||||
|
||||
c.statusMu.Lock()
|
||||
c.moveCmdAt = time.Now()
|
||||
c.statusMu.Unlock()
|
||||
|
||||
st, _ := c.GetStatus()
|
||||
if st.MotorsMoving == 0 {
|
||||
t.Error("a move commanded a moment ago is not reported as motion")
|
||||
}
|
||||
if c.lastStatus.MotorsMoving != 0 {
|
||||
t.Error("the optimistic flag leaked into the cached status the poll owns")
|
||||
}
|
||||
|
||||
// Bounded: an antenna that never reports motion must not latch the transmit
|
||||
// inhibit on for ever.
|
||||
c.statusMu.Lock()
|
||||
c.moveCmdAt = time.Now().Add(-moveOptimisticWindow - time.Second)
|
||||
c.statusMu.Unlock()
|
||||
if st, _ := c.GetStatus(); st.MotorsMoving != 0 {
|
||||
t.Error("the optimistic window never expires")
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,25 @@ const (
|
||||
// self-corrects instead of lying forever.
|
||||
const pendingDirTTL = 45 * time.Second
|
||||
|
||||
// The same two cadences the Ultrabeam uses, and for the same reason: transmit
|
||||
// is inhibited while the elements travel, so every poll interval between the
|
||||
// motors stopping and this client noticing is a second the operator cannot call
|
||||
// with the antenna already in place.
|
||||
const (
|
||||
pollIdle = 2 * time.Second
|
||||
pollMoving = 250 * time.Millisecond
|
||||
)
|
||||
|
||||
// moveOptimisticWindow is how long after a commanded move GetStatus reports
|
||||
// motion before the controller has had a chance to say so.
|
||||
//
|
||||
// The SteppIR reports nothing until its own poll comes round, so without this
|
||||
// the app's "block TX while moving" had a hole in it: the grace that covers the
|
||||
// command ran out before the first poll that would have seen the movement, and
|
||||
// the transmitter was released in the middle of a move. Bounded, so an antenna
|
||||
// that never reports motion cannot latch the inhibit on for ever.
|
||||
const moveOptimisticWindow = 3 * time.Second
|
||||
|
||||
// Transport says how to reach the controller.
|
||||
type Transport struct {
|
||||
Mode string // "tcp" | "serial"
|
||||
@@ -103,6 +122,8 @@ type Client struct {
|
||||
statusMu sync.RWMutex
|
||||
lastStatus *Status
|
||||
lastSetKHz int
|
||||
// moveCmdAt is when a move was last COMMANDED — see moveOptimisticWindow.
|
||||
moveCmdAt time.Time
|
||||
// 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.
|
||||
@@ -191,7 +212,28 @@ func (c *Client) GetStatus() (*Status, error) {
|
||||
if c.lastStatus == nil {
|
||||
return &Status{Connected: false}, nil
|
||||
}
|
||||
return c.lastStatus, nil
|
||||
// A COPY, so the optimistic flag below can never leak into the status the
|
||||
// poll goroutine owns — and so a caller holding the value cannot see it
|
||||
// change under them mid-read.
|
||||
st := *c.lastStatus
|
||||
if st.Connected && st.MotorsMoving == 0 && c.movingOptimisticallyLocked() {
|
||||
st.MotorsMoving = 1 // sentinel: commanded, not yet reported (read as != 0)
|
||||
}
|
||||
return &st, nil
|
||||
}
|
||||
|
||||
// movingOptimisticallyLocked reports a move commanded too recently for the
|
||||
// controller to have answered. Callers hold statusMu.
|
||||
func (c *Client) movingOptimisticallyLocked() bool {
|
||||
return !c.moveCmdAt.IsZero() && time.Since(c.moveCmdAt) < moveOptimisticWindow
|
||||
}
|
||||
|
||||
// MovingOptimistically is the same question from outside the lock — the poll
|
||||
// loop asks it to decide its cadence.
|
||||
func (c *Client) MovingOptimistically() bool {
|
||||
c.statusMu.RLock()
|
||||
defer c.statusMu.RUnlock()
|
||||
return c.movingOptimisticallyLocked()
|
||||
}
|
||||
|
||||
// open dials the transport. Callers hold connMu.
|
||||
@@ -266,8 +308,9 @@ func (c *Client) pollLoop() {
|
||||
close(c.done)
|
||||
}
|
||||
}()
|
||||
ticker := time.NewTicker(2 * time.Second)
|
||||
ticker := time.NewTicker(pollIdle)
|
||||
defer ticker.Stop()
|
||||
fast := false
|
||||
for {
|
||||
select {
|
||||
case <-c.stopChan:
|
||||
@@ -315,6 +358,17 @@ func (c *Client) pollLoop() {
|
||||
c.lastStatus = st
|
||||
c.statusMu.Unlock()
|
||||
c.checkDrift(st)
|
||||
|
||||
// Follow the motors with the poll rate; changed only when it changes,
|
||||
// since the antenna is polled for hours on end.
|
||||
if moving := st.MotorsMoving != 0 || c.MovingOptimistically(); moving != fast {
|
||||
fast = moving
|
||||
if fast {
|
||||
ticker.Reset(pollMoving)
|
||||
} else {
|
||||
ticker.Reset(pollIdle)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -639,6 +693,7 @@ func (c *Client) SetFrequency(freqKhz int, direction int) error {
|
||||
c.statusMu.Lock()
|
||||
c.lastSetKHz = freqKhz
|
||||
c.pendingDir, c.pendingDirAt, c.pendingDirSet = direction, time.Now(), true
|
||||
c.moveCmdAt = time.Now() // report motion at once — see moveOptimisticWindow
|
||||
c.statusMu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user