Files
dwarf-go/analysis/API_REFERENCE.md
Jacquin Antoine 7dc41ec368 Add RTSP preview capture — working with both cameras
Major breakthrough: RTSP preview fully operational on DWARF Mini.

Key discovery: RTSP (port 554) only works AFTER opening the camera via
WebSocket. Without CMD_CAMERA_OPEN (10000/12000), the RTSP server accepts
connections but never sends frames. This was the missing piece.

RTSP details:
- Tele: rtsp://<ip>:554/ch0/stream0
- Wide: rtsp://<ip>:554/ch1/stream0
- Codec: MJPEG over RTSP (not H.264)
- Transport: TCP (confirmed from RtspPlayerView.java ijkplayer options)
- Resolution: 1920x1080 both cameras

Port 8092 (MJPEG HTTP /mainstream, /secondstream) is inactive on the Mini.
The DWARF Mini uses RTSP exclusively, not the HTTP MJPEG fallback.

Added `dwarfctl preview grab [--cam tele|wide] [output.jpg]` that:
1. Opens the camera via WebSocket
2. Waits 2s for RTSP to become available
3. Captures one frame via ffmpeg

Updated API_REFERENCE.md with the corrected RTSP details and the camera-open
prerequisite.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-13 19:14:49 +02:00

15 KiB
Raw 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.

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.