package main // The two ways a satellite station points its antenna. // // Some operators drive their az/el rotator directly — EasyComm II, what // SatPC32 and Gpredict speak. Others already run PstRotator, which sits between // them and a dozen different controllers and handles az AND el; for those, // OpsLog talking to the controller itself would be a second program fighting // PstRotator over the same cable. // // So both, behind one small interface, chosen in Settings. Neither is more // "correct" than the other: the right one is whichever the station already has // working. import ( "fmt" "math" "strings" "sync" "hamlog/internal/rotator/easycomm" "hamlog/internal/rotator/pst" ) // satRotator is what the tracker needs of an antenna: point it, ask where it // is, and let go of it at the end of the pass. type satRotator interface { Point(az, el float64) error // Heading reports where the antenna is. live is false when the answer is // the last commanded position rather than a reading — a stuck rotator must // not be able to hide behind an order it never carried out. Heading() (az, el float64, live bool, err error) Close() } // The rotator kinds, as stored. const ( satRotEasycomm = "easycomm" satRotPst = "pstrotator" ) // newSatRotator builds the configured controller. func newSatRotator(s SatSettings) (satRotator, error) { switch s.RotType { case satRotPst: if strings.TrimSpace(s.RotHost) == "" && s.RotPort <= 0 { return nil, fmt.Errorf("no address for PstRotator") } return &pstSatRotator{c: pst.New(s.RotHost, s.RotPstPort), maxAz: s.RotMaxAz}, nil default: if s.RotTransport == "tcp" { if strings.TrimSpace(s.RotHost) == "" { return nil, fmt.Errorf("no address for the rotator") } return easycomm.New(s.RotHost, s.RotPort, s.RotMaxAz), nil } if strings.TrimSpace(s.RotCOM) == "" { return nil, fmt.Errorf("no COM port for the rotator") } return easycomm.NewSerial(s.RotCOM, s.RotBaud, s.RotMaxAz), nil } } // pstSatRotator points the antenna through PstRotator. // // PstRotator takes whole degrees and does its own overlap handling for a 450° // rotator — it knows which controller is on the other end, and OpsLog does not. // So the azimuth is sent plainly, and the 450° logic that EasyComm needs is // deliberately NOT applied here: two programs each deciding to go the long way // round is how an antenna ends up unwinding in the middle of a pass. type pstSatRotator struct { c *pst.Client maxAz int mu sync.Mutex // lastAz/lastEl are what was commanded, for the display when PstRotator // does not answer a position query — which is the usual case for the many // setups whose controller reports nothing back to it either. lastAz, lastEl float64 commanded bool azSilent bool // the azimuth query went unanswered; stop asking elSilent bool // likewise for elevation, and far more common } func (p *pstSatRotator) Point(az, el float64) error { a := math.Mod(az, 360) if a < 0 { a += 360 } if el < 0 { el = 0 } if el > 180 { el = 180 } if err := p.c.GoTo(int(math.Round(a)), true, int(math.Round(el))); err != nil { return err } p.mu.Lock() p.lastAz, p.lastEl, p.commanded = a, el, true p.mu.Unlock() return nil } func (p *pstSatRotator) Heading() (float64, float64, bool, error) { p.mu.Lock() azSilent, elSilent, la, le, commanded := p.azSilent, p.elSilent, p.lastAz, p.lastEl, p.commanded p.mu.Unlock() az, el, live := la, le, false if !azSilent { if v, _, err := p.c.Heading(); err == nil { az, live = float64(v), true } else { // One silence is enough. Each query binds a socket and waits a second // and a half; repeating that every few seconds for a setup that will // never answer is a stall per poll for nothing. p.mu.Lock() p.azSilent = true p.mu.Unlock() } } if !elSilent { if v, _, err := p.c.Elevation(); err == nil { el = float64(v) } else { p.mu.Lock() p.elSilent = true p.mu.Unlock() } } if !live && !commanded { return 0, 0, false, fmt.Errorf("PstRotator does not report the antenna position") } return az, el, live, nil } // Close: nothing to release. Every PstRotator command is one datagram, and the // socket lives for the length of a single write. func (p *pstSatRotator) Close() {}