Files
OpsLog/internal/cat/yaesu_test.go
T
rouggy f3807c21ba fix: FTDX101 answers the VFO query with two digits, and only the first was read
The log named the cause in one line: yaesu: receive VFO is read through FR
(answered "FR01;"). An FTDX10 answers "FR0;" — one digit — so reading the digit
straight after the prefix worked there and took the 0 of "01" on an FTDX101.

The rig was on SUB and OpsLog said MAIN, which is the reported "Sub shows the main
frequency". It also explains the second half: split is "TX VFO differs from RX
VFO", so a receive VFO that alternated between right and wrong made the split flag
flip between consecutive polls and swap freq and rx in the log.

The state is now taken from the LAST digit before the terminator, which is correct
on both forms, and the same reading applies to ST/FT/VS rather than leaving the
next two-digit rig to be found in the field.

On the operator's question of whether freq should simply be the active VFO: no —
freq is the TRANSMIT frequency by ADIF convention (FREQ is the QSO frequency,
FREQ_RX the receive one, set only when split). In simplex they are the same, so it
does read as the active VFO; in split it must stay the transmit VFO or the log
records the wrong frequency. What the operator saw was this bug, not that rule.
2026-07-29 18:35:47 +02:00

267 lines
9.0 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 reading them alike inverted the split
// display on an FTDX101 (F4NBZ, 2026-07-29): the panel showed split ON with the
// radio OFF and the reverse.
//
// ST is a split FLAG — ST1 means split, whatever VFO is in use.
// FT names the TX VFO — FT0 = transmit on A, FT1 = transmit on B.
//
// Split is on when the rig transmits on a DIFFERENT VFO from the one it listens
// to, so FT has to be compared with the current VFO. That is why the same model
// behaved correctly for one operator and backwards for another: one was on MAIN,
// the other on SUB.
func TestYaesuSplitReply(t *testing.T) {
cases := []struct {
reply, cmd, vfo string
want bool
}{
// The flag is absolute.
{"ST1;", "ST", "A", true},
{"ST0;", "ST", "A", false},
{"ST1;", "ST", "B", true},
{"ST0;", "ST", "B", false},
// Listening on A: transmit on B is split, transmit on A is not.
{"FT1;", "FT", "A", true},
{"FT0;", "FT", "A", false},
// Listening on B: exactly the opposite — the reported inversion.
{"FT0;", "FT", "B", true},
{"FT1;", "FT", "B", false},
// The pair enums the Yaesus also report start with the listening VFO.
{"FT0;", "FT", "BA", true},
{"FT1;", "FT", "AB", true},
{"ST1;", "FT", "A", false}, // reply for the other command — not accepted
{"ST", "ST", "A", false}, // truncated
{"", "ST", "A", false},
}
for _, c := range cases {
if got := yaesuSplitFromReply(c.reply, c.cmd, c.vfo); got != c.want {
t.Errorf("yaesuSplitFromReply(%q, %q, vfo=%q) = %v, want %v",
c.reply, c.cmd, c.vfo, 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)
}
}
}
// Writing split is as asymmetric as reading it.
//
// ST takes the state directly. FT sets which VFO TRANSMITS, so "split on" means
// transmit on the OTHER VFO from the one being listened to. Sending FT1 for "on"
// regardless is right only on VFO A — on SUB it would CLEAR the split it was
// asked to set, which is the same inversion that showed on the FTDX101 panel.
func TestYaesuSplitCommand(t *testing.T) {
cases := []struct {
cmd, vfo string
on bool
want string
}{
{"ST", "A", true, "ST1;"},
{"ST", "B", true, "ST1;"}, // the flag does not care which VFO
{"ST", "B", false, "ST0;"},
// Listening on A: split means transmit on B.
{"FT", "A", true, "FT1;"},
{"FT", "A", false, "FT0;"},
// Listening on B: split means transmit on A — the reverse.
{"FT", "B", true, "FT0;"},
{"FT", "B", false, "FT1;"},
// Pair enums start with the listening VFO.
{"FT", "BA", true, "FT0;"},
{"FT", "AB", true, "FT1;"},
}
for _, c := range cases {
if got := yaesuSplitCommand(c.cmd, c.vfo, c.on); got != c.want {
t.Errorf("yaesuSplitCommand(%q, vfo=%q, on=%v) = %q, want %q",
c.cmd, c.vfo, c.on, got, c.want)
}
}
}
// Which VFO the operator is listening on, on a rig with separate RX and TX
// selection.
//
// Reported on an FTDX101 (2026-07-29): moving RX alone to SUB displayed VFO B
// correctly, but moving BOTH RX and TX to SUB displayed VFO A — the operator was
// entirely on B and OpsLog showed the other one. FR reports the receive VFO; VS
// does not answer that question on this rig.
//
// With FR read correctly the split follows too, since split is "transmit VFO
// differs from receive VFO".
func TestYaesuReceiveVFOAndSplit(t *testing.T) {
cases := []struct {
name string
fr, ft string // replies
wantVFO string
wantSplit bool
}{
{"everything on main", "FR0;", "FT0;", "A", false},
{"RX on sub, TX still on main — split", "FR1;", "FT0;", "B", true},
{"RX and TX both on sub — NOT split", "FR1;", "FT1;", "B", false},
{"RX on main, TX on sub — split", "FR0;", "FT1;", "A", true},
}
for _, c := range cases {
vfo := "A"
if len(c.fr) >= 3 && c.fr[2] == '1' {
vfo = "B"
}
if vfo != c.wantVFO {
t.Errorf("%s: receive VFO = %s, want %s", c.name, vfo, c.wantVFO)
}
if got := yaesuSplitFromReply(c.ft, "FT", vfo); got != c.wantSplit {
t.Errorf("%s: split = %v, want %v", c.name, got, c.wantSplit)
}
}
}
// The state digit of a reply, when the parameter is not one character.
//
// An FTDX101 answers "FR01;" where an FTDX10 answers "FR0;". Reading the FIRST
// digit took the 0 of "01" as the answer, so a rig on SUB reported MAIN: OpsLog
// showed the main frequency, and — since split is "TX VFO differs from RX VFO" —
// the split flag flipped between consecutive polls, swapping freq and rx in the
// log (F4NBZ, 2026-07-29). The last digit is the state on both rigs.
func TestYaesuStateDigit(t *testing.T) {
cases := []struct {
reply, cmd string
want byte
}{
{"FR0;", "FR", '0'}, // FTDX10 form
{"FR1;", "FR", '1'},
{"FR01;", "FR", '1'}, // FTDX101 form — the bug: this read as '0'
{"FR00;", "FR", '0'},
{"ST1;", "ST", '1'},
{"FT0;", "FT", '0'},
{"VS1;", "VS", '1'},
{"FR1", "FR", '1'}, // terminator already stripped
{"ST1;", "FR", 0}, // another command's reply is never accepted
{"FR;", "FR", 0}, // query echoed with no value
{"FRx;", "FR", 0}, // not a digit
{"", "FR", 0},
}
for _, c := range cases {
if got := yaesuStateDigit(c.reply, c.cmd); got != c.want {
t.Errorf("yaesuStateDigit(%q, %q) = %q, want %q", c.reply, c.cmd, got, c.want)
}
}
// End to end: the FTDX101 two-digit form must place the operator on SUB.
if d := yaesuStateDigit("FR01;", "FR"); d != '1' {
t.Fatalf("FR01 read as %q — the operator is on SUB and must be seen there", d)
}
// And with the receive VFO right, both-on-sub is correctly NOT split.
if yaesuSplitFromReply("FT01;", "FT", "B") {
t.Error("RX and TX both on sub reported as split")
}
}