# 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 ```bash 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 ```bash # 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. ```bash # 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 1. Connects to the telescope WebSocket (port 9900) and keeps it alive with periodic ping/pong heartbeats 2. Switches to shooting mode 1 (photo/preview), then opens the camera — this activates the RTSP server 3. Starts the odometry loop: grabs frames at the configured interval, feeds them to the Tracker, logs cumulative orientation 4. 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: ```bash # 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: ```bash 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: ```bash # 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 - [x] **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 `orient` output 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 1. Grab two wide-angle frames (via RTSP `ffmpeg` grab, same as `preview grab`) 2. Downscale both to a square grid (default 256×256, power of two) 3. Compute the 2-D FFT of each, then the normalized cross-power spectrum 4. The inverse FFT gives a correlation surface; its peak = image-plane shift 5. Sub-pixel refinement via parabolic interpolation 6. 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 live` does this automatically.