fix: a CW macro knocked the Yaesu CAT link over

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.
This commit is contained in:
2026-07-29 13:03:09 +02:00
parent 37dc2a07e5
commit d0666581c3
2 changed files with 60 additions and 4 deletions
+33 -4
View File
@@ -297,8 +297,16 @@ func (y *Yaesu) ask(cmd string) (string, error) {
if err := y.write(cmd); err != nil { if err := y.write(cmd); err != nil {
return "", err return "", err
} }
buf := make([]byte, 0, 32) // Match the reply to the COMMAND, and drop anything else.
tmp := make([]byte, 32) //
// 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) deadline := time.Now().Add(600 * time.Millisecond)
for time.Now().Before(deadline) { for time.Now().Before(deadline) {
n, err := y.port.Read(tmp) 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 continue // read timeout — the rig may still be composing its answer
} }
buf = append(buf, tmp[:n]...) buf = append(buf, tmp[:n]...)
if i := strings.IndexByte(string(buf), ';'); i >= 0 { for {
return string(buf[:i+1]), nil 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 { if len(buf) > 512 {
return "", fmt.Errorf("yaesu: no ';' in %d bytes answering %q", len(buf), cmd) 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) 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. // parseYaesuFreq reads "FA014074000;" into Hz.
func parseYaesuFreq(reply, prefix string) (int64, bool) { func parseYaesuFreq(reply, prefix string) (int64, bool) {
r := strings.TrimSpace(reply) r := strings.TrimSpace(reply)
+27
View File
@@ -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)
}
}
}