Files
rouggy 95482ec1d8 feat(cat): per-rig Kenwood/Elecraft data-mode choice (USB / MD6 / keep)
No single Kenwood mode digit fits every rig for a data mode: a K3/K4 wants
DATA (MD6), a TS-590SG/TS-990S data mode is a USB modifier set on the rig,
and MD6 on a plain Kenwood is FSK/RTTY. So the operator now chooses in
Settings → CAT: USB (default), DATA (MD6), or leave the rig's mode unchanged.

Only soundcard/data modes (FT8, PSK, JT…) follow this; RTTY/FSK stays its own
native mode, and voice/CW are untouched (isKenwoodDataMode + test).
2026-08-04 18:14:26 +02:00

149 lines
5.2 KiB
Go

package cat
import "testing"
// The IF status frame, read by POSITION.
//
// One frame carries frequency, TX state, mode, VFO and split, which is why it is
// the poll. Every field is a fixed offset, so a length check comes first: a
// truncated read would otherwise index into the wrong characters and yield a
// plausible-looking wrong frequency — the worst kind, because it reaches the log.
//
// The layout is the one internal/catemu already EMITS to satisfy an ACOM
// amplifier, so these two halves of the repository agree by construction.
func TestParseKenwoodIF(t *testing.T) {
// IF | freq(11) | step(4) | RIT(±5) | 3 | mem(2) | rx/tx | mode | VFO | scan |
// split | tone | tone#(2) | shift | ; → 38 characters
const rx20mUSB = "IF00014250000" + "0000" + "+00000" + "000" + "00" + "0" + "2" + "0" + "0" + "0" + "0" + "00" + "0" + ";"
if len(rx20mUSB) != 38 {
t.Fatalf("the test's own frame is %d chars, not 38 — fix the fixture first", len(rx20mUSB))
}
f, ok := parseKenwoodIF(rx20mUSB)
if !ok {
t.Fatalf("a valid frame was rejected: %q", rx20mUSB)
}
if f.FreqHz != 14250000 {
t.Errorf("frequency = %d, want 14250000", f.FreqHz)
}
if f.Mode != '2' {
t.Errorf("mode = %q, want '2' (USB)", f.Mode)
}
if f.VFO != "A" || f.Split || f.TX {
t.Errorf("got VFO=%s split=%v tx=%v — want A, simplex, receiving", f.VFO, f.Split, f.TX)
}
// Same frame transmitting, on VFO B, split, in CW.
const txSplitB = "IF00007030000" + "0000" + "+00000" + "000" + "00" + "1" + "3" + "1" + "0" + "1" + "0" + "00" + "0" + ";"
f2, ok := parseKenwoodIF(txSplitB)
if !ok {
t.Fatalf("valid frame rejected: %q", txSplitB)
}
if !f2.TX || f2.Mode != '3' || f2.VFO != "B" || !f2.Split {
t.Errorf("got tx=%v mode=%q vfo=%s split=%v — want transmitting, CW, B, split",
f2.TX, f2.Mode, f2.VFO, f2.Split)
}
// Rejections: anything that would make position-reading unsafe.
for _, bad := range []string{
"",
"IF;", // query echoed with no payload
"IF0001425000;", // short — the trap this guards against
"FA00014250000;", // another command's reply
"IF0001425000x0000+00000000000203000000;", // non-numeric frequency
} {
if _, ok := parseKenwoodIF(bad); ok {
t.Errorf("accepted a frame it should have refused: %q", bad)
}
}
}
// FA/FB carry ELEVEN digits on Kenwood where Yaesu uses nine — the single most
// likely place to copy the Yaesu backend and be wrong by a factor of a hundred.
func TestParseKenwoodFreq(t *testing.T) {
cases := []struct {
reply, prefix string
want int64
ok bool
}{
{"FA00014250000;", "FA", 14250000, true},
{"FB00007030000;", "FB", 7030000, true},
{"FA00000474000;", "FA", 474000, true}, // 630 m — leading zeros must not truncate
{"FA10368000000;", "FA", 10368000000, true}, // 3 cm
{"FA00014250000;", "FB", 0, false}, // wrong VFO is never silently accepted
{"FA;", "FA", 0, false},
{"FAxxxxxxxxxxx;", "FA", 0, false},
{"", "FA", 0, false},
}
for _, c := range cases {
got, ok := parseKenwoodFreq(c.reply, c.prefix)
if got != c.want || ok != c.ok {
t.Errorf("parseKenwoodFreq(%q,%q) = %d,%v — want %d,%v", c.reply, c.prefix, got, ok, c.want, c.ok)
}
}
}
// The sideband follows the frequency by worldwide convention. A backend that
// puts USB on 40 m makes every SSB QSO in the log wrong, which is why this is
// pinned rather than left to the rig.
func TestKenwoodModeDigit(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'}, // an explicit choice wins over the convention
{"USB", 7150000, '2'},
{"CW", 7030000, '3'},
{"RTTY", 14080000, '6'},
{"AM", 7150000, '5'},
{"FM", 145000000, '4'},
{"FT8", 7074000, '2'}, // data is ALWAYS USB, even below 10 MHz (K3 "DATA REV" otherwise)
{"FT8", 14074000, '2'}, // …and above
{"", 14074000, 0}, // nothing to set
}
for _, c := range cases {
if got := kenwoodModeDigit(c.mode, c.hz); got != c.want {
t.Errorf("kenwoodModeDigit(%q, %d) = %q, want %q", c.mode, c.hz, got, c.want)
}
}
}
// The DATA-mode preference only applies to soundcard/data modes, never to voice,
// CW or native RTTY — getting this wrong would hijack an SSB or CW mode change.
func TestIsKenwoodDataMode(t *testing.T) {
data := []string{"FT8", "FT4", "PSK31", "JT65", "DATA", "DIGITAL", "OLIVIA"}
notData := []string{"", "LSB", "USB", "SSB", "CW", "CW-R", "FM", "AM", "RTTY", "FSK", "RTTY-R"}
for _, m := range data {
if !isKenwoodDataMode(m) {
t.Errorf("isKenwoodDataMode(%q) = false, want true", m)
}
}
for _, m := range notData {
if isKenwoodDataMode(m) {
t.Errorf("isKenwoodDataMode(%q) = true, want false", m)
}
}
}
// What the log records for each mode digit the rig reports.
func TestKenwoodModeToADIF(t *testing.T) {
cases := []struct {
d byte
want string
}{
{'1', "LSB"}, {'2', "USB"},
{'3', "CW"}, {'7', "CW"}, // CW-R is still CW in the log
{'4', "FM"}, {'5', "AM"},
{'6', "RTTY"}, {'9', "RTTY"}, // FSK-R likewise
{'0', ""}, // unknown → say nothing rather than guess
}
for _, c := range cases {
if got := kenwoodModeToADIF(c.d, "FT8"); got != c.want {
t.Errorf("kenwoodModeToADIF(%q) = %q, want %q", c.d, got, c.want)
}
}
}