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
This commit is contained in:
@ -8,13 +8,10 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"image"
|
||||
_ "image/jpeg"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
@ -22,6 +19,7 @@ import (
|
||||
|
||||
"github.com/antitbone/dwarfctl/internal/api"
|
||||
"github.com/antitbone/dwarfctl/internal/odometry"
|
||||
"github.com/antitbone/dwarfctl/internal/rtspgrab"
|
||||
)
|
||||
|
||||
// DefaultAddr is the default HTTP listen address for the daemon.
|
||||
@ -29,14 +27,15 @@ const DefaultAddr = "127.0.0.1:7777"
|
||||
|
||||
// Config configures the daemon.
|
||||
type Config struct {
|
||||
IP string
|
||||
Addr string
|
||||
FoVH float64
|
||||
FoVV float64
|
||||
IP string
|
||||
Addr string
|
||||
FoVH float64
|
||||
FoVV float64
|
||||
GridSize int
|
||||
Camera string // "wide" or "tele"
|
||||
Camera string // "wide" or "tele"
|
||||
Interval time.Duration // odometry frame interval
|
||||
Debug bool
|
||||
SettleDelay time.Duration // pause after motor stop before resuming
|
||||
Debug bool
|
||||
}
|
||||
|
||||
// Server is the persistent telescope daemon.
|
||||
@ -45,11 +44,15 @@ type Server struct {
|
||||
scope *api.Telescope
|
||||
tracker *odometry.Tracker
|
||||
|
||||
mu sync.Mutex
|
||||
camera api.Camera
|
||||
camOpen bool
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
mu sync.Mutex
|
||||
camera api.Camera
|
||||
camOpen bool
|
||||
stopCh chan struct{}
|
||||
wg sync.WaitGroup
|
||||
|
||||
// paused disables odometry frame capture (e.g. during motor slew to avoid
|
||||
// motion blur which destroys phase correlation).
|
||||
paused bool
|
||||
}
|
||||
|
||||
// New creates a daemon server.
|
||||
@ -72,6 +75,9 @@ func New(cfg Config) *Server {
|
||||
if cfg.Interval <= 0 {
|
||||
cfg.Interval = 5 * time.Second
|
||||
}
|
||||
if cfg.SettleDelay <= 0 {
|
||||
cfg.SettleDelay = 3 * time.Second
|
||||
}
|
||||
return &Server{
|
||||
cfg: cfg,
|
||||
stopCh: make(chan struct{}),
|
||||
@ -97,11 +103,14 @@ func (s *Server) Run() error {
|
||||
s.camera = api.CameraWide
|
||||
}
|
||||
log.Printf("Opening %s camera...", s.cfg.Camera)
|
||||
// Switch to shooting mode 1 (photo/preview) first — required for streaming.
|
||||
s.scope.SwitchShootingMode(1)
|
||||
if err := s.scope.OpenCamera(s.camera); err != nil {
|
||||
return fmt.Errorf("open camera: %w", err)
|
||||
}
|
||||
s.camOpen = true
|
||||
log.Println("Camera opened.")
|
||||
log.Println("Camera opened. Waiting 5s for RTSP to initialize...")
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
// 3. Start odometry loop in background.
|
||||
s.wg.Add(1)
|
||||
@ -157,7 +166,7 @@ func (s *Server) odometryLoop() {
|
||||
defer ticker.Stop()
|
||||
|
||||
// Prime with first frame.
|
||||
if img, err := grabFrame(url, tmpFile); err == nil {
|
||||
if img, err := rtspgrab.GrabFrame(url, tmpFile); err == nil {
|
||||
s.tracker.Update(img)
|
||||
log.Printf("Odometry primed (grid=%d, fov=%.1fx%.1f)", s.cfg.GridSize, s.cfg.FoVH, s.cfg.FoVV)
|
||||
} else {
|
||||
@ -169,7 +178,16 @@ func (s *Server) odometryLoop() {
|
||||
case <-s.stopCh:
|
||||
return
|
||||
case <-ticker.C:
|
||||
img, err := grabFrame(url, tmpFile)
|
||||
// Skip frame capture while motors are moving (motion blur destroys
|
||||
// phase correlation). The slew handler sets paused=true/false.
|
||||
s.mu.Lock()
|
||||
if s.paused {
|
||||
s.mu.Unlock()
|
||||
continue
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
img, err := rtspgrab.GrabFrame(url, tmpFile)
|
||||
if err != nil {
|
||||
if s.cfg.Debug {
|
||||
log.Printf("grab error: %v", err)
|
||||
@ -197,22 +215,6 @@ func (s *Server) rtspChannel() string {
|
||||
return "ch0"
|
||||
}
|
||||
|
||||
// grabFrame uses ffmpeg to grab a single RTSP frame, then decodes it.
|
||||
func grabFrame(url, tmpFile string) (image.Image, error) {
|
||||
cmd := exec.Command("ffmpeg", "-rtsp_transport", "tcp",
|
||||
"-i", url, "-frames:v", "1", "-q:v", "2", "-y", tmpFile)
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, fmt.Errorf("ffmpeg: %w", err)
|
||||
}
|
||||
f, err := os.Open(tmpFile)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
img, _, err := image.Decode(f)
|
||||
return img, err
|
||||
}
|
||||
|
||||
// --- HTTP API ---
|
||||
|
||||
func (s *Server) registerRoutes(mux *http.ServeMux) {
|
||||
@ -223,6 +225,8 @@ func (s *Server) registerRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/camera", s.handleCamera)
|
||||
mux.HandleFunc("/slew", s.handleSlew)
|
||||
mux.HandleFunc("/stop", s.handleStop)
|
||||
mux.HandleFunc("/pause", s.handlePause)
|
||||
mux.HandleFunc("/resume", s.handleResume)
|
||||
mux.HandleFunc("/fov", s.handleFoV)
|
||||
}
|
||||
|
||||
@ -251,13 +255,10 @@ func (s *Server) handleGrab(w http.ResponseWriter, r *http.Request) {
|
||||
if q := r.URL.Query().Get("path"); q != "" {
|
||||
out = q
|
||||
}
|
||||
cmd := exec.Command("ffmpeg", "-rtsp_transport", "tcp",
|
||||
"-i", url, "-frames:v", "1", "-q:v", "2", "-y", out)
|
||||
if err := cmd.Run(); err != nil {
|
||||
if _, err := rtspgrab.GrabFrame(url, out); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
// Return the image bytes or just the path.
|
||||
if r.URL.Query().Get("raw") == "1" {
|
||||
data, _ := os.ReadFile(out)
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
@ -318,16 +319,47 @@ func (s *Server) handleSlew(w http.ResponseWriter, r *http.Request) {
|
||||
body, _ := io.ReadAll(r.Body)
|
||||
json.Unmarshal(body, &req)
|
||||
}
|
||||
// Pause odometry during slew to avoid motion blur.
|
||||
s.mu.Lock()
|
||||
s.paused = true
|
||||
s.mu.Unlock()
|
||||
|
||||
if err := s.scope.SlewJoystick(req.Angle, req.Length); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
writeJSON(w, map[string]any{"angle": req.Angle, "length": req.Length})
|
||||
writeJSON(w, map[string]any{"angle": req.Angle, "length": req.Length, "odometry": "paused"})
|
||||
}
|
||||
|
||||
func (s *Server) handleStop(w http.ResponseWriter, _ *http.Request) {
|
||||
s.scope.MotorJoystickStop()
|
||||
writeJSON(w, map[string]string{"status": "motors stopped"})
|
||||
// Keep odometry paused — a settle delay prevents motion-blurred frames
|
||||
// from corrupting the tracker. The odometry loop auto-resumes after the
|
||||
// delay via a timer.
|
||||
s.mu.Lock()
|
||||
s.paused = true
|
||||
s.mu.Unlock()
|
||||
go func() {
|
||||
time.Sleep(s.cfg.SettleDelay)
|
||||
s.mu.Lock()
|
||||
s.paused = false
|
||||
s.mu.Unlock()
|
||||
}()
|
||||
writeJSON(w, map[string]string{"status": "motors stopped, settling..."})
|
||||
}
|
||||
|
||||
func (s *Server) handlePause(w http.ResponseWriter, _ *http.Request) {
|
||||
s.mu.Lock()
|
||||
s.paused = true
|
||||
s.mu.Unlock()
|
||||
writeJSON(w, map[string]string{"status": "odometry paused"})
|
||||
}
|
||||
|
||||
func (s *Server) handleResume(w http.ResponseWriter, _ *http.Request) {
|
||||
s.mu.Lock()
|
||||
s.paused = false
|
||||
s.mu.Unlock()
|
||||
writeJSON(w, map[string]string{"status": "odometry resumed"})
|
||||
}
|
||||
|
||||
func (s *Server) handleFoV(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user