From d0666581c3c64e4f951dbfad15bdcca4b70e61bc Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 29 Jul 2026 13:03:09 +0200 Subject: [PATCH] fix: a CW macro knocked the Yaesu CAT link over MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Clicking a macro dropped the CAT for a few seconds, keyed nothing, then came back. ask() returned the first ';'-terminated frame it saw, whatever command it belonged to. KY produces NO reply, so the next query — FA; from the poll loop — collected a leftover frame, failed to parse it as a frequency, and ReadState reported an error. The Manager reads that as "lost the rig": disconnect, wait, reconnect. Hence the drop and the automatic recovery a few seconds later. Replies are now matched to the command that asked for them: anything else is discarded and logged, so a stray frame costs one log line instead of the link. This also explains the silence — the send never got a clean run at the port while the backend was being torn down under it. --- internal/cat/yaesu.go | 37 +++++++++++++++++++++++++++++++++---- internal/cat/yaesu_test.go | 27 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 4 deletions(-) diff --git a/internal/cat/yaesu.go b/internal/cat/yaesu.go index 3d48c10..b1cb52e 100644 --- a/internal/cat/yaesu.go +++ b/internal/cat/yaesu.go @@ -297,8 +297,16 @@ func (y *Yaesu) ask(cmd string) (string, error) { if err := y.write(cmd); err != nil { return "", err } - buf := make([]byte, 0, 32) - tmp := make([]byte, 32) + // Match the reply to the COMMAND, and drop anything else. + // + // Returning the first ';'-terminated string whatever it was is what made a CW + // macro knock the CAT link over: KY produces no reply, so the next query — + // FA; from the poll loop — collected a leftover frame, failed to parse as a + // frequency, and the Manager treated that as "lost the rig" and reconnected. + // The operator saw the CAT drop for a few seconds on every macro click. + want := cmdPrefix(cmd) + buf := make([]byte, 0, 64) + tmp := make([]byte, 64) deadline := time.Now().Add(600 * time.Millisecond) for time.Now().Before(deadline) { n, err := y.port.Read(tmp) @@ -309,8 +317,17 @@ func (y *Yaesu) ask(cmd string) (string, error) { continue // read timeout — the rig may still be composing its answer } buf = append(buf, tmp[:n]...) - if i := strings.IndexByte(string(buf), ';'); i >= 0 { - return string(buf[:i+1]), nil + for { + i := strings.IndexByte(string(buf), ';') + if i < 0 { + break + } + frame := string(buf[:i+1]) + buf = buf[i+1:] + if want == "" || strings.HasPrefix(strings.ToUpper(frame), want) { + return frame, nil + } + debugLog.Printf("yaesu: discarding %q while waiting for %s (asked %q)", frame, want, cmd) } if len(buf) > 512 { return "", fmt.Errorf("yaesu: no ';' in %d bytes answering %q", len(buf), cmd) @@ -319,6 +336,18 @@ func (y *Yaesu) ask(cmd string) (string, error) { return "", fmt.Errorf("yaesu: timeout answering %q", cmd) } +// cmdPrefix is the leading letters of a command — what its reply starts with. +// "FA;" → "FA", "MD0;" → "MD", "KY;" → "KY". +func cmdPrefix(cmd string) string { + c := strings.ToUpper(strings.TrimSpace(cmd)) + for i := 0; i < len(c); i++ { + if c[i] < 'A' || c[i] > 'Z' { + return c[:i] + } + } + return c +} + // parseYaesuFreq reads "FA014074000;" into Hz. func parseYaesuFreq(reply, prefix string) (int64, bool) { r := strings.TrimSpace(reply) diff --git a/internal/cat/yaesu_test.go b/internal/cat/yaesu_test.go index 6b17ea0..a75120b 100644 --- a/internal/cat/yaesu_test.go +++ b/internal/cat/yaesu_test.go @@ -103,3 +103,30 @@ func TestYaesuModeDigit(t *testing.T) { } } } + +// A reply belongs to the command that asked for it. +// +// Without this, a CW macro knocked the CAT link over: KY produces no reply, so +// the poll loop's next FA; collected a leftover frame, failed to parse it as a +// frequency, and the Manager treated that as "lost the rig" and reconnected — +// the CAT dropping for a few seconds on every macro click. +func TestYaesuCmdPrefix(t *testing.T) { + cases := []struct{ cmd, want string }{ + {"FA;", "FA"}, + {"FB;", "FB"}, + {"MD0;", "MD"}, + {"KY;", "KY"}, + {"KY CQ TEST;", "KY"}, + {"SM0;", "SM"}, + {"RM4;", "RM"}, + {"AG0;", "AG"}, + {"TX;", "TX"}, + {"", ""}, + {";", ""}, + } + for _, c := range cases { + if got := cmdPrefix(c.cmd); got != c.want { + t.Errorf("cmdPrefix(%q) = %q, want %q", c.cmd, got, c.want) + } + } +}