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
34 KiB
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
- Network Architecture
- Discovery Layer (BLE)
- Control Layer (WebSocket)
- WsPacket Envelope
- Command Routing
- Response Model
- Notification System
- Camera Subsystem
- Motor Subsystem
- Focus Subsystem
- Astro Subsystem
- Preview / RTSP
- Device Models & Feature Gating
- Protocol Quirks & Gotchas
- 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://ifws_scheme == "wss"(rare). client_id: arbitrary string identifying this client. Used in the URL query parameter and in every WsPacket'sclient_idfield.- 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 (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.
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:
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 captureCMD_CAMERA_WIDE_PHOTOGRAPH(12022) — wide photoCMD_FOCUS_AUTO_FOCUS(15000) — autofocusCMD_FOCUS_MANUAL_SINGLE_STEP_FOCUS(15001) — manual focus stepCMD_SYSTEM_SET_TIME(13000) — time syncCMD_SYSTEM_SET_LOCATION(13010) — location setCMD_GLOBAL_TASK_GET_DEVICE_STATE_INFO(16405) — state queryCMD_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 offCMD_RGB_POWER_OPEN_RGB(13500) — RGB onCMD_CAMERA_TELE_OPEN_CAMERA(10000) — open camera (no ack)CMD_CAMERA_TELE_CLOSE_CAMERA(10001) — close cameraCMD_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 slewCMD_STEP_MOTOR_SERVICE_JOYSTICK_FIXED_ANGLE(14007)CMD_STEP_MOTOR_SERVICE_JOYSTICK_STOP(14008)CMD_STEP_MOTOR_RUN(14000) — motor run to positionCMD_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 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.
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.
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:
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:
- Astro calibration (night, starry sky): homes motors + plate solving
- 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.
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
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/stateNOTIFY_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 |