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.
207 lines
6.8 KiB
Go
207 lines
6.8 KiB
Go
package civ
|
|
|
|
import (
|
|
"bytes"
|
|
"testing"
|
|
)
|
|
|
|
func TestFreqBCDRoundTrip(t *testing.T) {
|
|
cases := []int64{0, 1, 7074000, 14250000, 28074000, 50313000, 144174000, 1296000000}
|
|
for _, hz := range cases {
|
|
b := FreqToBCD(hz)
|
|
if len(b) != 5 {
|
|
t.Fatalf("FreqToBCD(%d) len=%d, want 5", hz, len(b))
|
|
}
|
|
got, ok := BCDToFreq(b)
|
|
if !ok || got != hz {
|
|
t.Errorf("round trip %d → % X → %d (ok=%v)", hz, b, got, ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFreqBCDKnownEncoding(t *testing.T) {
|
|
// 14.250.000 Hz → little-endian BCD 00 00 25 14 00.
|
|
want := []byte{0x00, 0x00, 0x25, 0x14, 0x00}
|
|
if got := FreqToBCD(14250000); !bytes.Equal(got, want) {
|
|
t.Errorf("FreqToBCD(14250000) = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestFrame(t *testing.T) {
|
|
// Read-frequency request to a 7610 (0x98) from the controller (0xE0).
|
|
got := Frame(0x98, AddrController, CmdReadFreq)
|
|
want := []byte{0xFE, 0xFE, 0x98, 0xE0, 0x03, 0xFD}
|
|
if !bytes.Equal(got, want) {
|
|
t.Errorf("Frame = % X, want % X", got, want)
|
|
}
|
|
}
|
|
|
|
func TestScanSingleFreqResponse(t *testing.T) {
|
|
// Rig (0x98) → controller (0xE0): freq read response for 14.250 MHz.
|
|
in := Frame(AddrController, 0x98, CmdReadFreq, 0x00, 0x00, 0x25, 0x14, 0x00)
|
|
frames, consumed := Scan(in)
|
|
if consumed != len(in) {
|
|
t.Fatalf("consumed=%d, want %d", consumed, len(in))
|
|
}
|
|
if len(frames) != 1 {
|
|
t.Fatalf("got %d frames, want 1", len(frames))
|
|
}
|
|
f := frames[0]
|
|
if f.From != 0x98 || f.To != AddrController || f.Cmd != CmdReadFreq {
|
|
t.Errorf("addrs/cmd wrong: %+v", f)
|
|
}
|
|
if hz, ok := BCDToFreq(f.Data); !ok || hz != 14250000 {
|
|
t.Errorf("decoded freq %d (ok=%v), want 14250000", hz, ok)
|
|
}
|
|
}
|
|
|
|
func TestScanSkipsEchoAndKeepsPartial(t *testing.T) {
|
|
echo := Frame(0x98, AddrController, CmdReadFreq) // our outgoing (echoed back)
|
|
resp := Frame(AddrController, 0x98, CmdReadMode, ModeCW, 0x01) // a real response
|
|
buf := append(append([]byte{}, echo...), resp...)
|
|
buf = append(buf, 0xFE, 0xFE, 0x98) // a partial third frame (no FD yet)
|
|
|
|
frames, consumed := Scan(buf)
|
|
if len(frames) != 2 {
|
|
t.Fatalf("got %d frames, want 2", len(frames))
|
|
}
|
|
// The partial frame must be left unconsumed so the next read can finish it.
|
|
if consumed != len(echo)+len(resp) {
|
|
t.Errorf("consumed=%d, want %d (partial frame retained)", consumed, len(echo)+len(resp))
|
|
}
|
|
if frames[1].Cmd != CmdReadMode || len(frames[1].Data) < 1 || frames[1].Data[0] != ModeCW {
|
|
t.Errorf("second frame wrong: %+v", frames[1])
|
|
}
|
|
}
|
|
|
|
func TestModeToADIF(t *testing.T) {
|
|
cases := []struct {
|
|
m byte
|
|
data bool
|
|
want string
|
|
}{
|
|
{ModeUSB, false, "SSB"},
|
|
{ModeLSB, false, "SSB"},
|
|
{ModeUSB, true, "DATA"},
|
|
{ModeCW, false, "CW"},
|
|
{ModeCWR, false, "CW"},
|
|
{ModeRTTY, false, "RTTY"},
|
|
{ModeAM, false, "AM"},
|
|
{ModeFM, false, "FM"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := ModeToADIF(c.m, c.data); got != c.want {
|
|
t.Errorf("ModeToADIF(0x%02X, %v) = %q, want %q", c.m, c.data, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestLevelBCDRoundTrip(t *testing.T) {
|
|
for _, v := range []int{0, 1, 50, 99, 100, 128, 200, 255} {
|
|
b := LevelToBCD(v)
|
|
if len(b) != 2 {
|
|
t.Fatalf("LevelToBCD(%d) len=%d", v, len(b))
|
|
}
|
|
if got := BCDToLevel(b); got != v {
|
|
t.Errorf("level round trip %d → % X → %d", v, b, got)
|
|
}
|
|
}
|
|
// Known encodings from the Icom CI-V reference.
|
|
if got := LevelToBCD(128); !bytes.Equal(got, []byte{0x01, 0x28}) {
|
|
t.Errorf("LevelToBCD(128) = % X, want 01 28", got)
|
|
}
|
|
if got := LevelToBCD(255); !bytes.Equal(got, []byte{0x02, 0x55}) {
|
|
t.Errorf("LevelToBCD(255) = % X, want 02 55", got)
|
|
}
|
|
}
|
|
|
|
func TestByteBCDRoundTrip(t *testing.T) {
|
|
for _, v := range []int{0, 6, 12, 18, 21} {
|
|
if got := BCDToByte(ByteToBCD(v)); got != v {
|
|
t.Errorf("byte BCD round trip %d → %d", v, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestModelName(t *testing.T) {
|
|
if got := ModelName(0x98); got != "IC-7610" {
|
|
t.Errorf("ModelName(0x98) = %q, want IC-7610", got)
|
|
}
|
|
if got := ModelName(0x12); got != "Icom (0x12)" {
|
|
t.Errorf("ModelName(0x12) = %q, want fallback", got)
|
|
}
|
|
}
|
|
|
|
// CI-V addresses are hardware constants: a wrong one means the console shows the
|
|
// wrong model, and with it the wrong attenuator steps (6/12/18 dB on the big
|
|
// rigs, a single 20 dB on the small ones) — buttons the rig then NAKs.
|
|
//
|
|
// Two entries here were previously wrong in a way that pointed at each other:
|
|
// 0x80 was labelled IC-7800 (it is the IC-7410) and 0x88 IC-7700 (it is the
|
|
// IC-7100), while the real IC-7800 (0x6A) and IC-7700 (0x74) were missing — so an
|
|
// IC-7800 came up as "Icom (0x6A)" with a 20 dB attenuator it does not have.
|
|
func TestModelNameAddresses(t *testing.T) {
|
|
for addr, want := range map[byte]string{
|
|
0x6A: "IC-7800",
|
|
0x74: "IC-7700",
|
|
0x7A: "IC-7600",
|
|
0x7C: "IC-9100",
|
|
0x80: "IC-7410",
|
|
0x88: "IC-7100",
|
|
0x8E: "IC-7851",
|
|
0x94: "IC-7300",
|
|
0x98: "IC-7610",
|
|
0xA2: "IC-9700",
|
|
0xA4: "IC-705",
|
|
0xB6: "IC-7300MKII",
|
|
} {
|
|
if got := ModelName(addr); got != want {
|
|
t.Errorf("ModelName(0x%02X) = %q, want %q", addr, got, want)
|
|
}
|
|
}
|
|
// An unknown address must stay identifiable rather than masquerade as a model.
|
|
if got := ModelName(0x42); got != "Icom (0x42)" {
|
|
t.Errorf("ModelName(0x42) = %q, want the hex fallback", got)
|
|
}
|
|
}
|
|
|
|
// A frame that is not decimal BCD is refused rather than decoded.
|
|
//
|
|
// From an operator's log (F4JND, 2026-07-30): a rig sitting on 145.550 MHz FM
|
|
// produced status lines reading 16445550000, 8364550000, 5817408364 and 12640 Hz
|
|
// between good readings. CI-V had lost frame sync — a reply read at the wrong
|
|
// offset, alongside PTT timeouts and a scope stream the rig was rejecting — and
|
|
// the decoder treated the nibbles 0x0A..0x0F as the digits 10..15, turning noise
|
|
// into a plausible-looking number.
|
|
//
|
|
// Garbage that looks like a frequency is worse than a refused frame: it reaches
|
|
// the status bar, the band slots and the log, and it is what an operator then
|
|
// has to disprove.
|
|
func TestBCDToFreqRejectsNonDecimal(t *testing.T) {
|
|
// Valid: 145.550 MHz, encoded by the same package rather than by hand — my
|
|
// hand-written fixture was byte-reversed and this caught it.
|
|
good := FreqToBCD(145550000)
|
|
if hz, ok := BCDToFreq(good); !ok || hz != 145550000 {
|
|
t.Errorf("valid BCD % X decoded as %d (ok=%v), want 145550000", good, hz, ok)
|
|
}
|
|
|
|
// Every nibble value above 9 must be refused, in either half of the byte.
|
|
bad := [][]byte{
|
|
{0x0A, 0x00, 0x55, 0x45, 0x01}, // low nibble
|
|
{0x00, 0x00, 0xF5, 0x45, 0x01}, // high nibble
|
|
{0xFF, 0xFF, 0xFF, 0xFF, 0xFF}, // an idle line read as data
|
|
{0x00, 0x00, 0x00, 0x0B, 0x00},
|
|
}
|
|
for _, b := range bad {
|
|
if hz, ok := BCDToFreq(b); ok {
|
|
t.Errorf("% X was accepted as %d Hz — it is not decimal BCD", b, hz)
|
|
}
|
|
}
|
|
|
|
// A short frame still decodes what it holds: the caller decides whether a
|
|
// partial read is usable, and refusing it here would break the 4-byte forms.
|
|
if _, ok := BCDToFreq([]byte{0x00, 0x50}); !ok {
|
|
t.Error("a short but valid BCD frame must still decode")
|
|
}
|
|
}
|