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
16 KiB
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:
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://ifws_scheme == "wss". client_idis a client-generated identifier (y32…m58158c()).- Source:
v55.javafieldf43155b/ methodm55420t(ip). - device_id field in the envelope selects which telescope (multi-device),
defaults to
1for 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_CAMERA10000 orCMD_CAMERA_WIDE_OPEN_CAMERA12000) 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(seeRtspPlayerView.java:195). - Codec: MJPEG over RTSP (not H.264).
- Port 8092 (MJPEG HTTP
/mainstreamand/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:
- Open the wide camera (
CMD_CAMERA_WIDE_OPEN_CAMERA12000) — or letorient livedo it automatically - Grab frames via
ffmpeg -rtsp_transport tcp -i rtsp://<ip>/ch1/stream0 - 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):
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):
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.
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
- (once) BLE: connect, get
DwarfEcho→ extractsta.ipv4/ap.ipv4,psw, join Wi-Fi. - Open WebSocket
ws://<ip>:9900/?client_id=droid-oss-001. - Open the Tele camera: send
WsPacket{cmd=10000, data=ReqOpenCamera}, wait forComResponse{code=0}. - Set exposure (optional):
cmd=10009withReqSetExp{…}. - Photograph:
cmd=10002(CMD_CAMERA_TELE_PHOTOGRAPH). - 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 (Teleid=0,Wideimplicit), their FoV (fvWidth/fvHeight), preview size (1280×720), and every supported param withmin/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_idinWsPacketselects the active telescope when several are on the same network; default1.- 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=3always. - Reuse one persistent WebSocket; don't reconnect per command.
- Maintain a registry of
cmd → asyncio.Futurefor 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.