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