feat(sat): point the antenna — EasyComm II az/el rotator
EasyComm is what satellite rotator controllers agreed on, so a box that works with SatPC32, Gpredict or Hamlib works here. Serial or TCP, and its own settings rather than the HF rotator's: an az/el pair is a different machine on a different port, and an operator who has both must not have to choose. A great many EasyComm controllers — the Arduino trackers above all — accept commands and never say a word back. That is legal and common, so a silent controller is not treated as a broken one: it is still driven, and the last commanded position is reported in its place, marked as commanded rather than read. A stuck rotator must not be able to hide behind an order it never carried out, which is why the panel shows the antenna's position beside the satellite's. The 450° overlap is the reason a satellite rotator is worth having, so it is used: a pass crossing north continues past 360 instead of unwinding three quarters of a turn with the antenna sweeping the ground. Below the configured elevation the mast is left alone — the numbers are right all the way round the orbit, but a rotator that chases a satellite through the far side of the earth spends the night turning, and a mast has a finite number of turns in it.
This commit is contained in:
+92
-8
@@ -32,6 +32,20 @@ const (
|
||||
keySatAutoTLE = "sat.auto_tle" // fetch elements at startup when the set is stale
|
||||
keySatGrid = "sat.grid" // locator override ("" = the station's own)
|
||||
keySatAltM = "sat.alt_m" // antenna height above sea level, metres
|
||||
|
||||
// The az/el rotator. Its own settings rather than the HF rotator's: a
|
||||
// satellite station's elevation rotator is a different machine on a
|
||||
// different port, and an operator who has both must not have to choose.
|
||||
keySatRotOn = "sat.rot_enabled"
|
||||
keySatRotTransport = "sat.rot_transport" // "serial" | "tcp"
|
||||
keySatRotHost = "sat.rot_host"
|
||||
keySatRotPort = "sat.rot_port"
|
||||
keySatRotCOM = "sat.rot_com"
|
||||
keySatRotBaud = "sat.rot_baud"
|
||||
keySatRotMaxAz = "sat.rot_max_az" // 360 or 450
|
||||
keySatRotMinEl = "sat.rot_min_el" // don't drive the rotator below this elevation
|
||||
keySatRotStep = "sat.rot_step" // degrees of change worth a command
|
||||
keySatRotPark = "sat.rot_park" // park at az 0 / el 0 when tracking stops
|
||||
)
|
||||
|
||||
// customTLEName holds elements the operator pasted in by hand.
|
||||
@@ -50,6 +64,18 @@ type SatSettings struct {
|
||||
AutoTLE bool `json:"auto_tle"`
|
||||
Grid string `json:"grid"`
|
||||
AltM int `json:"alt_m"`
|
||||
|
||||
// The az/el rotator.
|
||||
RotOn bool `json:"rot_on"`
|
||||
RotTransport string `json:"rot_transport"`
|
||||
RotHost string `json:"rot_host"`
|
||||
RotPort int `json:"rot_port"`
|
||||
RotCOM string `json:"rot_com"`
|
||||
RotBaud int `json:"rot_baud"`
|
||||
RotMaxAz int `json:"rot_max_az"`
|
||||
RotMinEl int `json:"rot_min_el"`
|
||||
RotStep int `json:"rot_step"`
|
||||
RotPark bool `json:"rot_park"`
|
||||
}
|
||||
|
||||
// SatTransponder is one path through a satellite, as the UI needs it.
|
||||
@@ -172,14 +198,47 @@ func (a *App) satParts() (*sat.Store, *sat.Birds, *sat.Fetcher) {
|
||||
// ── Settings ────────────────────────────────────────────────────────────────
|
||||
|
||||
func (a *App) satSettings() SatSettings {
|
||||
out := SatSettings{MinEl: 10, WindowH: 24, AutoTLE: true}
|
||||
// The rotator defaults are the common case, not a blank form: EasyComm over
|
||||
// a serial port at 9600, a 360° machine, and a five-degree step — which on a
|
||||
// beam with any gain at all is well inside the beamwidth and keeps a pass
|
||||
// from being a command a second.
|
||||
out := SatSettings{
|
||||
MinEl: 10, WindowH: 24, AutoTLE: true,
|
||||
RotTransport: "serial", RotPort: 4533, RotBaud: 9600,
|
||||
RotMaxAz: 360, RotMinEl: 0, RotStep: 5,
|
||||
}
|
||||
if a.settings == nil {
|
||||
return out
|
||||
}
|
||||
m, err := a.settings.GetMany(a.ctx, keySatFavorites, keySatMinEl, keySatWindowH, keySatAutoTLE, keySatGrid, keySatAltM)
|
||||
m, err := a.settings.GetMany(a.ctx,
|
||||
keySatFavorites, keySatMinEl, keySatWindowH, keySatAutoTLE, keySatGrid, keySatAltM,
|
||||
keySatRotOn, keySatRotTransport, keySatRotHost, keySatRotPort, keySatRotCOM,
|
||||
keySatRotBaud, keySatRotMaxAz, keySatRotMinEl, keySatRotStep, keySatRotPark)
|
||||
if err != nil {
|
||||
return out
|
||||
}
|
||||
out.RotOn = m[keySatRotOn] == "1"
|
||||
if tr := m[keySatRotTransport]; tr == "tcp" || tr == "serial" {
|
||||
out.RotTransport = tr
|
||||
}
|
||||
out.RotHost = strings.TrimSpace(m[keySatRotHost])
|
||||
if v, err := strconv.Atoi(m[keySatRotPort]); err == nil && v > 0 && v <= 65535 {
|
||||
out.RotPort = v
|
||||
}
|
||||
out.RotCOM = strings.TrimSpace(m[keySatRotCOM])
|
||||
if v, err := strconv.Atoi(m[keySatRotBaud]); err == nil && v >= 1200 && v <= 115200 {
|
||||
out.RotBaud = v
|
||||
}
|
||||
if v, err := strconv.Atoi(m[keySatRotMaxAz]); err == nil && v == 450 {
|
||||
out.RotMaxAz = 450
|
||||
}
|
||||
if v, err := strconv.Atoi(m[keySatRotMinEl]); err == nil && v >= -10 && v <= 30 {
|
||||
out.RotMinEl = v
|
||||
}
|
||||
if v, err := strconv.Atoi(m[keySatRotStep]); err == nil && v >= 1 && v <= 30 {
|
||||
out.RotStep = v
|
||||
}
|
||||
out.RotPark = m[keySatRotPark] == "1"
|
||||
for _, n := range strings.Split(m[keySatFavorites], ",") {
|
||||
if n = strings.TrimSpace(n); n != "" {
|
||||
out.Favorites = append(out.Favorites, n)
|
||||
@@ -230,13 +289,38 @@ func (a *App) SaveSatSettings(s SatSettings) error {
|
||||
seen[strings.ToUpper(n)] = true
|
||||
favs = append(favs, n)
|
||||
}
|
||||
if s.RotTransport != "tcp" {
|
||||
s.RotTransport = "serial"
|
||||
}
|
||||
if s.RotMaxAz != 450 {
|
||||
s.RotMaxAz = 360
|
||||
}
|
||||
if s.RotStep < 1 || s.RotStep > 30 {
|
||||
s.RotStep = 5
|
||||
}
|
||||
if s.RotPort <= 0 || s.RotPort > 65535 {
|
||||
s.RotPort = 4533
|
||||
}
|
||||
if s.RotBaud < 1200 || s.RotBaud > 115200 {
|
||||
s.RotBaud = 9600
|
||||
}
|
||||
for k, v := range map[string]string{
|
||||
keySatFavorites: strings.Join(favs, ","),
|
||||
keySatMinEl: strconv.Itoa(s.MinEl),
|
||||
keySatWindowH: strconv.Itoa(s.WindowH),
|
||||
keySatAutoTLE: boolStr(s.AutoTLE),
|
||||
keySatGrid: strings.ToUpper(strings.TrimSpace(s.Grid)),
|
||||
keySatAltM: strconv.Itoa(s.AltM),
|
||||
keySatFavorites: strings.Join(favs, ","),
|
||||
keySatMinEl: strconv.Itoa(s.MinEl),
|
||||
keySatWindowH: strconv.Itoa(s.WindowH),
|
||||
keySatAutoTLE: boolStr(s.AutoTLE),
|
||||
keySatGrid: strings.ToUpper(strings.TrimSpace(s.Grid)),
|
||||
keySatAltM: strconv.Itoa(s.AltM),
|
||||
keySatRotOn: boolStr(s.RotOn),
|
||||
keySatRotTransport: s.RotTransport,
|
||||
keySatRotHost: strings.TrimSpace(s.RotHost),
|
||||
keySatRotPort: strconv.Itoa(s.RotPort),
|
||||
keySatRotCOM: strings.TrimSpace(s.RotCOM),
|
||||
keySatRotBaud: strconv.Itoa(s.RotBaud),
|
||||
keySatRotMaxAz: strconv.Itoa(s.RotMaxAz),
|
||||
keySatRotMinEl: strconv.Itoa(s.RotMinEl),
|
||||
keySatRotStep: strconv.Itoa(s.RotStep),
|
||||
keySatRotPark: boolStr(s.RotPark),
|
||||
} {
|
||||
if err := a.settings.Set(a.ctx, k, v); err != nil {
|
||||
return err
|
||||
|
||||
Reference in New Issue
Block a user