128 lines
3.4 KiB
Go
128 lines
3.4 KiB
Go
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)
|
|
}
|