Files
dwarf-go/analysis/PROTOCOL_SPEC.md
Jacquin Antoine 8daa9b4d58 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
2026-07-13 20:07:59 +02:00

34 KiB
Raw Blame History

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
  2. Discovery Layer (BLE)
  3. Control Layer (WebSocket)
  4. WsPacket Envelope
  5. Command Routing
  6. Response Model
  7. Notification System
  8. Camera Subsystem
  9. Motor Subsystem
  10. Focus Subsystem
  11. Astro Subsystem
  12. Preview / RTSP
  13. Device Models & Feature Gating
  14. Protocol Quirks & Gotchas
  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:

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:

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.

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)

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:

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).


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.

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.

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

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

message FocusPosition { int32 pos = 1; }

Position is visible in GetDeviceStatefocus_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

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