package spid import ( "math" "testing" ) // The frames are pinned against Hamlib's spid.c, the reference implementation. // This protocol has one trap and it is here: digits go out as ASCII and come // back RAW. Getting that backwards points an antenna at a heading nobody asked // for, and nothing in the app would notice. func TestSetFrameMatchesTheReference(t *testing.T) { // Hamlib: u_az = PH × (360 + az), then the four decimal digits as ASCII; // PH and PV raw; K = 0x2F. got := BuildSet(0, 0, 1) // 360 → "0360" want := []byte{0x57, '0', '3', '6', '0', 0x01, '0', '3', '6', '0', 0x01, 0x2F, 0x20} assertBytes(t, "az 0 res 1", got, want) // 90° at half-degree resolution: 2 × 450 = 900 → "0900". got = BuildSet(90, 0, 2) want = []byte{0x57, '0', '9', '0', '0', 0x02, '0', '7', '2', '0', 0x02, 0x2F, 0x20} assertBytes(t, "az 90 res 2", got, want) // A quarter-degree controller, 359°: 4 × 719 = 2876. got = BuildSet(359, 0, 4) want = []byte{0x57, '2', '8', '7', '6', 0x04, '1', '4', '4', '0', 0x04, 0x2F, 0x20} assertBytes(t, "az 359 res 4", got, want) } // Status and stop carry no position: every data byte is zero, only K differs. func TestStatusAndStopFrames(t *testing.T) { assertBytes(t, "status", BuildStatus(), []byte{0x57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x1F, 0x20}) assertBytes(t, "stop", BuildStop(), []byte{0x57, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x0F, 0x20}) } // A reply's digits are RAW, and the 360 offset is what lets a pulse-counting // controller report a rotator that has turned past north — the whole reason // these rotators exist. func TestParseStatusRot2Prog(t *testing.T) { // 0x57 H1 H2 H3 H4 PH V1 V2 V3 V4 PV 0x20 // az digits 4,5,1,5 → 451.5 − 360 = 91.5 frame := []byte{0x57, 4, 5, 1, 5, 0x02, 3, 6, 0, 0, 0x02, 0x20} az, el, res, err := ParseStatus(frame, Rot2Prog) if err != nil { t.Fatalf("ParseStatus: %v", err) } if math.Abs(az-91.5) > 0.001 { t.Errorf("az = %v, want 91.5", az) } if math.Abs(el-0) > 0.001 { t.Errorf("el = %v, want 0", el) } if res != 2 { t.Errorf("resolution = %d, want 2 — the controller's own value must win", res) } } // Rot1Prog: five bytes, three digits, no elevation. func TestParseStatusRot1Prog(t *testing.T) { az, el, res, err := ParseStatus([]byte{0x57, 4, 5, 1, 0x20}, Rot1Prog) if err != nil { t.Fatalf("ParseStatus: %v", err) } if math.Abs(az-91) > 0.001 { t.Errorf("az = %v, want 91", az) } if el != 0 || res != 1 { t.Errorf("el = %v, res = %d — Rot1Prog has neither", el, res) } } // A truncated or foreign frame must be refused rather than decoded into a // heading: half a reply read as a position turns an antenna somewhere real. func TestParseStatusRefusesRubbish(t *testing.T) { for name, frame := range map[string][]byte{ "short": {0x57, 4, 5, 1}, "no start": {0x00, 4, 5, 1, 5, 1, 3, 6, 0, 0, 1, 0x20}, "no end": {0x57, 4, 5, 1, 5, 1, 3, 6, 0, 0, 1, 0x00}, "empty": {}, } { if _, _, _, err := ParseStatus(frame, Rot2Prog); err == nil { t.Errorf("%s: decoded without complaint", name) } } } func assertBytes(t *testing.T, what string, got, want []byte) { t.Helper() if len(got) != len(want) { t.Fatalf("%s: % X\nwant % X", what, got, want) } for i := range want { if got[i] != want[i] { t.Fatalf("%s: % X\nwant % X", what, got, want) } } }