Files
dwarf-go/dwarfctl/internal/daemon/client.go
Jacquin Antoine c2f9e54eb4 Add camera streaming guide with all access methods
Comprehensive documentation for accessing the DWARF camera streams:
- RTSP URLs and channels (ch0=tele, ch1=wide)
- The critical prerequisite: camera must be opened via WebSocket first
- ffmpeg commands for frame capture, timelapse, and video recording
- mpv and VLC usage with TCP transport
- Python/OpenCV integration example
- Troubleshooting common issues (black image, connection refused, VLC delay)
- Comparison of RTSP vs MJPEG modes across device models

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-13 19:56:04 +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: 30 * 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
}