fix(pgxl): match a reply to the command that asked for it
The amplifier PUSHES status frames ("S0|state=…") on the same socket it answers
commands on, and it pushes them constantly once in OPERATE - power, SWR and
temperature all move while transmitting.
command() read exactly one line and took whatever arrived first as its answer.
One pushed frame therefore put the stream permanently one reply behind: every
later command read the PREVIOUS command's answer, and eventually one waited out
the 3 s deadline, failed, and dropped the connection. Hence a fresh TCP
connection and a fresh authentication every few seconds in the log.
The stall in transmit is the same defect seen from the other side: command()
holds the connection mutex across that whole dead wait, so SetOperate and every
other control queued behind up to three seconds of nothing.
It only appeared remotely and in OPERATE because that is when there is anything
to push - on a LAN with an idle amplifier the race hardly ever opens. Which is
why it did not do this yesterday.
readReplyLocked now reads until the R<id>| that belongs to the command, feeding
every frame it passes to parse() on the way - a pushed status is fresher than
the one we were about to ask for, so nothing is wasted.
authLocked used the same one-line read and worked around this by re-sending the
whole handshake, which is visible in the log as "auth reply=S0|state=IDLE
(try 1)" followed by a second attempt. It goes through the same reader now.
This commit is contained in:
+4
-2
@@ -6,13 +6,15 @@
|
|||||||
"DX cluster: an L badge next to a callsign marks a station that uploads to LoTW, with a matching LoTW users only filter, and a Spotter continent filter narrows the list by where the spot came FROM — a JA report on 20 m tells a European little about their own path. The filter panel has been tidied along one rule: a switch is a behaviour you turn on or off, chips pick any number from a set. Nothing appears in both shapes any more, the Lock buttons sit in the heading of the section they lock, every section can be cleared the same way, and the whole panel is finally translated.",
|
"DX cluster: an L badge next to a callsign marks a station that uploads to LoTW, with a matching LoTW users only filter, and a Spotter continent filter narrows the list by where the spot came FROM — a JA report on 20 m tells a European little about their own path. The filter panel has been tidied along one rule: a switch is a behaviour you turn on or off, chips pick any number from a set. Nothing appears in both shapes any more, the Lock buttons sit in the heading of the section they lock, every section can be cleared the same way, and the whole panel is finally translated.",
|
||||||
"DX cluster: the Spotter continent filter now actually matches. RBN skimmers report as VU2OY-# and cluster nodes as DL1ABC-2, and the suffix was passed straight to the prefix lookup, so every spot came back with no continent and the filter silently selected nothing.",
|
"DX cluster: the Spotter continent filter now actually matches. RBN skimmers report as VU2OY-# and cluster nodes as DL1ABC-2, and the suffix was passed straight to the prefix lookup, so every spot came back with no continent and the filter silently selected nothing.",
|
||||||
"A spot now stops being NEW the moment you log it. Logging announced the QSO but never dropped the cluster worked-index snapshot, so the refresh that follows re-read the answer computed BEFORE the contact — a station stayed yellow on the band map and in the cluster with the QSO plainly in the log, until something else happened to rebuild the index.",
|
"A spot now stops being NEW the moment you log it. Logging announced the QSO but never dropped the cluster worked-index snapshot, so the refresh that follows re-read the answer computed BEFORE the contact — a station stayed yellow on the band map and in the cluster with the QSO plainly in the log, until something else happened to rebuild the index.",
|
||||||
"Station information gains an IOTA field (ADIF MY_IOTA, e.g. EU-005), stamped on every QSO like the SOTA and POTA references beside it. The QSO table has carried the field since the first release and both ADIF import and export already handled it — only the station profile could not supply it, so an island activation meant typing the reference on every contact."
|
"Station information gains an IOTA field (ADIF MY_IOTA, e.g. EU-005), stamped on every QSO like the SOTA and POTA references beside it. The QSO table has carried the field since the first release and both ADIF import and export already handled it — only the station profile could not supply it, so an island activation meant typing the reference on every contact.",
|
||||||
|
"PowerGenius XL: fixed the amplifier link dropping and reconnecting every few seconds, and the stall while transmitting. The amp pushes status frames on the same socket it answers commands on, and it pushes them constantly once in OPERATE; OpsLog read one line per command and took whatever arrived first as its answer, so the stream slipped permanently one reply behind until a command timed out and the connection was dropped. Replies are now matched to the command that asked for them. Only ever visible over a remote link with the amplifier in OPERATE."
|
||||||
],
|
],
|
||||||
"fr": [
|
"fr": [
|
||||||
"Cluster DX : un badge L à côté de l indicatif signale une station qui utilise LoTW, avec le filtre Utilisateurs LoTW seulement qui va avec, et un filtre Continent du spotter restreint selon l origine du spot — un report JA sur 20 m dit peu de chose à un Européen sur son propre chemin. Le panneau de filtres a été remis d aplomb selon une seule règle : un interrupteur est un comportement qu on active ou non, les pastilles choisissent dans un ensemble. Plus rien n existe sous les deux formes, les boutons de verrouillage sont dans le titre de la section qu ils verrouillent, chaque section s efface de la même façon, et le panneau est enfin traduit.",
|
"Cluster DX : un badge L à côté de l indicatif signale une station qui utilise LoTW, avec le filtre Utilisateurs LoTW seulement qui va avec, et un filtre Continent du spotter restreint selon l origine du spot — un report JA sur 20 m dit peu de chose à un Européen sur son propre chemin. Le panneau de filtres a été remis d aplomb selon une seule règle : un interrupteur est un comportement qu on active ou non, les pastilles choisissent dans un ensemble. Plus rien n existe sous les deux formes, les boutons de verrouillage sont dans le titre de la section qu ils verrouillent, chaque section s efface de la même façon, et le panneau est enfin traduit.",
|
||||||
"Cluster DX : le filtre Continent du spotter fonctionne enfin. Les skimmers RBN s annoncent en VU2OY-# et les nœuds cluster en DL1ABC-2, et ce suffixe partait tel quel dans la recherche de préfixe — tous les spots revenaient sans continent et le filtre ne sélectionnait rien.",
|
"Cluster DX : le filtre Continent du spotter fonctionne enfin. Les skimmers RBN s annoncent en VU2OY-# et les nœuds cluster en DL1ABC-2, et ce suffixe partait tel quel dans la recherche de préfixe — tous les spots revenaient sans continent et le filtre ne sélectionnait rien.",
|
||||||
"Un spot cesse enfin d être NOUVEAU dès que tu l enregistres. La journalisation annonçait le QSO mais ne vidait jamais l instantané de l index des contacts, donc le rafraîchissement qui suit relisait la réponse calculée AVANT le contact — une station restait jaune sur le bandmap et dans le cluster alors que le QSO était bien au log, jusqu à ce qu autre chose reconstruise l index.",
|
"Un spot cesse enfin d être NOUVEAU dès que tu l enregistres. La journalisation annonçait le QSO mais ne vidait jamais l instantané de l index des contacts, donc le rafraîchissement qui suit relisait la réponse calculée AVANT le contact — une station restait jaune sur le bandmap et dans le cluster alors que le QSO était bien au log, jusqu à ce qu autre chose reconstruise l index.",
|
||||||
"Les informations station gagnent un champ IOTA (MY_IOTA en ADIF, par exemple EU-005), estampillé sur chaque QSO comme les références SOTA et POTA à côté. La table des QSO portait le champ depuis la première version et l import comme l export ADIF le géraient déjà — seul le profil station ne savait pas le fournir, donc une activation d île obligeait à retaper la référence à chaque contact."
|
"Les informations station gagnent un champ IOTA (MY_IOTA en ADIF, par exemple EU-005), estampillé sur chaque QSO comme les références SOTA et POTA à côté. La table des QSO portait le champ depuis la première version et l import comme l export ADIF le géraient déjà — seul le profil station ne savait pas le fournir, donc une activation d île obligeait à retaper la référence à chaque contact.",
|
||||||
|
"PowerGenius XL : corrigé le lien avec l ampli qui tombait et se reconnectait toutes les quelques secondes, et le blocage en émission. L ampli pousse des trames d état sur la socket même où il répond aux commandes, et il en pousse en permanence dès qu il est en OPERATE ; OpsLog lisait une ligne par commande et prenait la première arrivée pour sa réponse, donc le flux glissait définitivement d une réponse de retard jusqu à expiration et fermeture du lien. Les réponses sont désormais appariées à la commande qui les a demandées. Visible uniquement en liaison distante avec l ampli en OPERATE."
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultPort = 9008
|
defaultPort = 9008
|
||||||
dialTimeout = 5 * time.Second
|
dialTimeout = 5 * time.Second
|
||||||
ioTimeout = 3 * time.Second
|
ioTimeout = 3 * time.Second
|
||||||
// Poll fast enough that the amp's OWN forward/current figures make a usable
|
// Poll fast enough that the amp's OWN forward/current figures make a usable
|
||||||
// live meter on their own — the UI prefers them over the FlexRadio VITA stream
|
// live meter on their own — the UI prefers them over the FlexRadio VITA stream
|
||||||
// (which never traverses a public-IP/NAT link), so this direct reading is what
|
// (which never traverses a public-IP/NAT link), so this direct reading is what
|
||||||
@@ -240,12 +240,10 @@ func (c *Client) authLocked() error {
|
|||||||
if _, err := fmt.Fprintf(c.conn, "C%d|auth code=%s\n", id, c.password); err != nil {
|
if _, err := fmt.Fprintf(c.conn, "C%d|auth code=%s\n", id, c.password); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
_ = c.conn.SetReadDeadline(time.Now().Add(ioTimeout))
|
line, err := c.readReplyLocked(fmt.Sprintf("R%d|", id))
|
||||||
line, err := c.reader.ReadString('\n')
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
applog.Printf("pgxl: auth reply=%q (try %d)", line, try)
|
applog.Printf("pgxl: auth reply=%q (try %d)", line, try)
|
||||||
hex, msg := "", ""
|
hex, msg := "", ""
|
||||||
if p := strings.SplitN(line, "|", 3); len(p) >= 2 {
|
if p := strings.SplitN(line, "|", 3); len(p) >= 2 {
|
||||||
@@ -264,6 +262,39 @@ func (c *Client) authLocked() error {
|
|||||||
return fmt.Errorf("powergenius: authentication failed after 4 tries (R|%s|) — check the remote code", lastHex)
|
return fmt.Errorf("powergenius: authentication failed after 4 tries (R|%s|) — check the remote code", lastHex)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// readReplyLocked reads until the reply carrying `want` as its prefix arrives,
|
||||||
|
// feeding every unsolicited frame it passes to parse() on the way.
|
||||||
|
//
|
||||||
|
// The amplifier PUSHES status frames ("S0|state=…") on the same socket, and it
|
||||||
|
// pushes them constantly once it is in OPERATE — power, SWR and temperature all
|
||||||
|
// move while transmitting. The old code read exactly one line per command and
|
||||||
|
// took whatever came first as its answer, so a single pushed frame put the
|
||||||
|
// stream permanently one reply behind: every later command read the PREVIOUS
|
||||||
|
// command's answer, and the last one waited out the 3 s deadline, failed, and
|
||||||
|
// dropped the connection. That is the reconnect-every-few-seconds seen in the
|
||||||
|
// field, and the stall in transmit — command() holds the mutex across that whole
|
||||||
|
// dead wait, so anything else touching the amplifier queued behind it.
|
||||||
|
//
|
||||||
|
// It only showed up remotely and in OPERATE: on a LAN with an idle amplifier
|
||||||
|
// there is almost nothing to push and the race hardly ever opens.
|
||||||
|
func (c *Client) readReplyLocked(want string) (string, error) {
|
||||||
|
deadline := time.Now().Add(ioTimeout)
|
||||||
|
for {
|
||||||
|
_ = c.conn.SetReadDeadline(deadline)
|
||||||
|
line, err := c.reader.ReadString('\n')
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
// Every frame is worth having, ours or not — a pushed status is fresher
|
||||||
|
// than the one we were about to ask for.
|
||||||
|
c.parse(line)
|
||||||
|
if strings.HasPrefix(line, want) {
|
||||||
|
return line, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) dropConn() {
|
func (c *Client) dropConn() {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
if c.conn != nil {
|
if c.conn != nil {
|
||||||
@@ -286,14 +317,7 @@ func (c *Client) command(cmd string) (string, error) {
|
|||||||
if _, err := fmt.Fprintf(c.conn, "C%d|%s\n", id, cmd); err != nil {
|
if _, err := fmt.Fprintf(c.conn, "C%d|%s\n", id, cmd); err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
_ = c.conn.SetReadDeadline(time.Now().Add(ioTimeout))
|
return c.readReplyLocked(fmt.Sprintf("R%d|", id))
|
||||||
line, err := c.reader.ReadString('\n')
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
line = strings.TrimSpace(line)
|
|
||||||
c.parse(line)
|
|
||||||
return line, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parse handles "R<id>|0|<k=v …>" and "S0|<k=v …>" status lines.
|
// parse handles "R<id>|0|<k=v …>" and "S0|<k=v …>" status lines.
|
||||||
|
|||||||
Reference in New Issue
Block a user