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