Files
dwarf-go/analysis/API_REFERENCE.md
Jacquin Antoine c2f9e54eb4 Add camera streaming guide with all access methods
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
2026-07-13 19:56:04 +02:00

16 KiB
Raw Permalink Blame History

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:// 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):

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 (1000017099)
  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.mo7785dBaseProto.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 1520015303 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
1000010499 1 CAMERA_TELE Camera.proto
1100011499 3 ASTRO Astro.proto
1200012499 2 CAMERA_WIDE Camera.proto
1300013299 4 SYSTEM System.proto
1350013799 5 RGB_POWER (RGB led / power)
1400014499 6 MOTOR MotorControl.proto
1480014899 7 TRACK Track.proto
1500015199 8 FOCUS Focus.proto
1520015499 9 NOTIFY Notify.proto
1550015599 10 PANORAMA Panorama.proto
1570015799 11 ITIPS ITips.proto
1610016399 13 SHOOTING_SCHEDULE Schedule.proto
1640016599 14 TASK_CENTER TaskCenter.proto
1670016799 15 PARAM Param.proto
1680016899 16 VOICE_ASSISTANT VoiceAssistant.proto
1700017099 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

  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_* (1300513008): 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.