- 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
113 lines
2.7 KiB
Go
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
|
|
}
|