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

110 lines
5.7 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
```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 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.