package rotatorgenius import ( "bufio" "fmt" "net" "strconv" "strings" "time" . "git.rouggy.com/rouggy/ShackMaster/internal/devices" ) type Client struct { host string port int conn net.Conn } type Status struct { Rotator1 RotatorData `json:"rotator1"` Rotator2 RotatorData `json:"rotator2"` Panic bool `json:"panic"` } type RotatorData struct { CurrentAzimuth int `json:"current_azimuth"` LimitCW int `json:"limit_cw"` LimitCCW int `json:"limit_ccw"` Configuration string `json:"configuration"` // "A" for azimuth, "E" for elevation Moving int `json:"moving"` // 0=stopped, 1=CW, 2=CCW Offset int `json:"offset"` TargetAzimuth int `json:"target_azimuth"` StartAzimuth int `json:"start_azimuth"` OutsideLimit bool `json:"outside_limit"` Name string `json:"name"` Connected bool `json:"connected"` } func New(host string, port int) *Client { return &Client{ host: host, port: port, } } func (c *Client) Connect() error { conn, err := net.DialTimeout("tcp", fmt.Sprintf("%s:%d", c.host, c.port), 5*time.Second) if err != nil { return fmt.Errorf("failed to connect: %w", err) } c.conn = conn return nil } func (c *Client) Close() error { if c.conn != nil { return c.conn.Close() } return nil } func (c *Client) sendCommand(cmd string) (string, error) { if c.conn == nil { if err := c.Connect(); err != nil { return "", err } } // Get next command ID from global counter cmdID := GetGlobalCommandID().GetNextID() // Format command with ID: C| fullCmd := fmt.Sprintf("C%d%s", cmdID, cmd) // Send command _, err := c.conn.Write([]byte(fullCmd)) if err != nil { c.conn = nil return "", fmt.Errorf("failed to send command: %w", err) } // Read response reader := bufio.NewReader(c.conn) response, err := reader.ReadString('\n') if err != nil { c.conn = nil return "", fmt.Errorf("failed to read response: %w", err) } return strings.TrimSpace(response), nil } func (c *Client) GetStatus() (*Status, error) { resp, err := c.sendCommand("|h") if err != nil { return nil, err } return parseStatusResponse(resp) } func parseStatusResponse(resp string) (*Status, error) { if len(resp) < 80 { return nil, fmt.Errorf("response too short: %d bytes", len(resp)) } status := &Status{} // Parse panic flag status.Panic = resp[3] != 0x00 // Parse Rotator 1 (positions 4-38) status.Rotator1 = parseRotatorData(resp[4:38]) // Parse Rotator 2 (positions 38-72) if len(resp) >= 72 { status.Rotator2 = parseRotatorData(resp[38:72]) } return status, nil } func parseRotatorData(data string) RotatorData { rd := RotatorData{} // Current azimuth (3 bytes) if azStr := strings.TrimSpace(data[0:3]); azStr != "999" { rd.CurrentAzimuth, _ = strconv.Atoi(azStr) rd.Connected = true } else { rd.CurrentAzimuth = 999 rd.Connected = false } // Limits rd.LimitCW, _ = strconv.Atoi(strings.TrimSpace(data[3:6])) rd.LimitCCW, _ = strconv.Atoi(strings.TrimSpace(data[6:9])) // Configuration rd.Configuration = string(data[9]) // Moving state rd.Moving, _ = strconv.Atoi(string(data[10])) // Offset rd.Offset, _ = strconv.Atoi(strings.TrimSpace(data[11:15])) // Target azimuth if targetStr := strings.TrimSpace(data[15:18]); targetStr != "999" { rd.TargetAzimuth, _ = strconv.Atoi(targetStr) } else { rd.TargetAzimuth = 999 } // Start azimuth if startStr := strings.TrimSpace(data[18:21]); startStr != "999" { rd.StartAzimuth, _ = strconv.Atoi(startStr) } else { rd.StartAzimuth = 999 } // Limit flag rd.OutsideLimit = data[21] == '1' // Name rd.Name = strings.TrimSpace(data[22:34]) return rd } func (c *Client) MoveToAzimuth(rotator int, azimuth int) error { if rotator < 1 || rotator > 2 { return fmt.Errorf("rotator must be 1 or 2") } if azimuth < 0 || azimuth > 360 { return fmt.Errorf("azimuth must be between 0 and 360") } cmd := fmt.Sprintf("|A%d%03d", rotator, azimuth) resp, err := c.sendCommand(cmd) if err != nil { return err } if !strings.HasSuffix(resp, "K") { return fmt.Errorf("command failed: %s", resp) } return nil } func (c *Client) RotateCW(rotator int) error { if rotator < 1 || rotator > 2 { return fmt.Errorf("rotator must be 1 or 2") } cmd := fmt.Sprintf("|P%d", rotator) resp, err := c.sendCommand(cmd) if err != nil { return err } if !strings.HasSuffix(resp, "K") { return fmt.Errorf("command failed: %s", resp) } return nil } func (c *Client) RotateCCW(rotator int) error { if rotator < 1 || rotator > 2 { return fmt.Errorf("rotator must be 1 or 2") } cmd := fmt.Sprintf("|M%d", rotator) resp, err := c.sendCommand(cmd) if err != nil { return err } if !strings.HasSuffix(resp, "K") { return fmt.Errorf("command failed: %s", resp) } return nil } func (c *Client) Stop() error { resp, err := c.sendCommand("|S") if err != nil { return err } if !strings.HasSuffix(resp, "K") { return fmt.Errorf("command failed: %s", resp) } return nil }