From d2d64706f5deec2cff6698603d2aa9d31e39437e Mon Sep 17 00:00:00 2001 From: Gregory Salaun Date: Tue, 11 Aug 2026 08:55:49 +0200 Subject: [PATCH] fix(kenwood): KY takes a FIXED 24 characters, not a string MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- changelog.json | 6 +++-- internal/cat/kenwood_cw.go | 27 +++++++++++++++++++-- internal/cat/kenwood_cw_test.go | 42 +++++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 internal/cat/kenwood_cw_test.go diff --git a/changelog.json b/changelog.json index 58a3f4e..0dce8fb 100644 --- a/changelog.json +++ b/changelog.json @@ -3,10 +3,12 @@ "version": "0.24.5", "date": "", "en": [ - "Digital decodes: a station's grid square could be logged as its callsign. An unrecognised word after CQ made the parser skip a slot, and a four-character grid passes every shape test a callsign does, so \"CQ FOO JN36\" was read as a contact with JN36 — spotted, coloured and counted like any other station. A grid in the callsign position is now refused." + "Digital decodes: a station's grid square could be logged as its callsign. An unrecognised word after CQ made the parser skip a slot, and a four-character grid passes every shape test a callsign does, so \"CQ FOO JN36\" was read as a contact with JN36 — spotted, coloured and counted like any other station. A grid in the callsign position is now refused.", + "CW over CAT now works on a Kenwood. The KY command was written against Elecraft's, which takes a string of any length; a Kenwood requires exactly 24 characters, so every message was refused and OpsLog reported that the radio would not do CW over CAT at all — sending operators to buy a serial keyer they did not need. A semicolon is also stripped from CW text now: it ends a CAT frame, so one in a macro cut the command short." ], "fr": [ - "Décodes digitaux : le carré locator d une station pouvait être enregistré comme son indicatif. Un mot non reconnu après CQ faisait sauter un cran au parseur, et un grid de quatre caractères passe tous les tests de forme d un indicatif — « CQ FOO JN36 » était donc lu comme un contact avec JN36, spotté, coloré et compté comme n importe quelle autre station. Un grid à la place de l indicatif est maintenant refusé." + "Décodes digitaux : le carré locator d une station pouvait être enregistré comme son indicatif. Un mot non reconnu après CQ faisait sauter un cran au parseur, et un grid de quatre caractères passe tous les tests de forme d un indicatif — « CQ FOO JN36 » était donc lu comme un contact avec JN36, spotté, coloré et compté comme n importe quelle autre station. Un grid à la place de l indicatif est maintenant refusé.", + "Le CW par CAT fonctionne enfin sur un Kenwood. La commande KY avait été écrite d après celle d Elecraft, qui accepte une chaîne de longueur libre ; un Kenwood en exige exactement 24 caractères, donc chaque message était refusé et OpsLog annonçait que la radio ne savait pas faire de CW par CAT — envoyant des opérateurs acheter un keyer série dont ils n avaient pas besoin. Le point-virgule est aussi retiré du texte CW : il termine une trame CAT, donc un seul dans une macro coupait la commande." ] }, { diff --git a/internal/cat/kenwood_cw.go b/internal/cat/kenwood_cw.go index 3357784..e2ce2a7 100644 --- a/internal/cat/kenwood_cw.go +++ b/internal/cat/kenwood_cw.go @@ -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 { diff --git a/internal/cat/kenwood_cw_test.go b/internal/cat/kenwood_cw_test.go new file mode 100644 index 0000000..8eea68d --- /dev/null +++ b/internal/cat/kenwood_cw_test.go @@ -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) + } +}