# 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 (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://<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 (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://<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 |
