Files
OpsLog/internal/cat/xiegu_test.go
T
rouggy ef84b04c99 fix: CI-V frames read at the wrong offset decoded as plausible frequencies
From F4JND's log: a rig sitting on 145.550 MHz FM produced status lines reading
16445550000, 8364550000, 5817408364 and 12640 Hz between good readings — the last
of those even resolved to a 6 cm band.

BCDToFreq treated the nibbles 0x0A..0x0F as the digits 10..15, so a byte stream
that had lost frame sync still decoded into a number. The same log shows why sync
was lost: PTT round trips timing out and a scope stream the rig kept rejecting
(0x27), both competing with the poll on one serial line.

Non-decimal nibbles are now refused. Each caller keeps its last good value rather
than publishing noise: the frequency reader returns an error, the sub-VFO reader
returns not-ok, and the scope keeps its previous edges. Garbage that LOOKS like a
frequency is worse than a refused frame — it reaches the display, the band slots
and eventually the log, and it is the operator who then has to disprove it.

The test builds its fixture with FreqToBCD rather than by hand, because my
hand-written one was byte-reversed and the test caught it.

This does NOT explain the white screen that was also reported, and I have not
claimed it does: it removes one source of absurd values reaching the UI, which is
worth doing on its own evidence.
2026-07-30 14:05:30 +02:00

85 lines
2.7 KiB
Go

package cat
import (
"testing"
"hamlog/internal/cat/civ"
)
// The Xiegu mode table is SHORTER than Icom's: LSB, USB, AM, CW, CWR and
// nothing else. The interesting cases are the ones with no equivalent — a
// digital mode must land on plain sideband rather than be refused, because
// refusing it would leave the rig on whatever it was and silently log the wrong
// mode.
func TestXieguModeByte(t *testing.T) {
cases := []struct {
mode string
hz int64
want byte
ok bool
}{
{"SSB", 7150000, 0x00, true}, // LSB below 10 MHz
{"SSB", 14250000, 0x01, true}, // USB above
{"LSB", 14250000, 0x00, true}, // explicit beats the convention
{"USB", 7150000, 0x01, true},
{"AM", 7150000, 0x02, true},
{"CW", 7030000, 0x03, true},
{"CWR", 7030000, 0x07, true},
{"FT8", 7074000, 0x00, true}, // no data mode on this rig → LSB
{"FT8", 14074000, 0x01, true}, // → USB
{"RTTY", 14080000, 0x01, true},
{"FM", 145000000, 0x01, true}, // no FM either; sideband is the honest fallback
{"", 14074000, 0x00, false},
}
for _, c := range cases {
got, ok := xieguModeByte(c.mode, c.hz)
if got != c.want || ok != c.ok {
t.Errorf("xieguModeByte(%q, %d) = 0x%02X,%v — want 0x%02X,%v", c.mode, c.hz, got, ok, c.want, c.ok)
}
}
}
// The Xiegu mode bytes must decode through the shared Icom table, since that is
// the whole reason this backend reuses internal/cat/civ instead of its own
// codec. If the two ever disagree, the radio would be set to one mode and read
// back as another.
func TestXieguModesRoundTripThroughCIV(t *testing.T) {
cases := []struct {
mode string
adif string
}{
// ADIF has no LSB/USB distinction — both sidebands ARE the SSB mode, and
// that is what gets logged. The sideband still matters to the radio, which
// is why xieguModeByte picks it from the frequency.
{"LSB", "SSB"},
{"USB", "SSB"},
{"AM", "AM"},
{"CW", "CW"},
}
for _, c := range cases {
b, ok := xieguModeByte(c.mode, 14200000)
if !ok {
t.Fatalf("xieguModeByte(%q) refused", c.mode)
}
if got := civ.ModeToADIF(b, false); got != c.adif {
t.Errorf("%s → 0x%02X → %q, want %q", c.mode, b, got, c.adif)
}
}
}
// Frequency encoding is shared with Icom, and it is the one field where a byte
// out of place mistunes the radio silently. Pinned here at the boundaries a
// Xiegu actually covers (it is an HF rig, so the 5-byte BCD upper bytes are 0).
func TestXieguFrequencyEncoding(t *testing.T) {
for _, hz := range []int64{1840000, 7074000, 14074000, 28500000, 50313000} {
b := civ.FreqToBCD(hz)
if len(b) != 5 {
t.Fatalf("FreqToBCD(%d) produced %d bytes, want 5", hz, len(b))
}
got, ok := civ.BCDToFreq(b)
if !ok || got != hz {
t.Errorf("round trip %d → % X → %d (ok=%v)", hz, b, got, ok)
}
}
}