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) } }