# 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.
