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
5.7 KiB
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 (10000–17099) 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 1–4 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_idis never stored on a command — it is computed by range checks inWsCmd.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 inCMD_TABLE.md. - Notifications outnumber requests. ~130 of the 323 cmds are
CMD_NOTIFY_*(15200–15303). 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.jsonandwww/modules/eq/indexBilbo.html. Capability detection should key offStationModelfromDwarfEcho, not hardcode. typefield (WsMessageType): request=0, response=1, notification=2, reply=3. Responses usually reuse the request'scmd; match oncmd.- RTSP uses TCP. The app sets
rtsp_transport=tcp(RtspPlayerView.java:195). Path isrtsp://<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 fromextracted/jadx. Never hand-editprotos/orCMD_TABLE.md. - When a command's payload type is unclear, find the
WsMessageReqsubclass indata/bean/p021ws/request/—getMessage()returns the proto,getCmd()returns theWsCmd. - Cloud/relay features (
app.dwarflabapp.com/uls, NetEase IMnim/, Huawei HMS, Firebase analytics, Meizu push, ByteDance ASR) are NOT part of telescope control — ignore them unless working on remote/cloud access.