Reverse-engineer DWARF II telescope API and build open-source Go client

Complete reverse-engineering of the DWARFLAB Android app (v3.4.0) protocol
and implementation of a working CLI tool to control DWARF II telescopes.

Analysis (from APK decompilation with jadx):
- Extracted 17 protobuf definitions (382 messages) from embedded descriptors
- Mapped all 323 WebSocket command IDs across 16 modules
- Documented the full protocol: BLE discovery, WebSocket control (port 9900),
  RTSP preview, WsPacket envelope (proto v2.3)
- Documented the Android UI structure (screens, navigation, shooting modes)
- Key discovery: telescope responds with type=3 (reply), not type=1 (response),
  and several commands are fire-and-forget (RGB, camera open/close)

dwarfctl Go client:
- Protobuf bindings generated from extracted .proto files (397 messages)
- WebSocket transport layer with request-response matching and notification fan-out
- Typed API covering cameras, motors, astrophotography, focus, tracking, system, power
- Cobra CLI with 30+ subcommands and --debug traffic logging
- 57 unit tests (transport round-trip, command routing, proto encoding)
- Validated on real hardware: state, photo, motor slew (all directions/speeds),
  focus, RGB, time/location sync all confirmed working

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-07-12 15:18:56 +02:00
commit 814a836c5a
54 changed files with 35973 additions and 0 deletions

517
dwarfctl/proto/notify.proto Normal file
View File

