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) + } + } +}