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
354 lines
16 KiB
Markdown
354 lines
16 KiB
Markdown
# DWARFLAB Telescope — API Reference (reverse-engineered)
|
||
|
||
Reverse-engineered from `DWARFLAB.apk` v3.4.0 (build 629), package `com.convergence.dwarflab`.
|
||
This document describes the protocol used by the official Android app to talk to
|
||
DWARF II (and the in-development "Bilbo"/DWARF III) smart telescopes.
|
||
|
||
> Goal: enable an open-source client implementation. All command IDs, the wire
|
||
> envelope, and the inner protobuf payloads were recovered from the APK; nothing
|
||
> here is guessed.
|
||
|
||
---
|
||
|
||
## 1. Architecture at a glance
|
||
|
||
```
|
||
┌─────────────┐ BLE (GATT) ┌───────────┐
|
||
│ Phone app │ ───────────────▶ │ Telescope │ 1. discovery + Wi-Fi creds exchange
|
||
│ │ ◀── DwarfEcho ── │ │ (DwarfPing / DwarfEcho, see ble.proto)
|
||
└──────┬──────┘ └─────┬─────┘
|
||
│ joins telescope Wi-Fi │
|
||
│ (AP mode, or shared STA) │
|
||
│ │
|
||
│ WebSocket (binary) │ RTSP (TCP)
|
||
│ ws://<ip>:9900/?client_id=.. │ rtsp://<ip>/<cam>/stream0
|
||
▼ ▼
|
||
┌───────────────────────────┐ ┌──────────────┐
|
||
│ Control plane │ │ Live preview │
|
||
│ WsPacket (protobuf) │ │ ijkplayer / │
|
||
│ 323 commands, 17 modules │ │ ExoPlayer │
|
||
└───────────────────────────┘ └──────────────┘
|
||
```
|
||
|
||
Two distinct data planes:
|
||
|
||
| Plane | Transport | Port | Encoding | Purpose |
|
||
|-------|-----------|------|----------|---------|
|
||
| **Control** | WebSocket (binary frames) | **9900** | protobuf `WsPacket` envelope | All telescope commands & status |
|
||
| **Preview** | RTSP over TCP | 554 (std) | H.264/H.265 | Live camera viewfinder |
|
||
| **Discovery** | BLE GATT | — | protobuf (`ble.proto`) | Find telescope, get IP/SSID/PSK |
|
||
|
||
There is also a **cloud relay**: `https://app.dwarflabapp.com/app/uls/connect?d=…`
|
||
("ULS") used for remote access when the phone is not on the telescope's Wi-Fi.
|
||
Local LAN control (the focus of an OSS client) needs only BLE + WebSocket + RTSP.
|
||
|
||
---
|
||
|
||
## 2. Connection flow
|
||
|
||
### 2.1 Discovery & credentials (BLE)
|
||
The telescope advertises over BLE. The app sends a `DwarfPing` / `ReqGetconfig`
|
||
and receives a **`DwarfEcho`** (`ble.proto`) containing everything needed to
|
||
connect over IP:
|
||
|
||
```proto
|
||
message DwarfEcho {
|
||
VocalType vocaltype = 1;
|
||
uint64 timestamp = 2;
|
||
bytes magic = 3;
|
||
uint64 ts_ping = 4;
|
||
bytes mac_address = 5;
|
||
StationModel model = 6; // telescope family + revision
|
||
string sn = 7; // serial number
|
||
string name = 8; // advertised name
|
||
string psw = 9; // device password
|
||
string fw_version = 10;
|
||
string ws_scheme = 11; // "ws" or "wss"
|
||
uint32 session = 12;
|
||
NifAP ap = 13; // AP network (ifname/ssid/psw/ipv4/ipv6)
|
||
NifSTA sta = 14; // STA network (ifname/ssid/psw/rssi/ipv4/ipv6)
|
||
}
|
||
```
|
||
|
||
`ble_psd` (BLE password) + `client_id` authenticate the BLE requests
|
||
(`ReqGetconfig`, `ReqSta`, `ReqAp`, `ReqSetblewifi`). Responses carry the
|
||
**`ip`** field (`ResGetconfig.ip` / `ResSta.ip`) that the app feeds into the
|
||
WebSocket URL.
|
||
|
||
### 2.2 WebSocket control channel
|
||
Once the phone is on the telescope's network:
|
||
|
||
```
|
||
ws://<telescope_ip>:9900/?client_id=<client_id>
|
||
```
|
||
|
||
- Plain `ws://` by default; `wss://` if `ws_scheme == "wss"`.
|
||
- `client_id` is a client-generated identifier (`y32…m58158c()`).
|
||
- Source: `v55.java` field `f43155b` / method `m55420t(ip)`.
|
||
- **device_id** field in the envelope selects which telescope (multi-device),
|
||
defaults to `1` for the first/only connected scope.
|
||
|
||
### 2.3 RTSP preview
|
||
```
|
||
rtsp://<telescope_ip>:554/<channel>/stream0
|
||
```
|
||
- `<channel>`: `ch0` = Tele camera, `ch1` = Wide camera
|
||
- **IMPORTANT**: The camera MUST be opened via WebSocket (`CMD_CAMERA_TELE_OPEN_CAMERA` 10000 or `CMD_CAMERA_WIDE_OPEN_CAMERA` 12000) before the RTSP stream becomes available. Without opening the camera, the RTSP server accepts connections but never sends frames.
|
||
- Player options force `rtsp_transport = tcp` (see `RtspPlayerView.java:195`).
|
||
- Codec: MJPEG over RTSP (not H.264).
|
||
- Port 8092 (MJPEG HTTP `/mainstream` and `/secondstream`) exists but is **inactive** on the DWARF Mini — the Mini uses RTSP exclusively.
|
||
|
||
### 2.4 Visual odometry (image-based orientation)
|
||
|
||
The wide-angle RTSP frames can be used to estimate the telescope's pointing
|
||
rotation **without star identification or plate solving**, by comparing
|
||
consecutive frames with FFT phase correlation. This is implemented in
|
||
`dwarfctl` as the `orient` command (`internal/odometry/`).
|
||
|
||
**Principle:** for small rotations of a static mount, the scene undergoes an
|
||
almost pure 2-D translation in the image plane. Phase correlation recovers that
|
||
translation with sub-pixel accuracy, which maps directly to pan/tilt degrees via
|
||
the camera field of view. Accumulating frame-to-frame shifts gives cumulative
|
||
pointing orientation.
|
||
|
||
**Use cases on the DWARF Mini:**
|
||
- **Daytime airplane tracking** — the wide cam provides sky/horizon texture
|
||
- **Nighttime satellite tracking** — star fields serve as correlation texture
|
||
|
||
**Pipeline:**
|
||
1. Open the wide camera (`CMD_CAMERA_WIDE_OPEN_CAMERA` 12000) — or let `orient
|
||
live` do it automatically
|
||
2. Grab frames via `ffmpeg -rtsp_transport tcp -i rtsp://<ip>/ch1/stream0`
|
||
3. Compare consecutive frames (`Estimate`) or integrate continuously (`Tracker`)
|
||
|
||
**FoV caveat:** the DWARF Mini's display FoV is 8.0°×6.5° (see
|
||
`DEVICE_MODELS.md`), but the firmware-reported live FoV can differ. The pixel
|
||
shift is always correct; only the degree conversion depends on the FoV
|
||
parameter. Calibrate by slewing a known motor angle and comparing.
|
||
|
||
---
|
||
|
||
## 3. Wire format — `WsPacket` envelope
|
||
|
||
Every WebSocket binary frame is **one serialized `WsPacket`** (`base.proto`):
|
||
|
||
```proto
|
||
message WsPacket {
|
||
uint32 major_version = 1; // = 2 (WS_MAJOR_VERSION_NUMBER)
|
||
uint32 minor_version = 2; // = 3 (WS_MINOR_VERSION_NUMBER) → protocol v2.3
|
||
uint32 device_id = 3; // telescope index, default 1
|
||
uint32 module_id = 4; // derived from cmd (see §4)
|
||
uint32 cmd = 5; // operation id (10000–17099)
|
||
uint32 type = 6; // 0=request 1=response 2=notification 3=reply
|
||
bytes data = 7; // inner protobuf message, serialized
|
||
string client_id = 8; // same client_id as the WS URL
|
||
}
|
||
```
|
||
|
||
Confirmed build code (`e49.java`, `C9312a.m36361a`):
|
||
```java
|
||
WsPacket.newBuilder()
|
||
.setMajorVersion(2)
|
||
.setMinorVersion(3)
|
||
.setDeviceId(connectedDeviceId != null ? connectedDeviceId : 1)
|
||
.setModuleId(wsCmd.getModuleId().ordinal()) // derived from cmd range
|
||
.setCmd(wsCmd.getCmd())
|
||
.setType(wsCmd.getMessageType().ordinal()) // request=0
|
||
.setData(ByteString.copyFrom(innerProto.toByteArray()))
|
||
.setClientId(clientId)
|
||
.build();
|
||
```
|
||
|
||
`type` enum (`WsMessageType`): `request=0`, `response=1`, `notification=2`, `reply=3`.
|
||
|
||
### 3.1 Request/response matching
|
||
Responses are matched by **`cmd`**. The app registers a pending continuation
|
||
keyed by the expected response `cmd`; an incoming `WsPacket` whose `cmd` matches
|
||
is parsed (`WsRequestHandle.mo7785d` → `BaseProto.WsPacket.parseFrom`, then the
|
||
inner `data` is parsed into the expected proto class).
|
||
|
||
For most commands the **response uses the same `cmd`** as the request
|
||
(`WsMessageReq.getResponseCmd()` defaults to `getCmd()`, see `d49.m35708a`).
|
||
|
||
**IMPORTANT — discovered via live testing (not in the APK code):**
|
||
The telescope uses **`type=3` (REPLY)** for direct responses, not `type=1`
|
||
(RESPONSE). Additionally, not all commands get a reply:
|
||
|
||
| Response model | Commands (tested) | Implementation |
|
||
|---------------|-------------------|----------------|
|
||
| **type=3 reply (same cmd)** | photo (10002), photo wide (12022), focus auto (15000), focus step (15001), state (16405), sync-time (13000), set-location (13010) | request-response with timeout |
|
||
| **notification only (type=2)** | RGB on/off (13500/13501), camera open/close (10000/10001), camera params GET (10036) | fire-and-forget; verify via `state` |
|
||
| **fire-and-forget native** | motor slew (14006), motor stop (14002) | no ack expected |
|
||
|
||
Commands that only produce notifications still execute successfully — the
|
||
state change is visible in `GetDeviceState` (e.g. `rgb_state:{}` after RGB off).
|
||
|
||
### 3.2 Notifications
|
||
The telescope pushes `WsPacket` frames with `type=2` (notification) and `cmd` in
|
||
the **15200–15303** range (`CMD_NOTIFY_*`). An OSS client must listen for these
|
||
to reflect state changes (track results, burst/record progress, temperatures,
|
||
calibration state, SD-card info, power, etc.). See `Notify.proto` (81 messages)
|
||
and `CMD_TABLE.md` (NOTIFY module).
|
||
|
||
---
|
||
|
||
## 4. Command routing
|
||
|
||
`module_id` is **not** stored per-command — it is derived from `cmd` by range
|
||
checks in `WsCmd.getModuleId()`:
|
||
|
||
| cmd range | module_id (ordinal) | module | proto file |
|
||
|-----------|---------------------|--------|------------|
|
||
| 10000–10499 | 1 | CAMERA_TELE | `Camera.proto` |
|
||
| 11000–11499 | 3 | ASTRO | `Astro.proto` |
|
||
| 12000–12499 | 2 | CAMERA_WIDE | `Camera.proto` |
|
||
| 13000–13299 | 4 | SYSTEM | `System.proto` |
|
||
| 13500–13799 | 5 | RGB_POWER | (RGB led / power) |
|
||
| 14000–14499 | 6 | MOTOR | `MotorControl.proto` |
|
||
| 14800–14899 | 7 | TRACK | `Track.proto` |
|
||
| 15000–15199 | 8 | FOCUS | `Focus.proto` |
|
||
| 15200–15499 | 9 | NOTIFY | `Notify.proto` |
|
||
| 15500–15599 | 10 | PANORAMA | `Panorama.proto` |
|
||
| 15700–15799 | 11 | ITIPS | `ITips.proto` |
|
||
| 16100–16399 | 13 | SHOOTING_SCHEDULE | `Schedule.proto` |
|
||
| 16400–16599 | 14 | TASK_CENTER | `TaskCenter.proto` |
|
||
| 16700–16799 | 15 | PARAM | `Param.proto` |
|
||
| 16800–16899 | 16 | VOICE_ASSISTANT | `VoiceAssistant.proto` |
|
||
| 17000–17099 | 18 | DEVICE | `Device.proto` |
|
||
|
||
**Full table of all 323 commands → see [`CMD_TABLE.md`](CMD_TABLE.md).**
|
||
|
||
The mapping from command → inner protobuf message type lives in the
|
||
`data/websocket/<module>/` handlers (`*WsResponseHandle.java`) and the
|
||
`data/bean/p021ws/request/` request classes. To find the payload type for a
|
||
given cmd, grep the request package.
|
||
|
||
---
|
||
|
||
## 5. Proto definitions
|
||
|
||
Regenerated cleanly from the embedded `FileDescriptorProto` descriptors in the
|
||
APK — **17 files, 382 messages**. Located in `analysis/protos/`:
|
||
|
||
| File | msgs | Covers |
|
||
|------|------|--------|
|
||
| `Base.proto` | 6 | WsPacket envelope, ComResponse, CommonParam |
|
||
| `Camera.proto` | 55 | Both cameras: exp/gain/WB/ISP/RAW/record/burst/resolution |
|
||
| `Astro.proto` | 67 | Calibration, GOTO (DSO/solar), live-stacking, darks, EQ solving, AI enhance, mosaic, sky-finder |
|
||
| `MotorControl.proto` | 14 | RA/DEC motors: run/stop/runTo/joystick/reset/positions |
|
||
| `Track.proto` | 9 | Tracking, sentry mode, MOT, UFO (multi-object track) |
|
||
| `Focus.proto` | 9 | Auto-focus (normal + astro), manual continuous, user infinity |
|
||
| `Panorama.proto` | 18 | Grid/stitch/framing/upload/compress |
|
||
| `Notify.proto` | 81 | All async server-push events |
|
||
| `Schedule.proto` | 20 | Shooting plan sync/cancel/lock |
|
||
| `TaskCenter.proto` | 26 | Global task manager, state info, mode/tech switch |
|
||
| `System.proto` | 14 | Time/timezone, location, MTP, CPU mode, activation, low-temp protection |
|
||
| `Param.proto` | 8 | Generic param set (exposure/gain/WB/int/float/bool/auto) |
|
||
| `Device.proto` | 3 | Lens defog, auto-cooling, auto-shutdown |
|
||
| `Ble.proto` | 25 | BLE handshake (DwarfPing/DwarfEcho/config/AP/STA/wifi-scan) |
|
||
| `VoiceAssistant.proto` | 16 | On-device voice assistant tasking |
|
||
| `RGB.proto` | 6 | RGB LED ring / power indicator |
|
||
| `ITips.proto` | 5 | Tips content |
|
||
|
||
To regenerate: `python3 analysis/extract_protos.py <jadx proto dir> <out dir>`
|
||
|
||
---
|
||
|
||
## 6. Worked example — take a photo with the Tele camera
|
||
|
||
1. **(once)** BLE: connect, get `DwarfEcho` → extract `sta.ipv4`/`ap.ipv4`,
|
||
`psw`, join Wi-Fi.
|
||
2. Open WebSocket `ws://<ip>:9900/?client_id=droid-oss-001`.
|
||
3. Open the Tele camera: send `WsPacket{cmd=10000, data=ReqOpenCamera}`,
|
||
wait for `ComResponse{code=0}`.
|
||
4. Set exposure (optional): `cmd=10009` with `ReqSetExp{…}`.
|
||
5. Photograph: `cmd=10002` (`CMD_CAMERA_TELE_PHOTOGRAPH`).
|
||
6. Watch notifications `15273` (PHOTO_STATE) / `15274` (BURST_STATE) for result.
|
||
|
||
Common response type is `ComResponse{ int32 code = 1; }` — `code==0` means OK.
|
||
|
||
---
|
||
|
||
## 7. Parameter value maps (assets)
|
||
|
||
The APK ships ready-made enum maps that constrain valid values:
|
||
|
||
- **`assets/params_range.json`** — exposure index ↔ `"1/N"` label (0…full
|
||
mapping, e.g. index 0 = "1/10000", step 3).
|
||
- **`assets/shoot_plan_config.json`** — per-device capability matrix. Defines
|
||
DWARF II (id=1, fw 2.1.6): two cameras (`Tele` id=0, `Wide` implicit),
|
||
their FoV (`fvWidth/fvHeight`), preview size (1280×720), and every supported
|
||
param with `min/max/step/defaultValue/valueType` + Gear vs Continue modes.
|
||
This file is the authoritative source for **what values each camera accepts**.
|
||
- **`assets/astronomy_data.db`** — SQLite of celestial objects (GOTO targets).
|
||
- **`assets/www/modules/eq/`** — Three.js 3D equatorial-alignment helper
|
||
(polar-alignment UI). "Bilbo" = next-gen model codename (DWARF III).
|
||
|
||
---
|
||
|
||
## 8. Multi-device & activation notes
|
||
|
||
- `device_id` in `WsPacket` selects the active telescope when several are on the
|
||
same network; default `1`.
|
||
- Some telescopes are **factory-activated** via `CMD_SYSTEM_*` (13005–13008):
|
||
`ReqGetDeviceActivateInfo`, `ReqDeviceActivateWriteFile`, activation-notify,
|
||
factory-test un-activate. An OSS client should generally leave activation
|
||
alone.
|
||
- `CMD_SYSTEM_SET_MASTER` (13004) toggles master/slave mode
|
||
(`ReqsetMasterLock{bool lock}`).
|
||
- MTP mode (`CMD_SYSTEM_SET_MTP_MODE`, 13002) switches the telescope between
|
||
Mass-Storage (mount SD card over USB) and normal modes.
|
||
|
||
---
|
||
|
||
## 9. Suggested open-source client architecture
|
||
|
||
```
|
||
dwarf-oss/
|
||
├── proto/ ← copy analysis/protos/*.proto here
|
||
├── ble/ ← BLE scanner + DwarfPing/DwarfEcho handshake (ble.proto)
|
||
├── transport/
|
||
│ ├── ws_client.py ← ws://<ip>:9900/?client_id=…, send/recv WsPacket
|
||
│ ├── envelope.py ← WsPacket build/parse (Base.proto)
|
||
│ └── dispatcher.py ← cmd → pending-request matching, NOTIFY fan-out
|
||
├── rtsp/ ← GStreamer/ffmpeg/PyAV pull of rtsp://<ip>/<cam>/stream0
|
||
├── modules/
|
||
│ ├── camera.py ← cmds 10000/12000 (Tele/Wide)
|
||
│ ├── motor.py ← cmds 14000 (point/slew)
|
||
│ ├── astro.py ← cmds 11000 (calib/goto/stacking/darks)
|
||
│ ├── focus.py ← cmds 15000
|
||
│ ├── track.py ← cmds 14800
|
||
│ └── task.py ← cmds 16400 (one-click shooting)
|
||
└── cli.py ← `dwarf photo`, `dwarf goto M31`, `dwarf slew 1.2 0`
|
||
```
|
||
|
||
Key implementation tips:
|
||
- Send `major_version=2, minor_version=3` always.
|
||
- Reuse one persistent WebSocket; don't reconnect per command.
|
||
- Maintain a registry of `cmd → asyncio.Future` for request/response; a single
|
||
inbound dispatcher handles both responses and notifications.
|
||
- Start with `CMD_GLOBAL_TASK_GET_DEVICE_STATE_INFO` (16405) after connect to
|
||
snapshot the current state, then rely on NOTIFY pushes.
|
||
- For live view, a separate RTSP consumer is simpler than multiplexing over the
|
||
WebSocket.
|
||
|
||
---
|
||
|
||
## 10. Tooling used / how to reproduce
|
||
|
||
```
|
||
tools/jadx/bin/jadx -d extracted/jadx DWARFLAB.apk # decompile
|
||
python3 analysis/extract_protos.py extracted/.../proto analysis/protos
|
||
# WsCmd / WsModuleId / WsMessageType enums → analysis/CMD_TABLE.md
|
||
```
|
||
|
||
Key source locations inside `extracted/jadx/sources/`:
|
||
- `com/convergence/dwarflab/proto/` — 17 `*Proto.java` (descriptors).
|
||
- `com/convergence/dwarflab/data/bean/p021ws/WsCmd.java` — all command ids.
|
||
- `…/WsModuleId.java`, `…/WsMessageType.java` — enums.
|
||
- `…/request/WsMessageReq.java` — request interface (`d49.java` = sender).
|
||
- `com/convergence/dwarflab/data/websocket/<module>/` — per-module handlers.
|
||
- `p000/e49.java` — WsPacket builder (`C9312a.m36361a`).
|
||
- `p000/v55.java` — WebSocket connection manager (URL/port 9900).
|
||
- `p000/b49.java` — OkHttp WebSocket wrapper.
|