feat: native Yaesu CAT backend (FTDX10 / FTDX101), no OmniRig
Every Yaesu fault reported so far came from OmniRig's interpretation layer, not from the radio: a rig file that never exposes the VFO, a Freq property meaning A on one model and B on another, a split flag that alternates between polls. This talks to the rig directly, so what the radio answers is what is shown. Modern Yaesu CAT is plain ASCII with a ';' terminator — FA/FB for the VFOs, MD0 for the mode, VS for the selected VFO, TX to key. Frequency is written to the VFO the operator is actually on, not always to A, which is the failure that made a display disagree with the radio. Two things are genuinely uncertain across the family and are treated as such rather than guessed. SPLIT is read through ST, then FT if the rig ignores ST — whichever answers wins and the choice is remembered, because the two commands say DIFFERENT things (a split flag vs which VFO transmits). If neither answers, split is reported OFF and the fact is logged, rather than invented. Unknown model ids and mode bytes are logged raw for the same reason. Split resolution, frequency parsing and the mode mapping are pure functions with a table test — the OmniRig equivalent is where every Yaesu bug lived, and it had no test until late. Written from the CAT reference; NOT yet verified on a radio.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user