fix(kenwood): KY takes a FIXED 24 characters, not a string

CW over CAT never worked on a Kenwood. The KY implementation was written against
Elecraft's, which accepts a string of any length, so "KY OH5CX;" went out and a
TS-590SG answered "?;". OpsLog read that as "this radio refuses CW over CAT" and
told the operator to fit a serial keyer — advice that was wrong, and expensive.

The TS-590 manual is explicit: P2 has a fixed length of 24, blanks are filled
with spaces, and those spaces are not keyed. So the fix costs nothing on air; it
is simply the shape the command has. Elecraft stays variable-length, where
padding would key the trailing spaces as word gaps.

The semicolon is also gone from the allowed CW characters. It TERMINATES a CAT
frame, the manual forbids it in P2, and one in a macro would have closed the
command early and left the rest of the message to be read as commands.

Found from a log and a manual page, not from a rig: nobody here owns a Kenwood.
What is proven is the frame shape; that a TS-590SG then keys it still needs the
operator to confirm.
This commit is contained in:
2026-08-11 08:55:49 +02:00
parent cfdd24d52e
commit d2d64706f5
3 changed files with 71 additions and 4 deletions
+25 -2
View File
@@ -29,7 +29,30 @@ const kenwoodCWChunk = 24
// kenwoodCWAllowed is what the keyer can send; anything else is dropped, since an
// unsupported byte can abort the buffer and lose the rest of the message.
const kenwoodCWAllowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 /?.,-=+:;()"
// The semicolon is deliberately absent: it TERMINATES a CAT frame. The TS-590
// manual says so outright for P2, and one in a macro would close the command
// early and leave the rest of the text to be read as commands of its own.
const kenwoodCWAllowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 /?.,-=+:()"
// padCW pads a piece to the fixed 24-character P2 a Kenwood demands.
//
// This is where CW over CAT failed on a TS-590SG. The KY command was written
// against Elecraft's, which takes a variable-length string, so "KY OH5CX;" went
// out and the radio answered "?;" — read as "this rig refuses CW over CAT", and
// the operator was told to go and buy a serial keyer.
//
// The TS-590 manual is explicit: P2 has a FIXED length of 24, and characters
// left blank are filled with spaces which are NOT keyed. So the padding costs
// nothing on air — it is simply the shape the command has to have.
//
// Elecraft is left variable-length: it accepts short strings, and padding there
// would key the trailing spaces as word gaps.
func (k *Kenwood) padCW(chunk string) string {
if k.elecraft || len(chunk) >= kenwoodCWChunk {
return chunk
}
return chunk + strings.Repeat(" ", kenwoodCWChunk-len(chunk))
}
// SendCW queues a message on the rig's keyer, fed in 24-character pieces, waiting
// for buffer room between pieces so a long macro doesn't lose its tail.
@@ -51,7 +74,7 @@ func (k *Kenwood) SendCW(text string) error {
chunk := msg[:n]
msg = msg[n:]
k.waitCWBuffer(3 * time.Second)
if err := k.write("KY " + chunk + ";"); err != nil {
if err := k.write("KY " + k.padCW(chunk) + ";"); err != nil {
return err
}
if err := k.afterKY(); err != nil {
+42
View File
@@ -0,0 +1,42 @@
package cat
import (
"strings"
"testing"
)
// The TS-590 wants a FIXED 24-character P2. Sending the bare text is what made
// CW over CAT fail on a real TS-590SG: "KY OH5CX;" came back "?;", which OpsLog
// reported as "this radio rejected CW over CAT" and sent the operator off to buy
// a serial keyer he did not need.
func TestKenwoodCWPadsToTheFixedWidth(t *testing.T) {
k := &Kenwood{}
got := k.padCW("OH5CX")
if len(got) != kenwoodCWChunk {
t.Errorf("padCW(%q) is %d chars, want exactly %d — the rig rejects anything else",
"OH5CX", len(got), kenwoodCWChunk)
}
if got[:5] != "OH5CX" || strings.TrimRight(got, " ") != "OH5CX" {
t.Errorf("padCW = %q — the text must be intact and the rest spaces", got)
}
// A full piece is already the right width and must not grow.
full := strings.Repeat("A", kenwoodCWChunk)
if k.padCW(full) != full {
t.Error("a full 24-character piece was padded further")
}
// Elecraft takes variable length, and padding there would key the trailing
// spaces as word gaps.
e := &Kenwood{elecraft: true}
if e.padCW("OH5CX") != "OH5CX" {
t.Errorf("Elecraft piece was padded: %q", e.padCW("OH5CX"))
}
}
// ";" terminates a CAT frame. The manual forbids it in P2, and one arriving in a
// macro would close the command early and leave the rest to be read as commands.
func TestKenwoodCWDropsTheSemicolon(t *testing.T) {
if got := filterKenwoodCW("CQ; DE OH5CX"); strings.Contains(got, ";") {
t.Errorf("filterKenwoodCW kept a semicolon: %q", got)
}
}