package rotatorgenius import ( "bufio" "fmt" "net" "strconv" "strings" "sync" "time" ) type Client struct { host string port int conn net.Conn reader *bufio.Reader connMu sync.Mutex lastStatus *Status statusMu sync.RWMutex stopChan chan struct{} running bool } type Status struct { Heading int `json:"heading"` TargetHeading int `json:"target_heading"` Connected bool `json:"connected"` } func New(host string, port int) *Client { return &Client{ host: host, port: port, stopChan: make(chan struct{}), } } func (c *Client) Connect() error { c.connMu.Lock() defer c.connMu.Unlock() if c.conn != nil { return nil } fmt.Printf("RotatorGenius: Attempting to connect to %s:%d\n", c.host, c.port) conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.host, c.port), 5*time.Second) if err != nil { fmt.Printf("RotatorGenius: Connection failed: %v\n", err) return fmt.Errorf("failed to connect: %w", err) } c.conn = conn c.reader = bufio.NewReader(c.conn) fmt.Println("RotatorGenius: Connected successfully") return nil } func (c *Client) Close() error { c.connMu.Lock() defer c.connMu.Unlock() if c.stopChan != nil { close(c.stopChan) } if c.conn != nil { return c.conn.Close() } return nil } func (c *Client) Start() error { fmt.Println("RotatorGenius Start() called") if c.running { fmt.Println("RotatorGenius already running, skipping") return nil } fmt.Println("RotatorGenius attempting initial connection...") _ = c.Connect() c.running = true fmt.Println("RotatorGenius launching pollLoop...") go c.pollLoop() fmt.Println("RotatorGenius Start() completed") return nil } func (c *Client) pollLoop() { ticker := time.NewTicker(500 * time.Millisecond) defer ticker.Stop() for { select { case <-ticker.C: c.connMu.Lock() if c.conn == nil { c.connMu.Unlock() c.statusMu.Lock() c.lastStatus = &Status{Connected: false} c.statusMu.Unlock() if err := c.Connect(); err != nil { continue } c.connMu.Lock() } c.connMu.Unlock() status, err := c.queryStatus() if err != nil { c.connMu.Lock() if c.conn != nil { c.conn.Close() c.conn = nil c.reader = nil } c.connMu.Unlock() c.statusMu.Lock() c.lastStatus = &Status{Connected: false} c.statusMu.Unlock() continue } status.Connected = true c.statusMu.Lock() c.lastStatus = status c.statusMu.Unlock() case <-c.stopChan: return } } } func (c *Client) sendCommand(cmd string) error { c.connMu.Lock() defer c.connMu.Unlock() if c.conn == nil || c.reader == nil { return fmt.Errorf("not connected") } _, err := c.conn.Write([]byte(cmd)) if err != nil { c.conn = nil c.reader = nil return fmt.Errorf("failed to send command: %w", err) } return nil } func (c *Client) queryStatus() (*Status, error) { c.connMu.Lock() defer c.connMu.Unlock() if c.conn == nil || c.reader == nil { return nil, fmt.Errorf("not connected") } // Send |h command _, err := c.conn.Write([]byte("|h")) if err != nil { c.conn = nil c.reader = nil return nil, fmt.Errorf("failed to send query: %w", err) } // Read response - RotatorGenius doesn't send newline, read fixed amount c.conn.SetReadDeadline(time.Now().Add(500 * time.Millisecond)) defer c.conn.SetReadDeadline(time.Time{}) buf := make([]byte, 100) n, err := c.reader.Read(buf) if err != nil || n == 0 { c.conn = nil c.reader = nil return nil, fmt.Errorf("failed to read response: %w", err) } response := string(buf[:n]) return c.parseStatus(response), nil } func (c *Client) parseStatus(response string) *Status { status := &Status{} // Response format: |h2... // Example: |h2\x00183 8 10A0... // After |h2 there's a null byte, then 3 digits for heading if !strings.HasPrefix(response, "|h2") { return status } // Skip |h2 (3 chars) and null byte (1 char), then read 3 digits if len(response) >= 7 { // Position 3 is the null byte, position 4-6 are the heading headingStr := response[4:7] heading, err := strconv.Atoi(strings.TrimSpace(headingStr)) if err == nil { status.Heading = heading } targetStr := response[19:22] targetHeading, err := strconv.Atoi(strings.TrimSpace(targetStr)) if err == nil { status.TargetHeading = targetHeading } } return status } func (c *Client) GetStatus() (*Status, error) { c.statusMu.RLock() defer c.statusMu.RUnlock() if c.lastStatus == nil { return &Status{Connected: false}, nil } return c.lastStatus, nil } // SetHeading rotates to a specific azimuth func (c *Client) SetHeading(azimuth int) error { cmd := fmt.Sprintf("|A1%d", azimuth) return c.sendCommand(cmd) } // RotateCW rotates clockwise func (c *Client) RotateCW() error { return c.sendCommand("|P1") } // RotateCCW rotates counter-clockwise func (c *Client) RotateCCW() error { return c.sendCommand("|M1") } // Stop stops rotation func (c *Client) Stop() error { return c.sendCommand("|S") }