240 lines
8.2 KiB
Go
240 lines
8.2 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")
|
|
}
|
|
}
|
|
|
|
// A band stacking register reply, byte for byte as the rigs send it:
|
|
// 1A 01 <band> <reg> <freq 5 LE-BCD> <mode> <filter> <data>. The payload here
|
|
// is everything after the command byte, which is what Decoded.Data holds.
|
|
func TestDecodeBandStack(t *testing.T) {
|
|
// 14.074.000 MHz, USB, FIL1, data mode on — the FT8 stack on 20 m.
|
|
frame := []byte{0x01, 0x05, 0x03, 0x00, 0x40, 0x07, 0x14, 0x00, ModeUSB, 0x01, 0x01}
|
|
bs, ok := DecodeBandStack(frame, 0x05, 0x03)
|
|
if !ok {
|
|
t.Fatal("a well-formed register was rejected")
|
|
}
|
|
if bs.FreqHz != 14_074_000 {
|
|
t.Errorf("freq = %d, want 14074000", bs.FreqHz)
|
|
}
|
|
if bs.Mode != ModeUSB || !bs.Data {
|
|
t.Errorf("mode = 0x%02X data = %v, want USB + data", bs.Mode, bs.Data)
|
|
}
|
|
// The register ASKED FOR is part of the answer: a reply about another one is
|
|
// a desynchronised read, not a frequency to send the radio to.
|
|
if _, ok := DecodeBandStack(frame, 0x05, 0x01); ok {
|
|
t.Error("a reply for register 3 was accepted as register 1")
|
|
}
|
|
if _, ok := DecodeBandStack(frame, 0x03, 0x03); ok {
|
|
t.Error("a reply about 40 m was accepted as 20 m")
|
|
}
|
|
// A register without the data-mode byte is a shorter frame, not a bad one.
|
|
if bs, ok := DecodeBandStack(frame[:10], 0x05, 0x03); !ok || bs.FreqHz != 14_074_000 || bs.Data {
|
|
t.Errorf("short register = %+v ok=%v, want the frequency with no data flag", bs, ok)
|
|
}
|
|
if _, ok := DecodeBandStack([]byte{0x01, 0x05, 0x03}, 0x05, 0x03); ok {
|
|
t.Error("a truncated frame was accepted")
|
|
}
|
|
}
|