Cover serve command, HTTP API endpoints, daemon client usage, and
systemd deployment. Update architecture tree with new modules.
💘 Generated with Crush
Assisted-by: Crush:/models/Qwen3.6-27B-uncensored-heretic-v2-Native-MTP-Preserved-Q4_K_M.gguf
dwarfctl — Open-source DWARF telescope control CLI
A Go command-line tool for controlling DWARF II (and upcoming DWARF III) smart telescopes over Wi-Fi via their WebSocket API.
Built from reverse-engineered protocol specs (see ../analysis/).
Quick start
make build
./dwarfctl --ip 192.168.1.100 state
Prerequisites
The telescope must be connected to your Wi-Fi (or your phone joined to the
telescope's AP hotspot). Once you can ping the telescope's IP, dwarfctl can
reach it.
Find the IP from:
- The official DWARFLAB app's Settings → My Device
- Your router's DHCP table (hostname like
DWARFxxxx) - The BLE handshake (see
../analysis/API_REFERENCE.md)
Usage examples
# Device state
dwarfctl --ip 192.168.1.100 state
# Camera
dwarfctl camera open --cam tele
dwarfctl camera photo --cam tele
dwarfctl camera photo --cam wide --raw
dwarfctl camera burst start --cam tele
dwarfctl camera exp --cam tele 156 # 1/4s exposure
dwarfctl camera gain --cam tele 60
dwarfctl camera params --cam tele
# Motor / slew
dwarfctl motor slew 90 0.5 # angle 90°, half speed
dwarfctl motor stop 0 # stop RA motor
dwarfctl motor goto 0 45.0 5.0 # RA motor to 45° at speed 5
# Astrophotography
dwarfctl astro calibrate start
dwarfctl astro goto-dso 10.6847 41.2687 "M31 Andromeda"
dwarfctl astro goto-solar 3 # Earth = index 3
dwarfctl astro stack start
dwarfctl astro eq-solve start # polar alignment
# Focus
dwarfctl focus auto
dwarfctl focus step 1 # 1=out, 0=in
dwarfctl focus astro-af start
# Tracking
dwarfctl track start 640 360 100 100 0 # bbox at center of tele cam
dwarfctl track stop
# System
dwarfctl system sync-time
dwarfctl system set-location 48.8566 2.3522 35
# Power
dwarfctl power rgb-on
dwarfctl power rgb-off
dwarfctl power reboot
dwarfctl power off
# Monitor notifications (live status events)
dwarfctl monitor
# Visual odometry — image-based orientation (no star/plate solving)
dwarfctl orient compare before.jpg after.jpg # measure rotation between 2 frames
dwarfctl orient live --cam wide # live pointing tracker from RTSP
dwarfctl orient live --fov-h 42 --fov-v 24 # override FoV with measured values
Architecture
dwarfctl/
├── proto/dwarf.proto # unified proto3 definitions (397 messages)
├── proto/dwarf.pb.go # generated Go bindings (24948 lines)
├── internal/
│ ├── transport/client.go # WebSocket client + WsPacket envelope + heartbeat
│ ├── api/
│ │ ├── commands.go # 323 command IDs + module routing
│ │ └── client.go # typed Telescope API (camera/motor/astro/...)
│ ├── daemon/ # persistent server with HTTP control API
│ │ ├── server.go # WebSocket + camera + odometry manager
│ │ └── client.go # thin HTTP client for the daemon
│ ├── rtspgrab/ # RTSP frame capture via ffmpeg
│ └── odometry/ # FFT phase-correlation visual odometry
│ ├── fft.go # radix-2 Cooley-Tukey FFT (1D + 2D)
│ ├── odometry.go # Estimate() — translation + rotation
│ ├── rotation.go # log-polar phase correlation (rotation only)
│ ├── rotate.go # image rotation for de-rotation pass
│ └── tracker.go # Tracker — cumulative orientation integrator
└── cmd/dwarfctl/main.go # cobra CLI
Daemon mode
The serve subcommand runs dwarfctl as a persistent background service that
keeps the WebSocket connection, camera, and odometry tracker alive — exposing a
local HTTP API for short-lived clients to query state or issue commands without
reconnecting each time.
# Start the daemon (connects to telescope, opens camera, starts odometry loop)
dwarfctl serve --ip 192.168.88.1 --cam wide
# With custom settings
dwarfctl serve --ip 192.168.88.1 --cam wide --addr 127.0.0.1:7777 \
--interval 5s --fov-h 8.0 --fov-v 6.5
How it works
- Connects to the telescope WebSocket (port 9900) and keeps it alive with periodic ping/pong heartbeats
- Switches to shooting mode 1 (photo/preview), then opens the camera — this activates the RTSP server
- Starts the odometry loop: grabs frames at the configured interval, feeds them to the Tracker, logs cumulative orientation
- Serves the HTTP API on the configured address until SIGTERM/SIGINT
The odometry loop auto-pauses during motor slew (to avoid motion blur) and resumes after a settle delay (default 3 s). You can also pause and resume it manually via the HTTP API.
HTTP API
All endpoints are relative to the daemon address (default http://127.0.0.1:7777).
| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Daemon status, IP, camera state |
GET |
/orientation |
Cumulative pan/tilt/rotation in degrees |
GET |
/grab?path=/tmp/x.jpg&raw=1 |
Capture RTSP frame (returns path or raw JPEG) |
POST |
/reset |
Zero the odometry tracker |
POST |
/camera?action=open&cam=wide |
Open or close a camera |
POST |
/slew |
Slew joystick ({"angle":90,"length":0.5}) |
POST |
/stop |
Stop all motors |
GET |
/pause |
Pause odometry frame capture |
GET |
/resume |
Resume odometry frame capture |
POST |
/fov |
Override FoV ({"h":8.0,"v":6.5}) |
Using the daemon client
The --daemon flag points CLI commands at a running daemon instead of connecting
directly to the telescope:
# Terminal 1: start daemon
dwarfctl serve --ip 192.168.88.1
# Terminal 2: query orientation via daemon
dwarfctl --daemon 127.0.0.1:7777 state
# Or use curl directly
curl http://127.0.0.1:7777/orientation
curl http://127.0.0.1:7777/health
curl -X POST http://127.0.0.1:7777/slew -d '{"angle":90,"length":0.3}'
systemd deployment
A systemd unit is provided in deploy/dwarfctl.service. Install it with:
sudo cp deploy/dwarfctl.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now dwarfctl
Edit the unit to adjust --ip, --cam, and --addr to match your setup.
Protocol summary
| Layer | Transport | Port |
|---|---|---|
| Control | WebSocket (binary protobuf) | 9900 |
| Preview | RTSP (not implemented here) | 554 |
Every command is a serialized WsPacket{major=2, minor=3, device_id, module_id, cmd, type, data, client_id}. The module_id is derived from cmd
by range. See ../analysis/API_REFERENCE.md for details.
Regenerating protos
If the proto definitions change:
# From the APK analysis directory:
python3 ../analysis/extract_protos.py ../extracted/jadx/.../proto ../analysis/protos
# Merge into unified proto:
cd dwarfctl/proto
python3 merge.py . dwarf.proto
protoc --go_out=. --go_opt=paths=source_relative dwarf.proto
Roadmap
- Visual odometry — image-based orientation via FFT phase correlation (
orient) - BLE discovery + handshake (DwarfPing/DwarfEcho)
- RTSP preview viewer
- Interactive REPL mode
- Schedule plan management
- OTA firmware update
- Full Notify event parsing (typed)
- Closed-loop tracking: feed
orientoutput into motor corrections
Visual odometry (orient)
The orient command estimates the telescope's pointing rotation by comparing
wide-angle frames using FFT phase correlation. It does NOT use star
identification or plate solving — it works on any textured scene (sky, horizon,
clouds, daytime landscape), making it suitable for:
- Daytime airplane tracking — the wide cam sees sky/horizon texture
- Nighttime satellite tracking — the wide cam sees star fields as texture
How it works
- Grab two wide-angle frames (via RTSP
ffmpeggrab, same aspreview grab) - Downscale both to a square grid (default 256×256, power of two)
- Compute the 2-D FFT of each, then the normalized cross-power spectrum
- The inverse FFT gives a correlation surface; its peak = image-plane shift
- Sub-pixel refinement via parabolic interpolation
- Convert pixel shift → degrees using the camera field of view
For a static alt-az mount, the accumulated shifts give the cumulative pointing orientation relative to the starting frame — no absolute encoders or polar alignment needed.
Field of view calibration
The default FoV (8.0°×6.5°) comes from the DWARF Mini's display values in
DEVICE_MODELS.md, but the firmware-reported live FoV can differ. To calibrate:
slew the motors by a known angle and compare with the measured rotation. Override
with --fov-h / --fov-v once you have measured values.
Prerequisites
- ffmpeg must be installed (for RTSP frame capture)
- The wide camera must be opened first (
camera open --cam wide) —orient livedoes this automatically.