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 |