Add deep protocol specification covering all layers

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
This commit is contained in:
Jacquin Antoine
2026-07-13 20:07:59 +02:00
parent c2f9e54eb4
commit 8daa9b4d58

975
analysis/PROTOCOL_SPEC.md Normal file
View File

@ -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://<ip>:9900 │ │
│ │ │ │
│ │ RTSP (TCP) │ │
│ │◄──────────────────────────►│ │
│ │ rtsp://<ip>: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://<ip>:9900/?client_id=<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://<ip>: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://<telescope_ip>:9900/?client_id=<arbitrary_string>
```
- 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 (1000017099)
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:
```
1000010499 Camera Tele
1100011499 Astro
1200012499 Camera Wide
1300013299 System
1350013799 RGB / Power
1400014499 Motor
1480014899 Track
1500015199 Focus
1520015499 Notify (server-push only)
1550015599 Panorama
1570015799 ITips
1610016399 Shooting Schedule
1640016599 Task Center
1670016799 Param
1680016899 Voice Assistant
1700017099 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 **1520015399**
(`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 | 1000010499 | `ch0` |
| **Wide** | 1 | 1200012499 | `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 (1000710050 for Tele, 1200212036
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: 0800+. 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://<ip>:554/ch0/stream0` |
| URL (Wide) | `rtsp://<ip>:554/ch1/stream0` |
| Port | 554 |
| Transport | TCP (`rtsp_transport=tcp` mandatory) |
| Codec | MJPEG (not H.264) |
| Resolution | 1920×1080 |
| FPS | Variable (1530 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://<ip>: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 |