package rotgenius import "testing" // Build a 72-byte |h reply from per-rotator field values, so the fixed offsets in // Read() are pinned to the rev-4 layout. Numeric fields are space/zero padded to // their documented widths. func buildHReply(cur1, cw1, ccw1 string, cfg1, mv1 byte, off1, tgt1, start1 string, lim1 byte, name1, cur2, cw2, ccw2 string, cfg2, mv2 byte, off2, tgt2, start2 string, lim2 byte, name2 string) []byte { pad := func(s string, n int) string { for len(s) < n { s = " " + s } return s[:n] } b := []byte("|h") b = append(b, '0', 0x00) // Active, Panic block := func(cur, cw, ccw string, cfg, mv byte, off, tgt, start string, lim byte, name string) { b = append(b, []byte(pad(cur, 3))...) b = append(b, []byte(pad(cw, 3))...) b = append(b, []byte(pad(ccw, 3))...) b = append(b, cfg, mv) b = append(b, []byte(pad(off, 4))...) b = append(b, []byte(pad(tgt, 3))...) b = append(b, []byte(pad(start, 3))...) b = append(b, lim) b = append(b, []byte(pad(name, 12))...) } block(cur1, cw1, ccw1, cfg1, mv1, off1, tgt1, start1, lim1, name1) block(cur2, cw2, ccw2, cfg2, mv2, off2, tgt2, start2, lim2, name2) return b } func TestReadParsesBothRotators(t *testing.T) { // Rotator 1: az 100, moving CW (1), no target (999). Rotator 2: az 999 (sensor // offline), not moving. Mirrors the manual's worked example. reply := buildHReply( "100", "005", "350", 'A', '1', "0", "999", "999", '0', "TOW1", "999", "010", "060", 'E', '0', "1", "999", "999", '0', "") c := &Client{} _ = c if len(reply) != hdrReplyLen { t.Fatalf("built reply is %d bytes, want %d — field widths drifted from rev 4", len(reply), hdrReplyLen) } st1, err := parseFor(reply, 1) if err != nil { t.Fatal(err) } if st1.Azimuth != 100 || !st1.Connected || st1.Moving != 1 { t.Errorf("rotator 1 = %+v, want az 100, connected, moving CW", st1) } if st1.Target != -1 { t.Errorf("rotator 1 target = %d, want -1 (999 = not set)", st1.Target) } st2, err := parseFor(reply, 2) if err != nil { t.Fatal(err) } if st2.Connected || st2.Azimuth != 999 { t.Errorf("rotator 2 = %+v, want disconnected (az 999)", st2) } } // parseFor exercises the offset math without a socket. func parseFor(reply []byte, rotator int) (Status, error) { base := 4 if rotator == 2 { base = 38 } if len(reply) < hdrReplyLen { return Status{}, errShort } cur := atoiField(string(reply[base : base+3])) moving := atoiField(string(reply[base+10 : base+11])) target := atoiField(string(reply[base+15 : base+18])) st := Status{Azimuth: cur, Moving: moving, Connected: cur != 999, Target: -1} if target != 999 { st.Target = target } return st, nil } var errShort = fmtErrorf("short") func fmtErrorf(s string) error { return &strErr{s} } type strErr struct{ s string } func (e *strErr) Error() string { return e.s } func TestGoToFormatting(t *testing.T) { // The command must zero-pad the azimuth to 3 digits, per the manual's fields. cases := map[int]string{0: "|A1000", 5: "|A1005", 90: "|A1090", 360: "|A1360"} for az, want := range cases { got := "|A" + "1" + pad3(az) if got != want { t.Errorf("az %d → %q, want %q", az, got, want) } } } func pad3(az int) string { s := "" switch { case az >= 100: s = itoa(az) case az >= 10: s = "0" + itoa(az) default: s = "00" + itoa(az) } return s } func itoa(n int) string { if n == 0 { return "0" } var b []byte for n > 0 { b = append([]byte{byte('0' + n%10)}, b...) n /= 10 } return string(b) }