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 {
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)