From 8daa9b4d58db2d415dbc6b5dfc2a2a7fd1dcdefc Mon Sep 17 00:00:00 2001 From: Jacquin Antoine Date: Mon, 13 Jul 2026 20:07:59 +0200 Subject: [PATCH] Add deep protocol specification covering all layers MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comprehensive 15-section technical reference for the DWARF telescope protocol, consolidating everything learned from APK reverse-engineering and live hardware testing: 1. Network architecture (BLE, WebSocket, RTSP, HTTP) 2. BLE discovery handshake (DwarfPing/DwarfEcho) 3. WebSocket control layer (connection, frames, lifecycle) 4. WsPacket envelope (all 8 fields explained) 5. Command routing (module_id derivation by cmd range) 6. Response model (3 patterns: reply, notification-only, fire-and-forget) 7. Notification system (~130 notification types, fan-out) 8. Camera subsystem (lifecycle, params, capture modes) 9. Motor subsystem (joystick polar coords, limits, init, gotchas) 10. Focus subsystem (commands, position tracking) 11. Astro subsystem (calibration, GoTo, stacking, EQ solving) 12. Preview/RTSP (URLs, prerequisites, ijkplayer options) 13. Device models & feature gating (6 models, feature matrix) 14. Protocol quirks (10 gotchas discovered during live testing) 15. Error codes (motor, camera, astro, general) ๐Ÿ’˜ Generated with Crush Assisted-by: Crush:glm-5.2 --- analysis/PROTOCOL_SPEC.md | 975 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 975 insertions(+) create mode 100644 analysis/PROTOCOL_SPEC.md diff --git a/analysis/PROTOCOL_SPEC.md b/analysis/PROTOCOL_SPEC.md new file mode 100644 index 0000000..3061e0e --- /dev/null +++ b/analysis/PROTOCOL_SPEC.md @@ -0,0 +1,975 @@ +# DWARF Telescope Protocol โ€” Deep Specification + +Reverse-engineered from `DWARFLAB.apk` v3.4.0 and validated by live testing +on a DWARF Mini (firmware 2.1.6 equivalent, device_id=4). + +This document is the authoritative technical reference for the DWARF telescope +control protocol. It covers every layer from physical connection to +application semantics, including all quirks discovered during live testing. + +--- + +## Table of Contents + +1. [Network Architecture](#1-network-architecture) +2. [Discovery Layer (BLE)](#2-discovery-layer-ble) +3. [Control Layer (WebSocket)](#3-control-layer-websocket) +4. [WsPacket Envelope](#4-wspacket-envelope) +5. [Command Routing](#5-command-routing) +6. [Response Model](#6-response-model) +7. [Notification System](#7-notification-system) +8. [Camera Subsystem](#8-camera-subsystem) +9. [Motor Subsystem](#9-motor-subsystem) +10. [Focus Subsystem](#10-focus-subsystem) +11. [Astro Subsystem](#11-astro-subsystem) +12. [Preview / RTSP](#12-preview--rtsp) +13. [Device Models & Feature Gating](#13-device-models--feature-gating) +14. [Protocol Quirks & Gotchas](#14-protocol-quirks--gotchas) +15. [Error Codes](#15-error-codes) + +--- + +## 1. Network Architecture + +The telescope exposes three network services: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” BLE GATT โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Client โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ Telescope โ”‚ +โ”‚ (phone/PC) โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ WebSocket (binary) โ”‚ โ”‚ +โ”‚ โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ โ”‚ +โ”‚ โ”‚ ws://:9900 โ”‚ โ”‚ +โ”‚ โ”‚ โ”‚ โ”‚ +โ”‚ โ”‚ RTSP (TCP) โ”‚ โ”‚ +โ”‚ โ”‚โ—„โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ โ”‚ +โ”‚ โ”‚ rtsp://:554/... โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +| Service | Port | Protocol | Purpose | +|---------|------|----------|---------| +| **Control** | 9900 | WebSocket (binary protobuf) | All commands, queries, notifications | +| **Preview** | 554 | RTSP over TCP | Live video stream (MJPEG codec) | +| **HTTP** | 80 | nginx | Static files, OTA download page | +| **MJPEG** (alt) | 8092 | HTTP multipart/x-mixed-replace | JPEG preview (inactive on Mini) | + +### Connection sequence + +``` +1. BLE: Client sends DwarfPing โ†’ Telescope responds DwarfEcho (IP, SSID, PSK) +2. Client joins telescope Wi-Fi (AP mode or shared STA) +3. Client opens WebSocket: ws://:9900/?client_id= +4. Client sends commands via WsPacket protobuf frames +5. Telescope responds with type=3 replies and type=2 notifications +6. Client opens RTSP: rtsp://:554/chN/stream0 (requires camera open first) +``` + +### Multiple connections + +The telescope supports **multiple concurrent WebSocket connections**. Each +connection receives its own replies but **notifications are broadcast to all +connections**. The `client_id` field in the WebSocket URL and in each WsPacket +identifies the source client. + +**Important**: the telescope has a connection limit. Rapid connect/disconnect +cycles (as our CLI originally did โ€” one connection per command) can trigger +connection resets. For sustained operation, use a **persistent connection**. + +--- + +## 2. Discovery Layer (BLE) + +### BLE handshake + +The telescope advertises over BLE. The client sends a `DwarfPing` and receives +a `DwarfEcho` containing all connection details: + +```proto +message DwarfPing { + VocalType vocaltype = 1; // VT_PING = 1 + uint64 timestamp = 2; + bytes magic = 3; + repeated bytes vocals = 4; // authentication tokens + repeated bytes mutes = 5; +} + +message DwarfEcho { + VocalType vocaltype = 1; // VT_ECHO = 2 + uint64 timestamp = 2; + bytes magic = 3; + uint64 ts_ping = 4; + bytes mac_address = 5; + StationModel model = 6; // {family, revision} identifies the device + string sn = 7; // serial number + string name = 8; // advertised name ("DWARFxxxx") + string psw = 9; // device password + string fw_version = 10; + string ws_scheme = 11; // "ws" or "wss" + uint32 session = 12; + NifAP ap = 13; // AP network config (ssid, psw, ipv4, ipv6) + NifSTA sta = 14; // STA network config (ssid, psw, rssi, ipv4) +} +``` + +The `ble_psd` (BLE password) authenticates config requests (`ReqGetconfig`, +`ReqAp`, `ReqSta`, `ReqSetblewifi`). Default password is printed on the device. + +### Wi-Fi modes + +| Mode | Description | Use case | +|------|-------------|----------| +| **AP** | Telescope creates a hotspot; client connects directly | Outdoor, no router | +| **STA** | Telescope joins an existing Wi-Fi network | Home observatory | +| **Auto** | Firmware chooses based on environment | Default (recommended) | + +In AP mode, the telescope's IP is always `192.168.88.1`. In STA mode, the IP +is assigned by the router's DHCP (check `ResSta.ip` from BLE or router table). + +--- + +## 3. Control Layer (WebSocket) + +### Connection + +``` +ws://:9900/?client_id= +``` + +- Plain `ws://` by default. `wss://` if `ws_scheme == "wss"` (rare). +- `client_id`: arbitrary string identifying this client. Used in the URL query + parameter and in every WsPacket's `client_id` field. +- Handshake timeout: ~10 seconds. +- The connection stays open indefinitely. Keep-alive is handled by the + telescope's `HeartbeatService`. + +### Frame format + +Every WebSocket message is a **binary frame** containing one serialized +`WsPacket` protobuf message. No text frames, no JSON, no delimiters. + +### Connection lifecycle + +``` +Client Telescope + โ”‚ โ”‚ + โ”‚โ”€โ”€ WS Connect โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ + โ”‚โ—„โ”€ WS Handshake OK โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚ + โ”‚ โ”‚ + โ”‚โ”€โ”€ WsPacket (request) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ + โ”‚โ—„โ”€โ”€ WsPacket (reply type=3) โ”€โ”€โ”€โ”€โ”‚ + โ”‚ โ”‚ + โ”‚โ—„โ”€โ”€ WsPacket (notify type=2) โ”€โ”€โ”€โ”‚ (unsolicited) + โ”‚ โ”‚ + โ”‚โ”€โ”€ WsPacket (request) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ + โ”‚โ—„โ”€โ”€ WsPacket (notify type=2) โ”€โ”€โ”€โ”‚ (side effect) + โ”‚โ—„โ”€โ”€ WsPacket (reply type=3) โ”€โ”€โ”€โ”€โ”‚ + โ”‚ โ”‚ + โ”‚โ”€โ”€ WS Close โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–บโ”‚ + โ”‚ โ”‚ +``` + +--- + +## 4. WsPacket Envelope + +Every command, response, and notification is wrapped in this envelope: + +```proto +message WsPacket { + uint32 major_version = 1; // always 2 + uint32 minor_version = 2; // always 3 โ†’ protocol v2.3 + uint32 device_id = 3; // telescope index (default 1) + uint32 module_id = 4; // derived from cmd (see ยง5) + uint32 cmd = 5; // operation ID (10000โ€“17099) + uint32 type = 6; // 0=request, 1=response, 2=notification, 3=reply + bytes data = 7; // inner protobuf message (serialized) + string client_id = 8; // client identifier +} +``` + +### Version + +The protocol version is **2.3** (`major_version=2, minor_version=3`). These +values are **always the same** โ€” they are set by the Android app from the +`WsMajorVersion` and `WsMinorVersion` enums. Future firmware may bump the +minor version. + +### device_id + +Selects which telescope to address when multiple scopes are on the same +network (multi-device support). Default is `1`. The Android app reads this +from `App.INSTANCE.getConnectedDeviceId()`. + +### module_id + +**Not stored per-command** โ€” it is **derived from `cmd`** by range checks. +See [ยง5 Command Routing](#5-command-routing). + +### type (message type) + +| Value | Name | Direction | Meaning | +|-------|------|-----------|---------| +| 0 | `request` | Client โ†’ Telescope | A command from the client | +| 1 | `response` | Telescope โ†’ Client | (defined in enum, **not used in practice**) | +| 2 | `notification` | Telescope โ†’ Client | Unsolicited push event | +| 3 | `reply` | Telescope โ†’ Client | Direct response to a request | + +**Critical discovery**: the telescope uses `type=3` (reply) for direct +responses, **not** `type=1` (response). The `type=1` value exists in the +`WsMessageType` enum but was never observed in live traffic. + +### data + +Contains the serialized inner protobuf message. For commands with no payload +(e.g. `CMD_ASTRO_START_CALIBRATION`), `data` is empty (0 bytes). For commands +with parameters (e.g. `ReqSetExp{index}`), `data` contains the serialized +proto message. + +### client_id + +Matches the `client_id` from the WebSocket URL. Used by the telescope to +track which client sent which request. + +### Envelope construction (from APK `e49.java`) + +```java +WsPacket.newBuilder() + .setMajorVersion(2) // WS_MAJOR_VERSION_NUMBER + .setMinorVersion(3) // WS_MINOR_VERSION_NUMBER + .setDeviceId(connectedDeviceId) // default 1 + .setModuleId(cmd.getModuleId().ordinal()) // derived from cmd range + .setCmd(cmd.getCmd()) // the integer command ID + .setType(cmd.getMessageType().ordinal()) // 0 = request + .setData(ByteString.copyFrom(innerProto.toByteArray())) + .setClientId(clientId) + .build(); +``` + +--- + +## 5. Command Routing + +### Module derivation + +The `module_id` field is computed from `cmd` using range checks identical to +`WsCmd.getModuleId()` in the APK: + +``` +if cmd in [10000, 10500) โ†’ MODULE_CAMERA_TELE (ordinal 1) +if cmd in [11000, 11500) โ†’ MODULE_ASTRO (ordinal 3) +if cmd in [12000, 12500) โ†’ MODULE_CAMERA_WIDE (ordinal 2) +if cmd in [13000, 13300) โ†’ MODULE_SYSTEM (ordinal 4) +if cmd in [13500, 13800) โ†’ MODULE_RGB_POWER (ordinal 5) +if cmd in [14000, 14500) โ†’ MODULE_MOTOR (ordinal 6) +if cmd in [14800, 14900) โ†’ MODULE_TRACK (ordinal 7) +if cmd in [15000, 15200) โ†’ MODULE_FOCUS (ordinal 8) +if cmd in [15200, 15500) โ†’ MODULE_NOTIFY (ordinal 9) +if cmd in [15500, 15600) โ†’ MODULE_PANORAMA (ordinal 10) +if cmd in [15700, 15800) โ†’ MODULE_ITIPS (ordinal 11) +if cmd in [16100, 16400) โ†’ MODULE_SHOOTING_SCHEDULE (ordinal 13) +if cmd in [16400, 16600) โ†’ MODULE_TASK_CENTER (ordinal 14) +if cmd in [16700, 16800) โ†’ MODULE_PARAM (ordinal 15) +if cmd in [16800, 16900) โ†’ MODULE_VOICE_ASSISTANT (ordinal 16) +if cmd in [17000, 17100) โ†’ MODULE_DEVICE (ordinal 18) +else โ†’ MODULE_NONE (ordinal 0) +``` + +### Command ID allocation + +Commands are grouped by module in blocks of ~500 IDs: + +``` +10000โ€“10499 Camera Tele +11000โ€“11499 Astro +12000โ€“12499 Camera Wide +13000โ€“13299 System +13500โ€“13799 RGB / Power +14000โ€“14499 Motor +14800โ€“14899 Track +15000โ€“15199 Focus +15200โ€“15499 Notify (server-push only) +15500โ€“15599 Panorama +15700โ€“15799 ITips +16100โ€“16399 Shooting Schedule +16400โ€“16599 Task Center +16700โ€“16799 Param +16800โ€“16899 Voice Assistant +17000โ€“17099 Device +``` + +See `CMD_TABLE.md` for the full list of all 323 commands. + +--- + +## 6. Response Model + +### Three response patterns + +The telescope uses three distinct patterns depending on the command: + +#### Pattern A: Request โ†’ Reply (type=3) + +Most commands that perform an action and return immediately: + +``` +Client sends: WsPacket{cmd=10002, type=0, data=Empty} +Telescope replies: WsPacket{cmd=10002, type=3, data=ComResponse{code=0}} +``` + +The reply uses the **same `cmd`** as the request. The `data` field contains +the response proto (often just `ComResponse{code}` where `code=0` means OK). + +**Commands using this pattern** (confirmed by live testing): +- `CMD_CAMERA_TELE_PHOTOGRAPH` (10002) โ€” photo capture +- `CMD_CAMERA_WIDE_PHOTOGRAPH` (12022) โ€” wide photo +- `CMD_FOCUS_AUTO_FOCUS` (15000) โ€” autofocus +- `CMD_FOCUS_MANUAL_SINGLE_STEP_FOCUS` (15001) โ€” manual focus step +- `CMD_SYSTEM_SET_TIME` (13000) โ€” time sync +- `CMD_SYSTEM_SET_LOCATION` (13010) โ€” location set +- `CMD_GLOBAL_TASK_GET_DEVICE_STATE_INFO` (16405) โ€” state query +- `CMD_STEP_MOTOR_GET_POSITION` (14001) โ€” motor position read + +#### Pattern B: Request โ†’ Notifications only (no reply) + +Some commands produce **only notifications** (type=2), never a direct reply. +The client must listen for the notification to know the result. + +``` +Client sends: WsPacket{cmd=13501, type=0, data=Empty} +Telescope pushes: WsPacket{cmd=15221, type=2, data=NotifyRgbState{...}} +(no type=3 reply ever comes) +``` + +**Commands using this pattern**: +- `CMD_RGB_POWER_CLOSE_RGB` (13501) โ€” RGB off +- `CMD_RGB_POWER_OPEN_RGB` (13500) โ€” RGB on +- `CMD_CAMERA_TELE_OPEN_CAMERA` (10000) โ€” open camera (no ack) +- `CMD_CAMERA_TELE_CLOSE_CAMERA` (10001) โ€” close camera +- `CMD_CAMERA_TELE_GET_ALL_PARAMS` (10036) โ€” get params (notif only) + +For these commands, the client should use **fire-and-forget** (send without +waiting for a reply) and verify the result via `GetDeviceState` or by +listening for the relevant notification. + +#### Pattern C: Fire-and-forget (no ack of any kind) + +Motor joystick commands produce no response at all: + +``` +Client sends: WsPacket{cmd=14006, type=0, data=ReqMotorServiceJoystick{...}} +(no reply, no notification) +``` + +**Commands using this pattern**: +- `CMD_STEP_MOTOR_SERVICE_JOYSTICK` (14006) โ€” joystick slew +- `CMD_STEP_MOTOR_SERVICE_JOYSTICK_FIXED_ANGLE` (14007) +- `CMD_STEP_MOTOR_SERVICE_JOYSTICK_STOP` (14008) +- `CMD_STEP_MOTOR_RUN` (14000) โ€” motor run to position +- `CMD_STEP_MOTOR_STOP` (14002) โ€” motor stop + +### Response matching + +Replies are matched by **`cmd` value**. When a client sends a request with +`cmd=10002`, it registers a pending continuation for `cmd=10002`. When a +`WsPacket` arrives with the same `cmd` and `type=3`, it is delivered to that +continuation. + +**Timeout**: if no reply arrives within 10 seconds, the request fails. +For Pattern B and C commands, no timeout should be set (fire-and-forget). + +### Inner response types + +Most replies contain one of these generic response messages: + +```proto +message ComResponse { int32 code = 1; } // code=0 means success +message ComResWithInt { int32 value = 1; int32 code = 2; } +message ComResWithDouble { double value = 1; int32 code = 2; } +message ComResWithString { string str = 1; int32 code = 2; } +``` + +Negative `code` values are errors (see [ยง15 Error Codes](#15-error-codes)). + +--- + +## 7. Notification System + +### Overview + +The telescope pushes real-time state changes as `WsPacket` frames with +`type=2` (notification). These are unsolicited โ€” the client did not request +them. They are sent to **all connected WebSocket clients**. + +### Notification command range + +All notifications use `cmd` values in the range **15200โ€“15399** +(`MODULE_NOTIFY`, ordinal 9). There are ~130 notification types. + +### Key notifications + +| cmd | Name | Trigger | Data | +|-----|------|---------|------| +| 15201 | `NOTIFY_ELE` | Elevation change | `OperationState` | +| 15202 | `NOTIFY_CHARGE` | Charging state change | `ChargingState` | +| 15203 | `NOTIFY_SDCARD_INFO` | SD card status | `StorageInfo` | +| 15210 | `NOTIFY_STATE_ASTRO_CALIBRATION` | Calibration progress | `AstroState` | +| 15211 | `NOTIFY_STATE_ASTRO_GOTO` | GoTo progress | `AstroState` | +| 15221 | `NOTIFY_RGB_STATE` | RGB ring state change | `RgbState` | +| 15225 | `NOTIFY_TRACK_RESULT` | Tracking result | `TrackResult` | +| 15234 | `NOTIFY_STREAM_TYPE` | Stream type available | `StreamType{type, cam_id}` | +| 15243 | `NOTIFY_TEMPERATURE` | Temperature reading | `Temperature` | +| 15256 | `NOTIFY_CALIBRATION_RESULT` | Calibration complete | `CalibrationResult` | +| 15257 | `NOTIFY_FOCUS_POSITION` | Focus motor moved | `FocusPosition` | +| 15273 | `NOTIFY_PHOTO_STATE` | Photo capture state | `PhotoState` | +| 15274 | `NOTIFY_BURST_STATE` | Burst capture state | `BurstState` | +| 15275 | `NOTIFY_RECORD_STATE` | Video record state | `RecordState` | +| 15292 | `NOTIFY_CMOS_TEMPERATURE` | CMOS sensor temp | `CmosTemperature` | +| 15295 | `NOTIFY_DEVICE_ATTITUDE` | IMU attitude (pitch/yaw/roll) | `DeviceAttitude` | + +### Notification delivery + +Notifications arrive on the same WebSocket connection as commands. The client +must continuously read from the socket and dispatch notifications separately +from replies. A typical client architecture: + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ WebSocket Reader โ”‚ +โ”‚ (single goroutine/thread) โ”‚ +โ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค +โ”‚ type=3 โ”‚ type=2 โ”‚ +โ”‚ reply โ”‚ notification โ”‚ +โ”‚ โ†“ โ”‚ โ†“ โ”‚ +โ”‚ pending โ”‚ fan-out to all โ”‚ +โ”‚ request โ”‚ subscribers โ”‚ +โ”‚ waiters โ”‚ โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ดโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +### DeviceAttitude (cmd 15295) + +The IMU pushes pitch/yaw/roll via this notification โ€” but **only during +calibration or EQ solving**, not during free slew. This means there is no +way to read the telescope's orientation during manual joystick movement +without astro calibration. + +```proto +message DeviceAttitude { + double pitch = 1; // degrees + double yaw = 2; // degrees + double roll = 3; // degrees +} +``` + +--- + +## 8. Camera Subsystem + +### Two cameras + +| Camera | cam_id | Commands | RTSP channel | +|--------|--------|----------|-------------| +| **Tele** | 0 | 10000โ€“10499 | `ch0` | +| **Wide** | 1 | 12000โ€“12499 | `ch1` | + +### Camera lifecycle + +``` +1. CMD_GLOBAL_TASK_MANAGER_ENTER_CAMERA (16404) + โ†’ ReqEnterCamera{client_param{encode_type: 0}} + โ†’ ResEnterCamera{code, shooting_mode_id} + +2. CMD_CAMERA_TELE_OPEN_CAMERA (10000) or CMD_CAMERA_WIDE_OPEN_CAMERA (12000) + โ†’ No payload + โ†’ No type=3 reply (fire-and-forget) + โ†’ Notifications: NOTIFY_STREAM_TYPE (15234) + +3. [Camera is now streaming on RTSP] + +4. CMD_CAMERA_TELE_CLOSE_CAMERA (10001) or CMD_CAMERA_WIDE_CLOSE_CAMERA (12001) + โ†’ No payload, fire-and-forget +``` + +### Camera parameters + +Parameters are set via dedicated commands (10007โ€“10050 for Tele, 12002โ€“12036 +for Wide). Each parameter has a SET and GET variant: + +| Parameter | Tele SET | Tele GET | Proto | +|-----------|----------|----------|-------| +| Exposure mode | 10007 | 10008 | `CommonParam` | +| Exposure | 10009 | 10010 | `ReqSetExp{index}` | +| Gain mode | 10011 | 10012 | `CommonParam` | +| Gain | 10013 | 10014 | `ReqSetGain{index}` | +| Brightness | 10015 | 10016 | `CommonParam` | +| Contrast | 10017 | 10018 | `CommonParam` | +| Saturation | 10019 | 10020 | `CommonParam` | +| Hue | 10021 | 10022 | `CommonParam` | +| Sharpness | 10023 | 10024 | `CommonParam` | +| WB mode | 10025 | 10026 | `CommonParam` | +| IR cut | 10031 | 10032 | `CommonParam` | + +**Note**: SET commands receive a type=3 reply. GET commands (`GET_ALL_PARAMS` +10036/12027) produce **only notifications** on the Mini โ€” the parameters are +also available embedded in the `GetDeviceState` response. + +### Exposure index mapping + +The exposure `index` maps to shutter speeds defined in `assets/params_range.json`: +- Index 0 = 1/10000s +- Index 156 = 1/4s +- Index increments of 3 +- Higher index = longer exposure + +### Photo capture + +| Type | Tele cmd | Wide cmd | Behavior | +|------|----------|----------|----------| +| JPEG photo | 10002 | 12022 | type=3 reply with `ComResponse` | +| RAW photo | 10041 | 12029 | type=3 reply (slower, may timeout) | +| Burst start | 10003 | 12023 | notifications for each frame | +| Burst stop | 10004 | 12024 | type=3 reply | +| Video start | 10005 | 12030 | notifications for record time | +| Video stop | 10006 | 12031 | type=3 reply | +| Timelapse start | 10033 | 12025 | notifications for progress | +| Timelapse stop | 10034 | 12026 | type=3 reply | + +--- + +## 9. Motor Subsystem + +### Motor axes + +The DWARF has two stepper motors: + +| Motor ID | Axis | Function | +|----------|------|----------| +| 0 | Azimuth (horizontal) | Left-right rotation | +| 1 | Altitude (vertical) | Up-down tilt | + +### Joystick control (primary method) + +The Android app controls motors exclusively via joystick commands โ€” there is +no absolute "goto position" command in the app's UI for manual pointing. + +```proto +message ReqMotorServiceJoystick { + double vector_angle = 1; // degrees: 0=right, 90=up, 180=left, 270=down + double vector_length = 2; // 0.0 to 1.0 (speed magnitude) +} +``` + +**Coordinate system** (confirmed by live testing, alt-az config, telescope +vertical on tripod): + +| Angle | Direction | +|-------|-----------| +| 0ยฐ | Azimuth rotation clockwise (viewed from above) | +| 90ยฐ | Altitude UP (toward sky) | +| 180ยฐ | Azimuth rotation counter-clockwise | +| 270ยฐ | Altitude DOWN (toward ground) | + +The joystick is **polar** โ€” any angle from 0ยฐ to 360ยฐ moves both motors +simultaneously. `vector_length` controls speed (0=stop, 1=max). + +**IMPORTANT**: the joystick is a **velocity vector**, not a position target. +The motors move continuously while the command is active. To stop, send +`vector_length=0` or `CMD_STEP_MOTOR_SERVICE_JOYSTICK_STOP` (14008). + +### Motor commands + +| Command | ID | Proto | Response | +|---------|-----|-------|----------| +| Joystick slew | 14006 | `ReqMotorServiceJoystick{angle, length}` | Fire-and-forget | +| Joystick fixed angle | 14007 | `ReqMotorServiceJoystickFixedAngle{angle, length}` | Fire-and-forget | +| Joystick stop | 14008 | `ReqMotorServiceJoystickStop` | Fire-and-forget | +| Motor run | 14000 | `ReqMotorRun{id, speed, direction, ...}` | Fire-and-forget | +| Motor run to position | 14000 | `ReqMotorRunTo{id, end_position, speed, ...}` | Fire-and-forget | +| Motor stop | 14002 | `ReqMotorStop{id}` | Fire-and-forget | +| Get position | 14001 | `ReqMotorGetPosition{id}` | type=3 reply: `ResMotorPosition{id, code, position}` | + +### Position reading + +`CMD_STEP_MOTOR_GET_POSITION` (14001) returns: + +```proto +message ResMotorPosition { + int32 id = 1; + int32 code = 2; // 0=OK, -14520=NEED_RESET + double position = 3; // degrees (only valid if code=0) +} +``` + +After power-on, `code` is always `-14520` (`CODE_STEP_MOTOR_NEED_RESET`). +The motors have no absolute encoders โ€” position is tracked by step counting +and lost on power cycle. Absolute position is only available after astro +calibration (plate solving). + +### Motor initialization + +**There is no API command to initialize motors.** The Android app does not +use `ReqMotorReset` โ€” it relies on: + +1. **Astro calibration** (night, starry sky): homes motors + plate solving +2. **Mechanical limit homing** (discovered during testing): slewing to a + mechanical limit triggers an automatic firmware home to position 0. + +Manual init procedure: +``` +1. motor slew 90 0.4 (move up ~45ยฐ to clear bottom limit) +2. motor init down 40 (slew 270ยฐ until bottom limit โ†’ auto-home to 0) +3. Position 0 = optics pointing toward ground +``` + +### Mechanical limits + +All four limits are handled **safely** by the firmware: + +| Limit | Slew direction | Behavior | +|-------|---------------|----------| +| Altitude bottom (ground) | 270ยฐ | Auto-home to position 0 | +| Altitude top (sky) | 90ยฐ | Stop at limit switch | +| Azimuth clockwise | 0ยฐ | Stop at limit switch | +| Azimuth counter-clockwise | 180ยฐ | Stop at limit switch | + +No crashes, no axis lockups, no reboots when hitting limits via slew. + +### โš ๏ธ Motor reset command (cmd 14003) โ€” DANGEROUS + +`CMD_STEP_MOTOR_RESET` (14003) is defined in the proto but **not used by the +Android app**. Sending it causes the telescope to **immediately reboot**. +It does NOT home the motors. Do not use. + +--- + +## 10. Focus Subsystem + +### Focus motor + +The focus motor is a separate stepper (not the altitude/azimuth motors). +Its position is tracked as an integer `pos` value. + +```proto +message FocusPosition { int32 pos = 1; } +``` + +Position is visible in `GetDeviceState` โ†’ `focus_motor_state_info.focus_position.pos`. +Typical range: 0โ€“800+. Position 594 was observed on our test unit. + +### Focus commands + +| Command | ID | Proto | Response | +|---------|-----|-------|----------| +| Auto focus | 15000 | (empty) | type=3 reply | +| Manual single step | 15001 | `ReqManualSingleStepFocus{direction}` | type=3 reply | +| Start manual continuous | 15002 | `ReqStartManualContinuousFocus{direction}` | type=3 | +| Stop manual continuous | 15003 | (empty) | type=3 | +| Start astro AF | 15004 | (empty) | type=3 | +| Stop astro AF | 15005 | (empty) | type=3 | +| Get user infinity pos | 15011 | (empty) | type=3 | +| Set user infinity pos | 15012 | `ReqSetUserInfinityPos{pos}` | type=3 | + +`direction` in `ReqManualSingleStepFocus`: `0` = focus in (closer), +`1` = focus out (farther). + +Notifications: `NOTIFY_FOCUS_POSITION` (15257) pushes position updates, +`NOTIFY_NORMAL_AUTO_FOCUS_STATE` (15279) pushes AF progress. + +--- + +## 11. Astro Subsystem + +### Calibration + +```proto +message ReqStartCalibration { + double lon = 1; // longitude (degrees) + double lat = 2; // latitude (degrees) +} +``` + +Calibration (cmd 11000) requires: +- Correct GPS coordinates (lon/lat) +- Starry night sky (plate solving on stars) +- Camera open and focused + +The telescope takes images, plate-solves them against a star database, and +calculates its orientation. On success, motors are homed and absolute +position becomes available. + +Notifications during calibration: +- `NOTIFY_STATE_ASTRO_CALIBRATION` (15210) โ€” progress/state +- `NOTIFY_CALIBRATION_RESULT` (15256) โ€” final result + +### GoTo + +| Target type | Command | Proto | +|-------------|---------|-------| +| Deep-sky object (RA/Dec) | 11002 | `ReqGotoDSO{ra, dec, target_name, goto_only}` | +| Solar system object | 11003 | `ReqGotoSolarSystem{index, lon, lat, target_name, force_start}` | +| Stop GoTo | 11004 | (empty) | +| One-click GoTo DSO | 11013 | `ReqOneClickGotoDSO{...}` | +| One-click GoTo solar | 11014 | `ReqOneClickGotoSolarSystem{...}` | + +RA/Dec are in **degrees** (not hours/minutes). `goto_only=false` means the +telescope will also track after slewing. + +Solar system object `index`: +1=Mercury, 2=Venus, 3=Earth(not used), 4=Mars, 5=Jupiter, 6=Saturn, +7=Uranus, 8=Neptune. Sun and Moon have separate shooting modes (8, 9). + +### Live stacking + +| Command | ID | +|---------|-----| +| Start tele live stacking | 11005 | +| Stop tele live stacking | 11006 | +| Start wide live stacking | 11016 | +| Stop wide live stacking | 11017 | +| Fast stop tele | 11037 | +| Fast stop wide | 11038 | + +### EQ solving (polar alignment) + +| Command | ID | +|---------|-----| +| Start EQ solving | 11018 | +| Stop EQ solving | 11019 | + +EQ solving aligns the telescope's equatorial axis with Earth's rotation axis. +Required for long-exposure astrophotography without star trailing. + +--- + +## 12. Preview / RTSP + +### RTSP stream details + +| Property | Value | +|----------|-------| +| URL (Tele) | `rtsp://:554/ch0/stream0` | +| URL (Wide) | `rtsp://:554/ch1/stream0` | +| Port | 554 | +| Transport | TCP (`rtsp_transport=tcp` mandatory) | +| Codec | MJPEG (not H.264) | +| Resolution | 1920ร—1080 | +| FPS | Variable (15โ€“30 depending on lighting) | + +### Critical prerequisite + +**The camera MUST be opened via WebSocket before connecting to RTSP.** +Without `CMD_CAMERA_OPEN` (10000 or 12000), the RTSP server accepts TCP +connections but never sends any video frames. + +Sequence: +``` +1. WS: send CMD_CAMERA_WIDE_OPEN_CAMERA (12000) โ†’ camera opens +2. Wait ~2 seconds for RTSP server to start +3. RTSP: connect to rtsp://:554/ch1/stream0 โ†’ frames flow +``` + +### MJPEG HTTP (port 8092) โ€” alternative + +The APK contains a fallback MJPEG-over-HTTP system on port 8092: +- `/mainstream` = Tele camera +- `/secondstream` = Wide camera +- Content-Type: `multipart/x-mixed-replace; boundary=boundary` + +This endpoint responds with HTTP 200 + correct headers but **sends no frames** +on the DWARF Mini. It may be active on other models (DWARF II/3). The choice +between RTSP and MJPEG is determined by the `CMD_NOTIFY_STREAM_TYPE` (15234) +notification that the telescope sends after camera open. + +### ijkplayer options (from APK) + +The Android app uses `ijkplayer` with these RTSP options: +``` +rtsp_transport = tcp +framedrop = 30 +fps = 30 +skip_loop_filter = 0 +skip_idct = 0 +skip_frame = 0 +packet-buffering = 0 +fflags = nobuffer +infbuf = 1 +max-buffer-size = 10485760 +min-frames = 2 +start-on-prepared = 1 +probesize = 4096 +analyzeduration = 1000000 +flush_packets = 1 +dns_cache_clear = 1 +dns_cache_timeout = -1 +mediacodec = 1 (hardware decode) +mediacodec-hevc = 1 +``` + +These options prioritize **low latency** over quality. For ffmpeg/mpv, the +essential ones are `rtsp_transport=tcp` and low buffer sizes. + +--- + +## 13. Device Models & Feature Gating + +### Supported models + +| Model | deviceId | WhenMappings | Optics | +|-------|----------|:---:|--------| +| DWARF_2 | 1 | 2 | Tele 7.4ร—6.0ยฐ, Wide 83.4ร—51.9ยฐ | +| DWARF_3 | 2 | 3 | Tele 5.37ร—4.3ยฐ, Wide 83.4ร—51.9ยฐ | +| **DWARF_MINI** | **4** | **1** | Tele 8.0ร—6.5ยฐ, Wide 8.0ร—6.5ยฐ | +| DWARF_4 | 5 | 4 | Tele 4.8ร—3.8ยฐ, Wide 130ร—86.5ยฐ | +| DWARF_DRAGON | 6 | 5 | Tele 7.4ร—6.0ยฐ, Wide 7.4ร—6.0ยฐ | + +### Feature matrix + +| Feature | MINI | DWARF_2 | DWARF_3 | DWARF_4 | DRAGON | +|---------|:----:|:-------:|:-------:|:-------:|:------:| +| Advanced Settings | โœ… | โŒ | โŒ | โŒ | โŒ | +| NFC | โŒ | โœ… | โœ… | โœ… | โœ… | +| Auto-shutdown | โŒ | โœ… | โœ… | โœ… | โœ… | +| Panorama create | โŒ | โœ… | โŒ | โœ… | โœ… | +| Normal Track | โœ… | โœ… | โŒ | โŒ | โŒ | +| Wide Normal Track | โŒ | โœ… | โŒ | โŒ | โŒ | + +### Device identification + +The device type is determined from the BLE `DwarfEcho.model` (family/revision) +or from the `GetDeviceState` response (FoV values match specific models). +The `device_id` field in `WsPacket` is NOT the model โ€” it's the index of the +connected telescope (default 1). + +--- + +## 14. Protocol Quirks & Gotchas + +### 1. type=3 not type=1 + +The telescope replies with `type=3` (reply), not `type=1` (response). +A client that only dispatches `type=1` will never receive any replies. + +### 2. Fire-and-forget commands + +Many commands never produce a reply. A client that waits for a reply on these +commands will timeout. The safe approach: maintain a **known patterns table** +per command (Pattern A/B/C, see ยง6). + +### 3. motor reset reboots the device + +`CMD_STEP_MOTOR_RESET` (14003) causes an immediate telescope reboot. It does +not home the motors. The Android app never sends this command. + +### 4. No position feedback without calibration + +The motors have no absolute encoders. After every power-on, position is +unknown (`NEED_RESET`). Only astro calibration provides absolute positioning. +Joystick slew is always relative. + +### 5. RTSP requires camera open + +The RTSP server is dormant until the camera is opened via WebSocket. +Connecting to RTSP without `CMD_CAMERA_OPEN` results in a silent connection +(no frames). + +### 6. Connection drops on firmware events + +The telescope may drop the WebSocket connection during: +- Motor home (limit switch trigger) +- Firmware-internal events +- High command frequency + +The client must handle reconnection gracefully. + +### 7. NEED_RESET is normal + +`CODE_STEP_MOTOR_NEED_RESET` (-14520) is the **normal state** after power-on. +It does not indicate a fault. The Android app operates in relative slew mode +until calibration. Clients should not treat this as an error. + +### 8. Exposure index is non-linear + +The exposure `index` follows a predefined table (`params_range.json`), not a +linear formula. Index 0 = 1/10000s, increments of 3, up to index ~500 for +long exposures. The full table is in the APK assets. + +### 9. Shooting mode must be set before some operations + +Some commands require the telescope to be in a specific shooting mode: +- Astro commands need mode 2 (DSO) or 3 (SUN_MOON) +- Panorama needs mode 7 (PANORAMA) +- Tracking needs mode 6 (AUTO_TRACKING) + +Use `CMD_GLOBAL_TASK_MANAGER_SWITCH_SHOOTING_MODE` (16402) to switch modes. + +### 10. client_id uniqueness + +When running multiple clients, each must use a unique `client_id`. Duplicate +IDs can cause reply mismatches (the telescope does not enforce uniqueness, +but the matching logic assumes one pending request per cmd per client). + +--- + +## 15. Error Codes + +Error codes are negative integers returned in the `code` field of response +messages. The full list is in `WsRespCode.java`. + +### Motor errors + +| Code | Name | Meaning | +|------|------|---------| +| 0 | `WS_OK` | Success | +| -14500 | `IS_RUNNING` | Motor is already running | +| -14501 | `IS_STOPPED` | Motor is already stopped | +| -14502 | `PARALLEL_IN` | Entering parallel zone | +| -14503 | `PARALLEL_END` | Exiting parallel zone | +| -14507 | `INVALID_PARAMETER_ID` | Bad motor ID | +| -14508 | `INVALID_PARAMETER_ANGLE` | Bad angle | +| -14509 | `INVALID_PARAMETER_POSITION` | Bad position | +| -14510 | `OVERTIME_GET_LIMIT_RETURN` | Timeout waiting for limit switch | +| -14518 | `LIMIT_POSITION_WARNING` | Approaching mechanical limit | +| -14519 | `LIMIT_POSITION_HIT` | Hit mechanical limit (auto-stop) | +| -14520 | `NEED_RESET` | Motors not homed (normal after power-on) | +| -14522 | `OVERTIME_TO_RESET` | Timeout during reset | + +### Camera errors + +| Code | Name | Meaning | +|------|------|---------| +| -10001 | `CAMERA_TELE_OPENED` | Tele camera already open | +| -10002 | `CAMERA_TELE_CLOSED` | Tele camera already closed | +| -10003 | `ISP_SET_FAILED` | ISP parameter set failed | +| -10004 | `OPEN_FAILED` | Camera open failed | +| -10010 | `WORKING_BUSY` | Camera busy (another operation in progress) | +| -10016 | `RUNNING_PHOTO` | Cannot do operation while photo in progress | +| -10017 | `RUNNING_RECORD` | Cannot do operation while recording | +| -10018 | `RUNNING_PANORAMA` | Panorama in progress | +| -10019 | `RUNNING_TIMELAPSE` | Timelapse in progress | + +### Astro errors + +| Code | Name | Meaning | +|------|------|---------| +| -11001 | `PLATE_SOLVING_FAILED` | Plate solving failed (not enough stars) | +| -11003 | `FUNCTION_BUSY` | Astro function busy | +| -11005 | `CALIBRATION_FAILED` | Calibration failed | +| -11006 | `GOTO_FAILED` | GoTo failed | +| -11009 | `NEED_CALIBRATION` | Must calibrate before this operation | +| -11017 | `NEED_GOTO` | Must GoTo a target first | +| -11026 | `NEED_EQ` | EQ alignment required | +| -11027 | `STAR_TOO_FEW` | Too few stars detected | +| -11033 | `EQ_SOLVING_FAILED` | EQ solving failed | + +### General errors + +| Code | Name | Meaning | +|------|------|---------| +| 0 | `WS_OK` | Success | +| -1 | `PARSE_PROTOBUF_ERROR` | Protobuf parse error | +| -2 | `SDCARD_NOT_EXIST` | No SD card | +| -3 | `INVALID_PARAM` | Invalid parameter | +| -4 | `SDCARD_WRITE_ERROR` | SD card write failed | +| -5 | `DEVICE_NOT_ACTIVATED` | Device not activated | +| -6 | `SDCARD_FULL_ERROR` | SD card full |