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
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: 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
|
|
}
|