fix(cat): keep the Kenwood read buffer across commands; log the CAT error
A TS-480 connected, answered ID, then never reported a state: discarding " 000000000010000000;" while waiting for IF connected on COM3 @ 115200 baud cat:state → connected=false freq=0 A headless frame tail can only come from bytes that were thrown away. ask() buffered into a local slice, so everything left after the matched frame — including the head of a frame still arriving — went with it, and the link could sit one answer behind its questions. The buffer now lives on the backend, and Connect drains whatever the rig said before AI0 took effect rather than inheriting it. Also: the cat:state diagnostic prints RigState.Error. It read "connected=false freq=0" and said nothing about why, so every report of a CAT failure arrived without the one fact that explains it — the reason only ever reached a tooltip. New work moves to 0.22.8; 0.22.7 is released.
This commit is contained in:
@@ -971,8 +971,18 @@ func (a *App) startup(ctx context.Context) {
|
||||
// burst of these lines = the frontend is being re-rendered rapidly (the
|
||||
// "screen flickers" symptom). Shows WHAT is churning — connection flap,
|
||||
// or freq/split/mode oscillating between slices during FT8.
|
||||
applog.Printf("cat:state → connected=%v freq=%d rx=%d split=%v mode=%s band=%s",
|
||||
s.Connected, s.FreqHz, s.RxFreqHz, s.Split, s.Mode, s.Band)
|
||||
// The error goes in the line too. Without it a disconnect read
|
||||
// "connected=false freq=0" and said nothing about WHY — the reason was in
|
||||
// RigState.Error, which only ever reached a tooltip, so every report of
|
||||
// "it stopped connecting" arrived without the one fact that explains it.
|
||||
applog.Printf("cat:state → connected=%v freq=%d rx=%d split=%v mode=%s band=%s%s",
|
||||
s.Connected, s.FreqHz, s.RxFreqHz, s.Split, s.Mode, s.Band,
|
||||
func() string {
|
||||
if s.Error == "" {
|
||||
return ""
|
||||
}
|
||||
return " err=" + s.Error
|
||||
}())
|
||||
if a.ctx != nil {
|
||||
wruntime.EventsEmit(a.ctx, "cat:state", s)
|
||||
}
|
||||
|
||||
+16
-4
@@ -1,4 +1,18 @@
|
||||
[
|
||||
{
|
||||
"version": "0.22.8",
|
||||
"date": "",
|
||||
"en": [
|
||||
"Main tab: drag the divider between the two panes to resize them. The setting is remembered and travels with the data folder; double-click the divider to even them out.",
|
||||
"Kenwood: bytes arriving glued to the previous answer are no longer discarded, and the link starts from a clean buffer — a rig whose replies ran together could end up one answer behind and never report connected.",
|
||||
"The CAT log line now carries the error explaining a disconnection."
|
||||
],
|
||||
"fr": [
|
||||
"Onglet Principal : faites glisser le séparateur entre les deux volets pour les redimensionner. Le réglage est mémorisé et suit le dossier de données ; double-clic pour les égaliser.",
|
||||
"Kenwood : les octets reçus collés à la réponse précédente ne sont plus jetés, et la liaison démarre sur un tampon vide — une radio dont les réponses s’enchaînent pouvait rester une réponse en retard et ne jamais apparaître connectée.",
|
||||
"La ligne de journal CAT indique désormais l’erreur expliquant une déconnexion."
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.22.7",
|
||||
"date": "",
|
||||
@@ -12,8 +26,7 @@
|
||||
"Info (F2) panel: the fields are sized for what they hold — a wider county, narrower prefix and zones, and the address takes the space left by the bearing and distances.",
|
||||
"Clicking in the lower half no longer draws an orange rule under the tab strip.",
|
||||
"A green NEW badge appears on the County field in Info (F2) when the contacted US county has never been worked.",
|
||||
"DARC DOK award updated to v2 in the catalogue: one reference per QSO.",
|
||||
"Main tab: drag the divider between the two panes to resize them. The setting is remembered and travels with the data folder; double-click the divider to even them out."
|
||||
"DARC DOK award updated to v2 in the catalogue: one reference per QSO."
|
||||
],
|
||||
"fr": [
|
||||
"Le CAT série ne met plus la radio en émission par le seul fait de se connecter : DTR et RTS sont abaissés sur Xiegu, Yaesu et Kenwood comme ils l'étaient déjà sur Icom — un Xiegu G90 derrière un DE-19 partait en émission et y restait.",
|
||||
@@ -25,8 +38,7 @@
|
||||
"Panneau Info (F2) : les champs sont dimensionnés selon leur contenu — comté plus large, préfixe et zones plus étroits, et l’adresse récupère la place laissée par l’azimut et les distances.",
|
||||
"Un clic dans la moitié basse ne trace plus de trait orange sous la barre d’onglets.",
|
||||
"Un badge vert NOUV apparaît sur le champ Comté dans Info (F2) quand le comté américain contacté n’a jamais été travaillé.",
|
||||
"Diplôme DARC DOK mis à jour en v2 dans le catalogue : une seule référence par QSO.",
|
||||
"Onglet Principal : faites glisser le séparateur entre les deux volets pour les redimensionner. Le réglage est mémorisé et suit le dossier de données ; double-clic pour les égaliser."
|
||||
"Diplôme DARC DOK mis à jour en v2 dans le catalogue : une seule référence par QSO."
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
+54
-14
@@ -29,6 +29,7 @@ package cat
|
||||
// from the radio, and the rig file decides what a "Freq" property means.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
@@ -71,6 +72,17 @@ type Kenwood struct {
|
||||
curVFO string // "A" or "B"
|
||||
// Commands this rig answered "?;" to — asked once, then never again.
|
||||
unsupported map[string]bool
|
||||
|
||||
// rx holds bytes read but not yet consumed, ACROSS calls to ask.
|
||||
//
|
||||
// It has to survive: a rig answers faster than we ask, so one Read often
|
||||
// returns a whole reply plus the start of the next frame. When this buffer
|
||||
// was local to ask, everything after the matched frame was dropped — half a
|
||||
// frame included — and the link desynchronised permanently: every ask then
|
||||
// found the PREVIOUS command's answer and timed out waiting for its own.
|
||||
// That is the "discarding \" 000000000010000000;\" while waiting for IF"
|
||||
// a TS-480 reported, followed by connected=false for ever.
|
||||
rx []byte
|
||||
}
|
||||
|
||||
// NewKenwoodTCP builds a backend that reaches the rig over a socket instead of
|
||||
@@ -122,6 +134,13 @@ func (k *Kenwood) Connect() error {
|
||||
// request/response pairs and make a reply impossible to attribute. We poll.
|
||||
_ = k.write("AI0;")
|
||||
|
||||
// Start from silence. A reconnect inherits whatever the rig said last —
|
||||
// unsolicited frames sent before AI0 landed, the tail of an answer nobody
|
||||
// read — and one stale frame is enough to leave every ask one reply behind
|
||||
// its question for the rest of the session.
|
||||
k.rx = nil
|
||||
k.drain(250 * time.Millisecond)
|
||||
|
||||
answered := false
|
||||
if id, err := k.ask("ID;"); err == nil && strings.HasPrefix(id, "ID") {
|
||||
answered = true
|
||||
@@ -300,6 +319,23 @@ func (k *Kenwood) write(cmd string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// drain reads and throws away whatever the rig has already sent, until it stays
|
||||
// quiet for one read timeout or the budget runs out.
|
||||
func (k *Kenwood) drain(budget time.Duration) {
|
||||
if k.port == nil {
|
||||
return
|
||||
}
|
||||
tmp := make([]byte, 256)
|
||||
deadline := time.Now().Add(budget)
|
||||
for time.Now().Before(deadline) {
|
||||
n, err := k.port.Read(tmp)
|
||||
if err != nil || n == 0 {
|
||||
return // an error here is not interesting: we are throwing this away
|
||||
}
|
||||
traceText("kenwood", "RX-drop", string(tmp[:n]))
|
||||
}
|
||||
}
|
||||
|
||||
// ask sends a query and returns the reply belonging to THAT command. Anything
|
||||
// else on the wire is discarded: a stray frame parsed as a frequency reads as
|
||||
// "lost the rig" to the Manager, which then reconnects — the CAT link dropping
|
||||
@@ -312,25 +348,18 @@ func (k *Kenwood) ask(cmd string) (string, error) {
|
||||
if err := k.write(cmd); err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf := make([]byte, 0, 64)
|
||||
tmp := make([]byte, 64)
|
||||
deadline := time.Now().Add(600 * time.Millisecond)
|
||||
for time.Now().Before(deadline) {
|
||||
n, err := k.port.Read(tmp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n == 0 {
|
||||
continue // read timeout — the rig may still be composing its answer
|
||||
}
|
||||
buf = append(buf, tmp[:n]...)
|
||||
for {
|
||||
i := strings.IndexByte(string(buf), ';')
|
||||
// Consume whatever is already buffered BEFORE reading more: the answer
|
||||
// may have arrived attached to the previous one.
|
||||
for {
|
||||
i := bytes.IndexByte(k.rx, ';')
|
||||
if i < 0 {
|
||||
break
|
||||
}
|
||||
frame := string(buf[:i+1])
|
||||
buf = buf[i+1:]
|
||||
frame := string(k.rx[:i+1])
|
||||
k.rx = k.rx[i+1:]
|
||||
traceText("kenwood", "RX", frame)
|
||||
if frame == "?;" {
|
||||
// The rig rejected the command. Remember it so the poll loop stops
|
||||
@@ -344,9 +373,20 @@ func (k *Kenwood) ask(cmd string) (string, error) {
|
||||
}
|
||||
debugLog.Printf("kenwood: discarding %q while waiting for %s", frame, want)
|
||||
}
|
||||
}
|
||||
if !time.Now().Before(deadline) {
|
||||
return "", fmt.Errorf("kenwood: timeout answering %q", cmd)
|
||||
}
|
||||
n, err := k.port.Read(tmp)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if n > 0 {
|
||||
k.rx = append(k.rx, tmp[:n]...)
|
||||
}
|
||||
// n == 0 is a read timeout, not silence for good: the rig may still be
|
||||
// composing its answer.
|
||||
}
|
||||
}
|
||||
|
||||
// ── Frame parsing ──────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user