From 37dc2a07e5bdff71d5309c4b8874883a9e9d43b1 Mon Sep 17 00:00:00 2001 From: rouggy Date: Wed, 29 Jul 2026 13:00:30 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20the=20Yaesu=20keyer=20refused=20to=20sen?= =?UTF-8?q?d=20=E2=80=94=20a=20status=20reply=20we=20read=20too=20strictly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "the keyer buffer stayed full for 4s" and nothing keyed, on a rig whose CAT is working. The buffer was not full: the check demanded the reply be exactly "KY0;" with the digit at byte 2, so anything the FTDX10 phrases differently — a space before the digit, another command's reply arriving first — read as "still full" on every poll until the deadline. The test is now asymmetric on purpose: only a clear "1" holds the send back. Anything unrecognised goes ahead. Refusing to transmit because a status line was phrased unexpectedly is the worse failure — the operator gets silence with no explanation, where at worst sending early truncates a long message, which the chunk loop then recovers from. The reply is also logged once per run, since we have no verified sample of it — that line is what turns the next surprise into a fact instead of a theory. --- internal/cat/yaesu_cw.go | 51 ++++++++++++++++++++++++++++++++--- internal/cat/yaesu_cw_test.go | 31 +++++++++++++++++++++ 2 files changed, 79 insertions(+), 3 deletions(-) diff --git a/internal/cat/yaesu_cw.go b/internal/cat/yaesu_cw.go index d7edbb2..c73c5ea 100644 --- a/internal/cat/yaesu_cw.go +++ b/internal/cat/yaesu_cw.go @@ -27,6 +27,10 @@ import ( // split and fed as the rig drains its buffer. const yaesuCWChunk = 24 +// cwStatusLogged makes the KY status reply appear in the log once per run: it +// is a line we have no verified sample of, so the first one is worth keeping. +var cwStatusLogged bool + // yaesuCWAllowed is what the rig's keyer can actually send. Anything else is // dropped rather than passed through: an unsupported byte can abort the whole // buffer, losing the rest of the message with no error anywhere. @@ -97,16 +101,57 @@ func (y *Yaesu) waitCWBuffer(within time.Duration) error { debugLog.Printf("yaesu cw: KY status query failed (%v) — sending anyway", err) return nil } - if len(r) >= 3 && r[2] == '0' { - return nil // room in the buffer + // Only an explicit "full" holds us back. + // + // The first version required the reply to be exactly "KY0;" at byte 2, and + // on a real FTDX10 that refused to send at all: anything the rig phrases + // differently — a space before the digit, a longer status — read as "still + // full" and the send died after four seconds having keyed nothing. The rig + // is the one that knows; when its answer is not a clear "1", the right move + // is to go ahead rather than to sit and time out. + // Log the shape once per session: this is a reply we have no verified + // sample of, and the log is what turns the next surprise into a fact. + if !cwStatusLogged { + cwStatusLogged = true + debugLog.Printf("yaesu cw: KY status reply is %q (full=%v)", r, yaesuCWBufferFull(r)) + } + if !yaesuCWBufferFull(r) { + return nil } if time.Now().After(deadline) { - return fmt.Errorf("yaesu: the keyer buffer stayed full for %s", within) + return fmt.Errorf("yaesu: the keyer buffer stayed full for %s (last reply %q)", within, r) } time.Sleep(50 * time.Millisecond) } } +// yaesuCWBufferFull reads the KY status reply. +// +// Deliberately asymmetric: only a clear "1" means full. Anything else — an +// unexpected shape, a reply from another command that arrived first, an empty +// string — is treated as "go ahead". Refusing to send because a status line was +// phrased unexpectedly is the worse failure: the operator gets silence with no +// explanation, where at worst sending early truncates a long message. +func yaesuCWBufferFull(reply string) bool { + r := strings.TrimSpace(reply) + if !strings.HasPrefix(strings.ToUpper(r), "KY") { + return false + } + for _, c := range r[2:] { + switch c { + case '0': + return false + case '1': + return true + case ' ', ';': + continue + default: + return false // not a status digit — don't block on it + } + } + return false +} + // filterYaesuCW upper-cases and strips what the keyer cannot send. func filterYaesuCW(text string) string { var b strings.Builder diff --git a/internal/cat/yaesu_cw_test.go b/internal/cat/yaesu_cw_test.go index f28f2ef..50db9fb 100644 --- a/internal/cat/yaesu_cw_test.go +++ b/internal/cat/yaesu_cw_test.go @@ -61,3 +61,34 @@ func TestYaesuCWChunking(t *testing.T) { t.Errorf("chunks rejoin to %q, want %q", joined, msg) } } + +// Reading the KY status reply. +// +// The first version demanded exactly "KY0;" with the digit at byte 2, and on a +// real FTDX10 that refused to send at all: the reply was phrased differently, +// every poll read as "still full", and after four seconds the send gave up +// having keyed nothing. Hence the asymmetry — only a clear "1" blocks. +func TestYaesuCWBufferFull(t *testing.T) { + cases := []struct { + reply string + full bool + }{ + {"KY0;", false}, + {"KY1;", true}, + {"KY 0;", false}, // a space before the digit + {"KY 1;", true}, + {"ky1;", true}, // lower case + {" KY0; ", false}, + // Shapes we have no sample of must NOT block: silence with no explanation + // is a worse failure than sending a moment early. + {"KY;", false}, + {"", false}, + {"FA014074000;", false}, // another command's reply arriving first + {"KYX;", false}, + } + for _, c := range cases { + if got := yaesuCWBufferFull(c.reply); got != c.full { + t.Errorf("yaesuCWBufferFull(%q) = %v, want %v", c.reply, got, c.full) + } + } +}