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

View File

@ -0,0 +1,243 @@
package api
// Module IDs (WsModuleId enum ordinals, derived from WsCmd.getModuleId ranges).
const (
ModuleNone = 0
ModuleCameraTele = 1
ModuleCameraWide = 2
ModuleAstro = 3
ModuleSystem = 4
ModuleRGBPower = 5
ModuleMotor = 6
ModuleTrack = 7
ModuleFocus = 8
ModuleNotify = 9
ModulePanorama = 10
ModuleITips = 11
ModuleShootingSchedule = 13
ModuleTaskCenter = 14
ModuleParam = 15
ModuleVoiceAssistant = 16
ModuleCameraGuide = 17
ModuleDevice = 18
)
// ModuleIDForCmd derives the module_id from a command ID using the same
// range logic as WsCmd.getModuleId() in the original APK.
func ModuleIDForCmd(cmd uint32) uint32 {
switch {
case cmd >= 10000 && cmd < 10500:
return ModuleCameraTele
case cmd >= 11000 && cmd < 11500:
return ModuleAstro
case cmd >= 12000 && cmd < 12500:
return ModuleCameraWide
case cmd >= 13000 && cmd < 13300:
return ModuleSystem
case cmd >= 13500 && cmd < 13800:
return ModuleRGBPower
case cmd >= 14000 && cmd < 14500:
return ModuleMotor
case cmd >= 14800 && cmd < 14900:
return ModuleTrack
case cmd >= 15000 && cmd < 15200:
return ModuleFocus
case cmd >= 15200 && cmd < 15500:
return ModuleNotify
case cmd >= 15500 && cmd < 15600:
return ModulePanorama
case cmd >= 15700 && cmd < 15800:
return ModuleITips
case cmd >= 16100 && cmd < 16400:
return ModuleShootingSchedule
case cmd >= 16400 && cmd < 16600:
return ModuleTaskCenter
case cmd >= 16700 && cmd < 16800:
return ModuleParam
case cmd >= 16800 && cmd < 16900:
return ModuleVoiceAssistant
case cmd >= 17000 && cmd < 17100:
return ModuleDevice
default:
return ModuleNone
}
}
// Command IDs — Camera Tele (MODULE_CAMERA_TELE, 1000010499).
const (
CmdTeleOpenCamera = 10000
CmdTeleCloseCamera = 10001
CmdTelePhotograph = 10002
CmdTeleBurst = 10003
CmdTeleStopBurst = 10004
CmdTeleStartRecord = 10005
CmdTeleStopRecord = 10006
CmdTeleSetExpMode = 10007
CmdTeleGetExpMode = 10008
CmdTeleSetExp = 10009
CmdTeleGetExp = 10010
CmdTeleSetGainMode = 10011
CmdTeleGetGainMode = 10012
CmdTeleSetGain = 10013
CmdTeleGetGain = 10014
CmdTeleSetBrightness = 10015
CmdTeleGetBrightness = 10016
CmdTeleSetContrast = 10017
CmdTeleGetContrast = 10018
CmdTeleSetSaturation = 10019
CmdTeleGetSaturation = 10020
CmdTeleSetHue = 10021
CmdTeleGetHue = 10022
CmdTeleSetSharpness = 10023
CmdTeleGetSharpness = 10024
CmdTeleSetWBMode = 10025
CmdTeleGetWBMode = 10026
CmdTeleSetWBScene = 10027
CmdTeleGetWBScene = 10028
CmdTeleSetWBCT = 10029
CmdTeleGetWBCT = 10030
CmdTeleSetIRCut = 10031
CmdTeleGetIRCut = 10032
CmdTeleStartTimelapse = 10033
CmdTeleStopTimelapse = 10034
CmdTeleSetAllParams = 10035
CmdTeleGetAllParams = 10036
CmdTeleSetFeatureParam = 10037
CmdTeleGetAllFeatureParams = 10038
CmdTeleGetWorkingState = 10039
CmdTeleSetJpgQuality = 10040
CmdTelePhotoRaw = 10041
CmdTeleSetRtspBitrate = 10042
CmdTeleSwitchResolution = 10047
CmdTeleSwitchFramerate = 10048
CmdTeleSwitchCropRatio = 10049
CmdTeleSetPreviewQuality = 10050
)
// Camera Wide (MODULE_CAMERA_WIDE, 1200012499).
const (
CmdWideOpenCamera = 12000
CmdWideCloseCamera = 12001
CmdWideSetExpMode = 12002
CmdWideGetExpMode = 12003
CmdWideSetExp = 12004
CmdWideGetExp = 12005
CmdWideSetGain = 12006
CmdWideGetGain = 12007
CmdWidePhotograph = 12022
CmdWideBurst = 12023
CmdWideStopBurst = 12024
CmdWidePhotoRaw = 12029
CmdWideStartRecord = 12030
CmdWideStopRecord = 12031
CmdWideGetAllParams = 12027
CmdWideSetAllParams = 12028
)
// Astro (MODULE_ASTRO, 1100011499).
const (
CmdAstroStartCalibration = 11000
CmdAstroStopCalibration = 11001
CmdAstroStartGotoDSO = 11002
CmdAstroStartGotoSolar = 11003
CmdAstroStopGoto = 11004
CmdAstroStartLiveStacking = 11005
CmdAstroStopLiveStacking = 11006
CmdAstroStartRawDark = 11007
CmdAstroStopRawDark = 11008
CmdAstroGoLive = 11010
CmdAstroStartOneClickGoto = 11013
CmdAstroStopOneClickGoto = 11015
CmdAstroStartEqSolving = 11018
CmdAstroStopEqSolving = 11019
CmdAstroStartAiEnhance = 11029
CmdAstroStopAiEnhance = 11030
CmdAstroStartMosaic = 11031
CmdAstroStartSkyFinder = 11047
CmdAstroStopSkyFinder = 11048
)
// System (MODULE_SYSTEM, 1300013299).
const (
CmdSystemSetTime = 13000
CmdSystemSetTimeZone = 13001
CmdSystemSetMtpMode = 13002
CmdSystemSetCpuMode = 13003
CmdSystemSetMasterLock = 13004
CmdSystemSetLocation = 13010
)
// RGB Power (MODULE_RGB_POWER, 1350013799).
const (
CmdRGBOn = 13500
CmdRGBOff = 13501
CmdPowerDown = 13502
CmdReboot = 13505
)
// Motor constants — extended (14001-14005 are inferred from gaps)
const (
CmdMotorRun = 14000
CmdMotorGetPosition = 14001 // inferred: missing ID between RUN and STOP
CmdMotorStop = 14002
CmdMotorReset = 14003 // inferred: missing ID after STOP
CmdMotorChangeSpeed = 14004 // inferred
CmdMotorChangeDirection = 14005 // inferred
CmdMotorJoystick = 14006
CmdMotorJoystickFixedAngle = 14007
CmdMotorJoystickStop = 14008
CmdMotorDualCameraLinkage = 14009
)
// Track (MODULE_TRACK, 1480014899).
const (
CmdTrackStart = 14800
CmdTrackStop = 14801
CmdSentryStart = 14802
CmdSentryStop = 14803
CmdSwitchMainPreview = 14809
)
// Focus (MODULE_FOCUS, 1500015199).
const (
CmdFocusAutoFocus = 15000
CmdFocusManualSingle = 15001
CmdFocusStartManualCont = 15002
CmdFocusStopManualCont = 15003
CmdFocusStartAstroAF = 15004
CmdFocusStopAstroAF = 15005
CmdFocusGetUserInfinity = 15011
CmdFocusSetUserInfinity = 15012
)
// Panorama (MODULE_PANORAMA, 1550015599).
const (
CmdPanoStartGrid = 15500
CmdPanoStop = 15501
CmdPanoStartStitch = 15503
CmdPanoStopStitch = 15504
CmdPanoStartFraming = 15509
CmdPanoStopFraming = 15510
CmdPanoResetFraming = 15511
CmdPanoUpdateFramingRect = 15512
)
// Task Center (MODULE_TASK_CENTER, 1640016599).
const (
CmdTaskStartTask = 16400
CmdTaskStopTask = 16401
CmdTaskSwitchMode = 16402
CmdTaskSwitchTech = 16403
CmdTaskGetDeviceState = 16405
)
// Notify (MODULE_NOTIFY, 1520015399) — server-pushed, not requestable.
const (
NotifyTrackResult = 15225
NotifyStateCaptureRawDark = 15206
NotifyCalibrationResult = 15256
NotifyStateAstroGoto = 15211
NotifyTemperature = 15243
NotifySDCardInfo = 15203
NotifyPowerOff = 15229
)