Files
dwarf-go/dwarfctl/internal/daemon/client.go
Jacquin Antoine d319ef6b4a Improve odometry with rotation estimation, daemon stability, and RTSP fixes
- Add rotation estimation via log-polar phase correlation (EstimateRotation,
  rotateGray) — de-rotate before translation to avoid aliasing
- Track cumulative field rotation in Tracker, reject low-confidence frames
- Extract RTSP grabber into internal/rtspgrab module
- Daemon: pause odometry during slew (motion blur), auto-resume after settle
  delay, add /pause and /resume HTTP endpoints, switch shooting mode before
  opening camera, use ReqOpenCamera with rtsp_encode_type=1
- WebSocket heartbeat (ping/pong every 10s) to prevent idle disconnects
- FFmpeg low-latency flags (-fflags nobuffer, -probesize, -analyzeduration)
- Add SendRaw API for custom payloads, increase daemon HTTP timeout to 60s

💘 Generated with Crush

Assisted-by: Crush:/models/Qwen3.6-27B-uncensored-heretic-v2-Native-MTP-Preserved-Q4_K_M.gguf
2026-07-13 23:00:30 +02:00

113 lines
2.7 KiB
Go

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
}