package daemon import ( "bytes" "encoding/json" "fmt" "io" "net/http" "time" "github.com/antitbone/dwarfctl/internal/odometry" ) // Client is a thin HTTP client that talks to the daemon. type Client struct { baseURL string http *http.Client } // NewClient creates a daemon client for the given address (e.g. "127.0.0.1:7777"). func NewClient(addr string) *Client { if addr == "" { addr = DefaultAddr } return &Client{ baseURL: "http://" + addr, http: &http.Client{Timeout: 60 * time.Second}, } } // IsAlive checks if the daemon is reachable. func (c *Client) IsAlive() bool { resp, err := c.http.Get(c.baseURL + "/health") return err == nil && resp.StatusCode == 200 } // Orientation returns the current cumulative orientation from the daemon. func (c *Client) Orientation() (odometry.Orientation, error) { resp, err := c.http.Get(c.baseURL + "/orientation") if err != nil { return odometry.Orientation{}, err } defer resp.Body.Close() var o odometry.Orientation return o, json.NewDecoder(resp.Body).Decode(&o) } // Grab captures a frame. If path is empty, the daemon picks a temp path. // Returns the saved file path. func (c *Client) Grab(path string) (string, error) { url := c.baseURL + "/grab" if path != "" { url += "?path=" + path } resp, err := c.http.Get(url) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != 200 { body, _ := io.ReadAll(resp.Body) return "", fmt.Errorf("daemon: %s", body) } var result map[string]string json.NewDecoder(resp.Body).Decode(&result) return result["path"], nil } // Reset zeros the odometry. func (c *Client) Reset() error { _, err := c.http.Post(c.baseURL+"/reset", "", nil) return err } // Slew sends a joystick vector. func (c *Client) Slew(angle, length float64) error { body, _ := json.Marshal(map[string]float64{"angle": angle, "length": length}) resp, err := c.http.Post(c.baseURL+"/slew", "application/json", bytes.NewReader(body)) if err != nil { return err } resp.Body.Close() return nil } // StopMotors stops all motors. func (c *Client) StopMotors() error { _, err := c.http.Post(c.baseURL+"/stop", "", nil) return err } // Camera opens or closes a camera. func (c *Client) Camera(action, cam string) error { url := fmt.Sprintf("%s/camera?action=%s&cam=%s", c.baseURL, action, cam) resp, err := c.http.Post(url, "", nil) if err != nil { return err } resp.Body.Close() return nil } // SetFoV overrides the field of view. func (c *Client) SetFoV(h, v float64) error { body, _ := json.Marshal(map[string]float64{"h": h, "v": v}) resp, err := c.http.Post(c.baseURL+"/fov", "application/json", bytes.NewReader(body)) if err != nil { return err } resp.Body.Close() return nil }