@ -0,0 +1,517 @@
syntax = "proto3";
option go_package = "github.com/antitbone/dwarfctl/proto;pb";
import "base.proto";
enum OperationState {
OPERATION_STATE_IDLE = 0;
OPERATION_STATE_RUNNING = 1;
OPERATION_STATE_STOPPING = 2;
OPERATION_STATE_STOPPED = 3;
}
enum AstroState {
ASTRO_STATE_IDLE = 0;
ASTRO_STATE_RUNNING = 1;
ASTRO_STATE_STOPPING = 2;
ASTRO_STATE_STOPPED = 3;
ASTRO_STATE_PLATE_SOLVING = 4;
}
enum SentryModeState {
SENTRY_MODE_STATE_IDLE = 0;
SENTRY_MODE_STATE_INIT = 1;
SENTRY_MODE_STATE_DETECT = 2;
SENTRY_MODE_STATE_TRACK = 3;
SENTRY_MODE_STATE_TRACK_FINISH = 4;
SENTRY_MODE_STATE_STOPPING = 5;
}
enum SentryObjectType {
SENTRY_OBJECT_TYPE_UNKNOWN = 0;
SENTRY_OBJECT_TYPE_UFO = 1;
SENTRY_OBJECT_TYPE_BIRD = 2;
SENTRY_OBJECT_TYPE_PERSON = 3;
SENTRY_OBJECT_TYPE_ANIMAL = 4;
SENTRY_OBJECT_TYPE_VEHICLE = 5;
SENTRY_OBJECT_TYPE_FLYING = 6;
SENTRY_OBJECT_TYPE_BOAT = 7;
}
message PictureMatching {
uint32 x = 1;
uint32 y = 2;
uint32 width = 3;
uint32 height = 4;
}
message StorageInfo {
uint32 available_size = 1;
uint32 total_size = 2;
int32 storage_type = 3;
bool is_valid = 4;
}
message Temperature {
int32 code = 1;
int32 temperature = 2;
}
message CmosTemperature {
// oneof _temperature
int32 temperature = 1;
int32 camera_type = 2;
}
message RecordTime {
uint32 record_time = 1;
uint32 camera_type = 2;
}
message TimeLapseOutTime {
uint32 interval = 1;
uint32 out_time = 2;
uint32 total_time = 3;
uint32 camera_type = 4;
}
message OperationStateNotify {
OperationState state = 1;
}
message AstroCalibrationState {
AstroState state = 1;
int32 plate_solving_times = 2;
}
message AstroGotoState {
AstroState state = 1;
string target_name = 2;
}
message AstroTrackingState {
OperationState state = 1;
string target_name = 2;
}
message ProgressCaptureRawDark {
int32 progress = 1;
int32 remaining_time = 2;
int32 camera_type = 3;
}
message ProgressCaptureRawLiveStacking {
// oneof _shooting_time
// oneof _stacked_time
int32 total_count = 1;
int32 update_type = 2;
int32 current_count = 3;
int32 stacked_count = 4;
int32 exp_index = 5;
int32 gain_index = 6;
string target_name = 7;
int32 camera_type = 8;
int32 shooting_time = 9;
int32 stacked_time = 10;
}
message Param {
repeated CommonParam param = 1;
}
message BurstProgress {
uint32 total_count = 1;
uint32 completed_count = 2;
uint32 camera_type = 3;
}
message PanoramaProgress {
int32 total_count = 1;
int32 completed_count = 2;
uint32 camera_type = 3;
}
message RgbState {
int32 state = 1;
}
message PowerIndState {
int32 state = 1;
}
message ChargingState {
int32 state = 1;
}
message BatteryInfo {
int32 percentage = 1;
}
message HostSlaveMode {
int32 mode = 1;
bool lock = 2;
}
message MTPState {
int32 mode = 1;
}
message TrackResult {
int32 x = 1;
int32 y = 2;
int32 w = 3;
int32 h = 4;
int32 id = 5;
}
message CPUMode {
int32 mode = 1;
}
message AstroTrackingSpecialState {
OperationState state = 1;
string target_name = 2;
int32 index = 3;
}
message PowerOff {
}
message AlbumUpdate {
int32 media_type = 1;
}
message SentryState {
SentryModeState state = 1;
SentryObjectType object_type = 2;
}
message OneClickGotoState {
// oneof current_state
AstroAutoFocusState astro_auto_focus_state = 1;
AstroCalibrationState astro_calibration_state = 2;
AstroGotoState astro_goto_state = 3;
AstroTrackingState astro_tracking_state = 4;
}
message StreamType {
int32 stream_type = 1;
int32 cam_id = 2;
}
message MultiTrackResult {
repeated TrackResult results = 1;
}
message EqSolvingState {
OperationState state = 1;
}
message LongExpPhotoProgress {
double total_time = 1;
double exposured_time = 2;
uint32 camera_type = 3;
}
message ShootingScheduleResultAndState {
string schedule_id = 1;
int32 result = 2;
int32 state = 3;
}
message ShootingTaskState {
string schedule_task_id = 1;
int32 state = 2;
int32 code = 3;
}
message SkySeacherState {
OperationState state = 1;
}
message ProgressAiEnhance {
int32 progress = 1;
int32 total_time = 2;
}
message CommonProgress {
enum ProgressType {
PROGRESS_TYPE_INITING = 0;
PROGRESS_TYPE_MOSAIC_MOVING = 1;
}
int32 current = 1;
int32 total = 2;
CommonProgress.ProgressType progress_type = 3;
}
message CalibrationResult {
double azi = 1;
double alt = 2;
}
message FocusPosition {
int32 pos = 1;
}
message SentryAutoHand {
int32 mode = 1;
}
message PanoramaStitchUploadComplete {
int32 code = 1;
string user_id = 2;
string busi_no = 3;
string panorama_name = 4;
string mac = 5;
}
message PanoramaCompressionComplete {
int32 code = 1;
string panorama_name = 2;
string zip_file_path = 3;
string zip_file_md5 = 4;
uint64 zip_file_size = 5;
string stitch_param = 6;
}
message PanoramaCompressionProgress {
string panorama_name = 1;
uint32 total_files_num = 2;
uint32 compressed_files_num = 3;
}
message PanoramaUploadCompressionProgress {
string user_id = 1;
string busi_no = 2;
string panorama_name = 3;
string mac = 4;
uint32 total_files_num = 5;
uint32 compressed_files_num = 6;
double speed = 7;
}
message PanoramaUploadProgress {
string user_id = 1;
string busi_no = 2;
string panorama_name = 3;
string mac = 4;
uint64 total_size = 5;
uint64 uploaded_size = 6;
double speed = 7;
}
message PanoramaCurrentUploadState {
int32 code = 1;
string user_id = 2;
string busi_no = 3;
string panorama_name = 4;
string mac = 5;
uint32 total_files_num = 6;
uint32 compressed_files_num = 7;
uint64 total_size = 8;
uint64 uploaded_size = 9;
uint32 step = 10;
}
message LowTempProtectionMode {
int32 mode = 1;
}
message StateSystemResourceOccupation {
enum TaskId {
IDLE = 0;
PANORAMA_UPLOAD = 1;
ASTRO_MULTI_STACK = 2;
}
StateSystemResourceOccupation.TaskId task_id = 1;
OperationState state = 2;
}
message BodyStatus {
enum BodyStatusEnum {
UNKNOWN = 0;
EQ_MODE = 1;
AZI_MODE = 2;
}
BodyStatus.BodyStatusEnum body_status = 1;
}
message ProgressCaptureMosaic {
int32 total_count = 1;
int32 update_type = 2;
int32 current_count = 3;
int32 stacked_count = 4;
int32 exp_index = 5;
int32 gain_index = 6;
string target_name = 7;
int32 horizontal_scale = 8;
int32 vertical_scale = 9;
int32 rotation = 10;
int32 fov_id = 11;
int32 fov_total = 12;
}
message Wb {
uint64 mode = 1;
int32 ct = 2;
int32 scene = 3;
int32 camera_type = 4;
}
message GeneralIntParam {
uint64 param_id = 1;
int32 mode = 2;
int32 value = 3;
}
message GeneralFloatParam {
uint64 param_id = 1;
float value = 2;
}
message GeneralBoolParams {
uint64 param_id = 1;
bool value = 2;
}
message SwitchShootingMode {
int32 state = 1;
int32 source_mode = 2;
int32 dst_mode = 3;
}
message SwitchCropRatioState {
int32 state = 1;
int32 crop_ratio = 2;
}
message ResolutionParam {
uint64 param_id = 1;
int32 current_res_value = 2;
int32 current_fps_value = 3;
repeated int32 supported_fps_list = 4;
repeated int32 supported_resolution_list = 5;
}
message CaptureRawState {
OperationState state = 1;
int32 camera_type = 2;
}
message PhotoState {
OperationState state = 1;
int32 camera_type = 2;
}
message BurstState {
OperationState state = 1;
int32 camera_type = 2;
}
message RecordState {
OperationState state = 1;
int32 camera_type = 2;
}
message TimeLapseState {
OperationState state = 1;
int32 camera_type = 2;
}
message CaptureRawDarkState {
OperationState state = 1;
int32 camera_type = 2;
}
message PanoramaState {
OperationState state = 1;
int32 camera_type = 2;
}
message AstroAutoFocusState {
OperationState state = 1;
}
message NormalAutoFocusState {
OperationState state = 1;
}
message AstroAutoFocusFastState {
OperationState state = 1;
}
message AreaAutoFocusState {
OperationState state = 1;
}
message DualCameraLinkageState {
OperationState state = 1;
}
message NormalTrackState {
OperationState state = 1;
int32 camera_type = 2;
}
message SwitchResolutionFpsState {
OperationState state = 1;
int32 camera_type = 2;
}
message CaptureCaliFrameState {
OperationState state = 1;
int32 camera_type = 2;
int32 cali_frame_type = 3;
}
message CaptureCaliFrameProgress {
int32 progress = 1;
int32 camera_type = 2;
int32 cali_frame_type = 3;
}
message DeviceAttitude {
double pitch = 1;
double yaw = 2;
double roll = 3;
}
message SkyTargetFinderState {
OperationState state = 1;
int32 scene_type = 2;
}
message PanoFramingThumbnailUpdateNotify {
bytes webp_data = 1;
}
message PanoFramingRectUpdateNotify {
double norm_x_tl = 1;
double norm_y_tl = 2;
double norm_x_br = 3;
double norm_y_br = 4;
double norm_limit_x_left = 5;
double norm_limit_y_top = 6;
double norm_limit_x_right = 7;
double norm_limit_y_bottom = 8;
double rect_hor_fov = 9;
double rect_ver_fov = 10;
int32 error_code = 11;
}
message PanoFramingStateNotify {
int32 state = 1;
}
message AutoShutdown {
int32 state = 1;
}
message LensDefog {
int32 state = 1;
}
message AutoCooling {
int32 state = 1;
}