Clicking a macro dropped the CAT for a few seconds, keyed nothing, then came back. ask() returned the first ';'-terminated frame it saw, whatever command it belonged to. KY produces NO reply, so the next query — FA; from the poll loop — collected a leftover frame, failed to parse it as a frequency, and ReadState reported an error. The Manager reads that as "lost the rig": disconnect, wait, reconnect. Hence the drop and the automatic recovery a few seconds later. Replies are now matched to the command that asked for them: anything else is discarded and logged, so a stray frame costs one log line instead of the link. This also explains the silence — the send never got a clean run at the port while the backend was being torn down under it.
133 lines
4.1 KiB
Go
133 lines
4.1 KiB
Go
package cat
|
|
|
|
import "testing"
|
|
|
|
func TestParseYaesuFreq(t *testing.T) {
|
|
cases := []struct {
|
|
reply, prefix string
|
|
want int64
|
|
ok bool
|
|
}{
|
|
{"FA014074000;", "FA", 14074000, true},
|
|
{"FB007100000;", "FB", 7100000, true},
|
|
{"FA000474000;", "FA", 474000, true}, // 630 m — leading zeros must not truncate
|
|
{"FA010368000000;", "FA", 10368000000, true},
|
|
{"FB;", "FB", 0, false}, // query echoed back with no value
|
|
{"FA014074000;", "FB", 0, false}, // wrong VFO — never silently accepted
|
|
{"", "FA", 0, false},
|
|
{"FAxxxxxxxxx;", "FA", 0, false},
|
|
}
|
|
for _, c := range cases {
|
|
got, ok := parseYaesuFreq(c.reply, c.prefix)
|
|
if got != c.want || ok != c.ok {
|
|
t.Errorf("parseYaesuFreq(%q,%q) = %d,%v — want %d,%v", c.reply, c.prefix, got, ok, c.want, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The split rules. Getting these wrong writes a WRONG TX frequency into the log,
|
|
// which is why an ambiguous state resolves to "not split" rather than to a
|
|
// guess — the same principle the OmniRig backend arrived at the hard way.
|
|
func TestResolveYaesuVFOs(t *testing.T) {
|
|
const a, b = 14074000, 14100000
|
|
cases := []struct {
|
|
name string
|
|
fa, fb int64
|
|
vfo string
|
|
split bool
|
|
wantTX, wantRX int64
|
|
wantSplit bool
|
|
}{
|
|
{"simplex on A", a, b, "A", false, a, 0, false},
|
|
{"simplex on B", a, b, "B", false, b, 0, false},
|
|
{"split, listening on A → TX on B", a, b, "A", true, b, a, true},
|
|
{"split, listening on B → TX on A", a, b, "B", true, a, b, true},
|
|
{"split flag but the other VFO is unread", a, 0, "A", true, a, 0, false},
|
|
{"split flag but both VFOs identical", a, a, "A", true, a, 0, false},
|
|
}
|
|
for _, c := range cases {
|
|
tx, rx, sp := resolveYaesuVFOs(c.fa, c.fb, c.vfo, c.split)
|
|
if tx != c.wantTX || rx != c.wantRX || sp != c.wantSplit {
|
|
t.Errorf("%s: got tx=%d rx=%d split=%v — want tx=%d rx=%d split=%v",
|
|
c.name, tx, rx, sp, c.wantTX, c.wantRX, c.wantSplit)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ST and FT say different things and must not be read the same way — see the
|
|
// comment on yaesuSplitOn.
|
|
func TestYaesuSplitReply(t *testing.T) {
|
|
cases := []struct {
|
|
reply, cmd string
|
|
want bool
|
|
}{
|
|
{"ST1;", "ST", true},
|
|
{"ST0;", "ST", false},
|
|
{"FT1;", "FT", true},
|
|
{"FT0;", "FT", false},
|
|
{"ST1;", "FT", false}, // reply for the other command — not accepted
|
|
{"ST", "ST", false}, // truncated
|
|
{"", "ST", false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := yaesuSplitOn(c.reply, c.cmd); got != c.want {
|
|
t.Errorf("yaesuSplitOn(%q,%q) = %v, want %v", c.reply, c.cmd, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The sideband follows the frequency, worldwide convention — a CAT backend that
|
|
// puts USB on 40 m makes every SSB QSO wrong.
|
|
func TestYaesuModeDigit(t *testing.T) {
|
|
cases := []struct {
|
|
mode string
|
|
hz int64
|
|
want byte
|
|
}{
|
|
{"SSB", 7150000, '1'}, // LSB below 10 MHz
|
|
{"SSB", 14250000, '2'}, // USB above
|
|
{"LSB", 14250000, '1'}, // explicit wins over the convention
|
|
{"USB", 7150000, '2'},
|
|
{"CW", 7030000, '3'},
|
|
{"RTTY", 14080000, '6'},
|
|
{"AM", 7150000, '5'},
|
|
{"FM", 145000000, '4'},
|
|
{"FT8", 7074000, '8'}, // DATA-LSB
|
|
{"FT8", 14074000, 'C'}, // DATA-USB
|
|
{"JS8", 14078000, 'C'}, // any unknown digital rides on DATA
|
|
{"", 14074000, 0}, // nothing to set
|
|
}
|
|
for _, c := range cases {
|
|
if got := yaesuModeDigit(c.mode, c.hz); got != c.want {
|
|
t.Errorf("yaesuModeDigit(%q, %d) = %q, want %q", c.mode, c.hz, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A reply belongs to the command that asked for it.
|
|
//
|
|
// Without this, a CW macro knocked the CAT link over: KY produces no reply, so
|
|
// the poll loop's next FA; collected a leftover frame, failed to parse it as a
|
|
// frequency, and the Manager treated that as "lost the rig" and reconnected —
|
|
// the CAT dropping for a few seconds on every macro click.
|
|
func TestYaesuCmdPrefix(t *testing.T) {
|
|
cases := []struct{ cmd, want string }{
|
|
{"FA;", "FA"},
|
|
{"FB;", "FB"},
|
|
{"MD0;", "MD"},
|
|
{"KY;", "KY"},
|
|
{"KY CQ TEST;", "KY"},
|
|
{"SM0;", "SM"},
|
|
{"RM4;", "RM"},
|
|
{"AG0;", "AG"},
|
|
{"TX;", "TX"},
|
|
{"", ""},
|
|
{";", ""},
|
|
}
|
|
for _, c := range cases {
|
|
if got := cmdPrefix(c.cmd); got != c.want {
|
|
t.Errorf("cmdPrefix(%q) = %q, want %q", c.cmd, got, c.want)
|
|
}
|
|
}
|
|
}
|