The satellite page configured its own EasyComm or PstRotator link while five other backends were configured in the rotator list. An operator with one az/el mast therefore described it twice, and could describe it differently the second time — a station that works on HF and not on a pass, for no reason visible anywhere on screen. Now every interface lives in Settings ▸ Rotator, once, and the satellite page stores only a KEY into that list plus the tracking policy that is genuinely its own (minimum elevation, step, park). The key and not the index: deleting the first rotor must not silently point the tracker at a different mast. migrateSatRotator() turns an existing satellite link into a real entry in the list, selects it, and clears the old keys so it cannot run twice. Which rotors have an elevation axis is now a question with one answer, in Go: rotatorTypes plus rotorHasElevation, exposed to the panel by GetRotatorTypes. The dropdown, the labels, each backend's default port and default baud all come from there, so TypeScript no longer keeps a second copy of the same knowledge to drift out of step. Three cases do not follow from the type alone and are treated as such: PstRotator forwards elevation to a mast that may not have any, so the operator says; a SPID's dialect decides (Rot1Prog has no elevation in its reply format); and an ARCO and an ERC-M speak the same GS-232 while only one of them lifts. Each interface carries an Az / Az+El badge beside it. The satellite rotor dropdown LISTS the azimuth-only ones, disabled, rather than hiding them: an operator who owns one rotator and does not see it concludes OpsLog cannot find it, where a greyed row saying "azimuth only" teaches the actual thing. ERC-M by DF9GR is new — the az/el interface for a Yaesu G-5500. It emulates GS-232, so internal/rotator/gs232 grew the elevation half: W for a two-axis move, C2 to read both, falling back to C+B for the firmware that answers C2 with the azimuth alone. That fallback is the point of the parser tests: reading such a reply as "elevation zero" would put the antenna on the horizon, which is the one wrong answer that looks plausible. EasyComm II is promoted to an ordinary rotator interface, so it can also turn the antenna from the compass and from a spot click. The ERC-M is UNTESTED on hardware. Its Test button reads BOTH axes rather than just the azimuth, so a controller wired for azimuth alone says so there instead of during a pass. Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
84 lines
3.2 KiB
Go
84 lines
3.2 KiB
Go
package main
|
|
|
|
import "testing"
|
|
|
|
// Which rotors the satellite page may offer. Getting this wrong is not a
|
|
// cosmetic fault: a rotor listed as az/el that has no elevation motor is a
|
|
// tracker sending W commands into a controller that ignores them, and a pass
|
|
// spent wondering why the antenna never lifts.
|
|
func TestRotorHasElevation(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
dev RotatorDevice
|
|
want bool
|
|
}{
|
|
{"ERC-M drives both axes of a G-5500", RotatorDevice{Type: "erc"}, true},
|
|
{"EasyComm is an az/el protocol", RotatorDevice{Type: "easycomm"}, true},
|
|
{"an ARCO is azimuth only", RotatorDevice{Type: "arco"}, false},
|
|
{"a Rotator Genius is azimuth only", RotatorDevice{Type: "rotgenius"}, false},
|
|
{"a DCU-1 is azimuth only", RotatorDevice{Type: "dcu1"}, false},
|
|
|
|
// SPID: the dialect decides. Rot1Prog has no elevation in its reply
|
|
// format at all, so offering it would be offering a rotor that cannot
|
|
// answer the question.
|
|
{"SPID Rot2Prog has elevation", RotatorDevice{Type: "spid", SpidModel: "rot2prog"}, true},
|
|
{"SPID defaults to Rot2Prog", RotatorDevice{Type: "spid"}, true},
|
|
{"SPID Rot1Prog does not", RotatorDevice{Type: "spid", SpidModel: "rot1prog"}, false},
|
|
|
|
// PstRotator: the elevation belongs to the station, not the protocol.
|
|
// PstRotator will happily forward EL to a controller that has no
|
|
// elevation motor, so only the operator can answer this one.
|
|
{"PstRotator with elevation declared", RotatorDevice{Type: "pst", HasElevation: true}, true},
|
|
{"PstRotator without", RotatorDevice{Type: "pst"}, false},
|
|
|
|
// An unknown type falls back to PstRotator, and must not fall back to
|
|
// "has elevation" with it.
|
|
{"an unknown backend", RotatorDevice{Type: "nonsense"}, false},
|
|
}
|
|
for _, c := range cases {
|
|
if got := rotorHasElevation(c.dev); got != c.want {
|
|
t.Errorf("%s: got %v, want %v", c.name, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The satellite page stores a rotor KEY, not a list index: deleting the first
|
|
// rotor must not silently point the tracker at a different mast.
|
|
func TestFlattenRotorsKeys(t *testing.T) {
|
|
devs := []RotatorDevice{
|
|
{ID: "a", Name: "HF", Type: "pst"},
|
|
{ID: "b", Name: "Sat", Type: "erc"},
|
|
{ID: "c", Name: "RG", Type: "rotgenius", Dual: true, Name2: "RG 2"},
|
|
}
|
|
got := flattenRotors(devs)
|
|
want := []struct {
|
|
key string
|
|
hasEl bool
|
|
}{
|
|
{"a", false},
|
|
{"b", true},
|
|
{"c", false},
|
|
{"c#2", false}, // the second port of a Dual Rotator Genius
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("got %d logical rotors, want %d", len(got), len(want))
|
|
}
|
|
for i, w := range want {
|
|
if got[i].Key != w.key || got[i].HasEl != w.hasEl {
|
|
t.Errorf("rotor %d: key %q el %v, want key %q el %v", i, got[i].Key, got[i].HasEl, w.key, w.hasEl)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Each backend's default baud, because a blanket 9600 is silence on two of
|
|
// them: a SPID runs at 600 and an ERC-M ships at 19200, and a wrong rate reads
|
|
// exactly like a dead controller.
|
|
func TestRotorDefaultBaud(t *testing.T) {
|
|
cases := map[string]int{"spid": 600, "erc": 19200, "dcu1": 4800, "arco": 9600, "easycomm": 9600}
|
|
for typ, want := range cases {
|
|
if got := rotorTypeInfo(typ).DefaultBaud; got != want {
|
|
t.Errorf("%s: default baud %d, want %d", typ, got, want)
|
|
}
|
|
}
|
|
}
|