The log settled it: the rig answers "?;" — its "unknown command" — to KY; and to MG;. So the keyer status query cannot work on this model, and every send spent four seconds waiting for an answer that was never coming. "?;" is now recognised as what it is. A command refused once is never asked again: models implement different subsets, and re-asking costs a 600 ms timeout on every slow beat for a control that will never answer — which is also why the panel felt sluggish. Without a buffer status there is still a real constraint: one KY command carries 24 characters and the rig DROPS the rest silently. So the send is now paced by how long the text takes to key, from PARIS timing at the rig's own speed. A long macro goes out complete instead of losing its tail. That leaves the question the log cannot answer: whether KY <text>; itself is accepted. If the rig also replies "?;" to it, the CAT menu's PC KEYING setting is the next suspect — but nothing in the code will be guessing at it.
123 lines
4.0 KiB
Go
123 lines
4.0 KiB
Go
package cat
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Pacing when the rig will not report its buffer.
|
|
//
|
|
// An FTDX10 answers "?;" to KY;, so there is nothing to wait on and the send has
|
|
// to be spaced by how long the text takes to key — otherwise the second piece
|
|
// overruns the 24-character buffer and the rig drops it silently.
|
|
func TestYaesuCWDuration(t *testing.T) {
|
|
// PARIS: at 20 wpm a dit is 60 ms and a character averages 10 dits, so 24
|
|
// characters take about 14.4 s.
|
|
got := yaesuCWDuration("ABCDEFGHIJKLMNOPQRSTUVWX", 20)
|
|
if got < 13*time.Second || got > 16*time.Second {
|
|
t.Errorf("24 chars at 20 wpm = %s, expected about 14.4s", got)
|
|
}
|
|
// Twice the speed, half the time.
|
|
fast := yaesuCWDuration("ABCDEFGHIJKLMNOPQRSTUVWX", 40)
|
|
if fast >= got || fast < got/3 {
|
|
t.Errorf("40 wpm = %s vs 20 wpm = %s — should be about half", fast, got)
|
|
}
|
|
// A nonsense speed must not produce a zero wait (send everything at once) nor
|
|
// an absurd one (the operator waiting minutes).
|
|
if d := yaesuCWDuration("TEST", 0); d <= 0 || d > 10*time.Second {
|
|
t.Errorf("4 chars at an unknown speed = %s, want a sane fallback", d)
|
|
}
|
|
if d := yaesuCWDuration("", 20); d != 0 {
|
|
t.Errorf("empty text = %s, want 0", d)
|
|
}
|
|
}
|