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
This commit is contained in:
109
AGENTS.md
Normal file
109
AGENTS.md
Normal file
@ -0,0 +1,109 @@
|
||||
# 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
|
||||
|
||||
```bash
|
||||
# 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_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_*`
|
||||
(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.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.
|
||||
Reference in New Issue
Block a user