feat: CW through the Yaesu keyer — fifth CW engine
The radio has a keyer and a command to feed it (KY), so an FTDX10 needs no WinKeyer and no second cable, exactly as the Icom CI-V and Flex CWX engines already do. The rig keys with its own timing, which is why the spacing is right where a PC keying a line through USB latency drifts. Text is filtered to what the keyer can actually send: an unsupported byte does not produce an error on a Yaesu, it can abort the whole buffer, so the rest of a macro would vanish silently. It is then fed in 24-character pieces, waiting for room between them — the rig DROPS what does not fit, again with no error, so a contest CQ would lose its tail. STOP is the honest gap. Yaesu documents no buffer-clear, so it drops the transmitter (TX0) instead: nothing queued reaches the air, which is what Escape means to an operator. It deliberately does NOT send "KY0;" — a plausible-looking clear that the rig would read as the CHARACTER zero and transmit. A test caught a real one on the way: tabs and newlines were dropped as "unsupported", gluing the words either side together, so a macro written on two lines went out as CQCQ. Whitespace now becomes a word gap before filtering. Settings warn when the Yaesu keyer is selected without the Yaesu CAT backend — otherwise it simply never keys, with nothing on screen saying why. Send path follows the CAT reference and how Hamlib drives these rigs; NOT yet verified on the air.
This commit is contained in:
@@ -0,0 +1,63 @@
|
||||
package cat
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// What reaches the rig's keyer.
|
||||
//
|
||||
// An unsupported byte does not produce an error on a Yaesu — it can abort the
|
||||
// whole buffer, so the rest of the message is lost silently. Filtering is
|
||||
// therefore part of correctness, not tidiness.
|
||||
func TestFilterYaesuCW(t *testing.T) {
|
||||
cases := []struct{ in, want string }{
|
||||
{"cq cq de f4bpo", "CQ CQ DE F4BPO"},
|
||||
{"F4BPO/P", "F4BPO/P"},
|
||||
{"599 tu 73", "599 TU 73"},
|
||||
// Accents and symbols the keyer has no code for.
|
||||
{"héllo", "HLLO"},
|
||||
{"a*b#c", "ABC"},
|
||||
// Runs of whitespace collapse: the rig sends each space as a word gap, so
|
||||
// three in a row is three gaps and the message crawls.
|
||||
{"cq cq", "CQ CQ"},
|
||||
{" padded ", "PADDED"},
|
||||
{"\tcq\ncq\t", "CQ CQ"},
|
||||
{"", ""},
|
||||
{" ", ""},
|
||||
}
|
||||
for _, c := range cases {
|
||||
if got := filterYaesuCW(c.in); got != c.want {
|
||||
t.Errorf("filterYaesuCW(%q) = %q, want %q", c.in, got, c.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Long macros have to be split: one KY command carries 24 characters and the rig
|
||||
// DROPS what does not fit rather than reporting an error, so a contest CQ would
|
||||
// lose its tail with no sign anywhere.
|
||||
func TestYaesuCWChunking(t *testing.T) {
|
||||
msg := filterYaesuCW("cq cq cq de f4bpo f4bpo f4bpo pse k")
|
||||
if len(msg) <= yaesuCWChunk {
|
||||
t.Fatalf("test message is %d chars — too short to exercise chunking", len(msg))
|
||||
}
|
||||
var chunks []string
|
||||
for rest := msg; len(rest) > 0; {
|
||||
n := yaesuCWChunk
|
||||
if len(rest) < n {
|
||||
n = len(rest)
|
||||
}
|
||||
chunks = append(chunks, rest[:n])
|
||||
rest = rest[n:]
|
||||
}
|
||||
for i, c := range chunks {
|
||||
if len(c) > yaesuCWChunk {
|
||||
t.Errorf("chunk %d is %d chars, over the %d limit", i, len(c), yaesuCWChunk)
|
||||
}
|
||||
}
|
||||
// Nothing added, nothing lost: the message the operator typed is what the
|
||||
// keyer receives.
|
||||
if joined := strings.Join(chunks, ""); joined != msg {
|
||||
t.Errorf("chunks rejoin to %q, want %q", joined, msg)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user