Files
dwarf-go/AGENTS.md
Jacquin Antoine 814a836c5a Reverse-engineer DWARF II telescope API and build open-source Go client
Complete reverse-engineering of the DWARFLAB Android app (v3.4.0) protocol
and implementation of a working CLI tool to control DWARF II telescopes.

Analysis (from APK decompilation with jadx):
- Extracted 17 protobuf definitions (382 messages) from embedded descriptors
- Mapped all 323 WebSocket command IDs across 16 modules
- Documented the full protocol: BLE discovery, WebSocket control (port 9900),
  RTSP preview, WsPacket envelope (proto v2.3)
- Documented the Android UI structure (screens, navigation, shooting modes)
- Key discovery: telescope responds with type=3 (reply), not type=1 (response),
  and several commands are fire-and-forget (RGB, camera open/close)

dwarfctl Go client:
- Protobuf bindings generated from extracted .proto files (397 messages)
- WebSocket transport layer with request-response matching and notification fan-out
- Typed API covering cameras, motors, astrophotography, focus, tracking, system, power
- Cobra CLI with 30+ subcommands and --debug traffic logging
- 57 unit tests (transport round-trip, command routing, proto encoding)
- Validated on real hardware: state, photo, motor slew (all directions/speeds),
  focus, RGB, time/location sync all confirmed working

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
2026-07-12 15:18:56 +02:00

5.7 KiB
Raw Permalink Blame History

AGENTS.md — DWARFLAB APK reverse-engineering workspace

This directory reverse-engineers the DWARFLAB Android app to document the DWARF II telescope control protocol for an open-source client.

What's here

DWARFLAB.apk              original APK (v3.4.0, com.convergence.dwarflab)
tools/jadx/               jadx 1.5.6 decompiler (run tools/jadx/bin/jadx)
extracted/raw/            unzip of the APK (dex, assets, lib/*.so, manifest)
extracted/jadx/           jadx decompiled Java sources (23968 classes)
analysis/
  extract_protos.py       regenerates clean .proto from jadx *Proto.java
  protos/                 17 recovered .proto files (382 messages) ← THE API
  CMD_TABLE.md            all 323 command ids ↔ modules (auto-generated)
  API_REFERENCE.md        full protocol spec (read this first)

Start with analysis/API_REFERENCE.md — it explains BLE discovery → WebSocket control (port 9900) → RTSP preview, the WsPacket envelope, and how command routing works.

The protocol in one paragraph

Phone discovers telescope over BLE (ble.proto, gets IP/SSID/PSK via DwarfEcho), joins its Wi-Fi, opens ws://<ip>:9900/?client_id=<id>, then sends binary protobuf WsPacket frames {major=2, minor=3, device_id, module_id, cmd, type, data, client_id}. cmd (1000017099) selects the operation; module_id is derived from cmd by range; data is a serialized inner proto (Req*/Res*). Live preview is a separate RTSP stream (stream0).

Commands for working here

# Re-decompile (only if APK changes)
tools/jadx/bin/jadx --show-bad-code --deobf -d extracted/jadx DWARFLAB.apk

# Regenerate .proto files after re-decompile
python3 analysis/extract_protos.py \
  extracted/jadx/sources/com/convergence/dwarflab/proto analysis/protos

# Regenerate the command table
python3 -c "…"  # see the inline script that built analysis/CMD_TABLE.md

Both extractors are deterministic — re-running after a new APK version produces a clean diff for tracking protocol changes.

Where things live in the decompiled source

All paths relative to extracted/jadx/sources/.

Looking for… Go to…
Protobuf message definitions com/convergence/dwarflab/proto/*Proto.java → run extractor → analysis/protos/
All command IDs com/convergence/dwarflab/data/bean/p021ws/WsCmd.java
Module enum (camera/astro/motor…) …/p021ws/WsModuleId.java
Message type enum (req/resp/notify) …/p021ws/WsMessageType.java
Request interface + sender …/p021ws/request/WsMessageReq.java, p000/d49.java
WsPacket envelope builder p000/e49.java (C9312a.m36361a)
WebSocket connection / URL / port p000/v55.java (field f43155b, method m55420t)
OkHttp WebSocket wrapper p000/b49.java
Inbound dispatch / req-resp matching com/convergence/dwarflab/data/websocket/WsRequestHandle.java, WsResponseHandle.java
Per-module handlers com/convergence/dwarflab/data/websocket/<module>/*WsResponseHandle.java
RTSP preview player com/convergence/dwarflab/media/RtspPlayerView.java, …/activity/capture/CaptureActivity.java
BLE handshake com/convergence/dwarflab/data/bluetooth/, …/data/bean/ble/
Camera param value maps APK assets/params_range.json, assets/shoot_plan_config.json

Gotchas

  • p000/ is the obfuscated package. R8 minification collapsed most app internals into 14 char names there (v55, b49, e49, d49, jc0…). Kotlin metadata comments (@Metadata(d2={…})) often still carry the original readable names — read them when a class is opaque.
  • module_id is never stored on a command — it is computed by range checks in WsCmd.getModuleId(). Don't grep for "set module id"; read that method.
  • Some command IDs appear as symbolic constants in WsCmd.java (jadx didn't inline them). Resolved values: IMediaPlayer.MEDIA_INFO_* = 10004/10006/10008/10009, RequestManager.NOTIFY_CONNECT_* = 10011/10012/10013, ComposeVersion.version = 13000, FirebaseError.ERROR_INVALID_CUSTOM_TOKEN = 17000, ERROR_CUSTOM_TOKEN_MISMATCH = 17002. These are already resolved in CMD_TABLE.md.
  • Notifications outnumber requests. ~130 of the 323 cmds are CMD_NOTIFY_* (1520015303). They are server-pushed (type=2), not requestable. An OSS client must subscribe to them for live state.
  • Two telescopes share one app. "DWARF II" (current, id 1) and "Bilbo" (codename for the next model, likely DWARF III) appear in shoot_plan_config.json and www/modules/eq/indexBilbo.html. Capability detection should key off StationModel from DwarfEcho, not hardcode.
  • type field (WsMessageType): request=0, response=1, notification=2, reply=3. Responses usually reuse the request's cmd; match on cmd.
  • RTSP uses TCP. The app sets rtsp_transport=tcp (RtspPlayerView.java:195). Path is rtsp://<ip>/<stream_selector>/stream0.
  • jadx reports ~294 decompile errors out of ~24k classes — expected and irrelevant for the proto/command extraction (those come from clean descriptor data, not decompiled bodies).

Conventions when extending this analysis

  • Keep generated artifacts regenerable: extract_protos.py + the CMD_TABLE inline script read only from extracted/jadx. Never hand-edit protos/ or CMD_TABLE.md.
  • When a command's payload type is unclear, find the WsMessageReq subclass in data/bean/p021ws/request/getMessage() returns the proto, getCmd() returns the WsCmd.
  • Cloud/relay features (app.dwarflabapp.com/uls, NetEase IM nim/, Huawei HMS, Firebase analytics, Meizu push, ByteDance ASR) are NOT part of telescope control — ignore them unless working on remote/cloud access.