Add health check command and panorama control

- New `dwarfctl health` command: checks WebSocket connectivity, device state,
  battery, storage, RGB, focus motor, both cameras, motor position status,
  and CMOS temperature in one pass with clear OK/FAIL output
- Added `dwarfctl pano start|stop` for panorama grid control
- Added StartPanoramaGrid/StopPanorama to the API client
- Fixed CMOS temperature display (int32 not float64)

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
This commit is contained in:
Jacquin Antoine
2026-07-12 15:42:18 +02:00
parent e7b97ad3d1
commit 9d07d13e97

View File

@ -29,6 +29,7 @@ func main() {
rootCmd.PersistentFlags().BoolVarP(&flagDebug, "debug", "d", false, "verbose WebSocket traffic log") rootCmd.PersistentFlags().BoolVarP(&flagDebug, "debug", "d", false, "verbose WebSocket traffic log")
rootCmd.AddCommand( rootCmd.AddCommand(
cmdHealth(),
cmdState(), cmdState(),
cmdCamera(), cmdCamera(),
cmdMotor(), cmdMotor(),
@ -71,6 +72,106 @@ func mustCam(s string) api.Camera {
} }
} }
// --- health ---
func cmdHealth() *cobra.Command {
return &cobra.Command{
Use: "health",
Short: "Full telescope health check (connectivity + device state)",
Run: func(_ *cobra.Command, _ []string) {
checks := 0
passed := 0
check := func(name string, ok bool, detail string) {
checks++
if ok {
passed++
fmt.Printf(" ✅ %-20s %s\n", name, detail)
} else {
fmt.Printf(" ❌ %-20s %s\n", name, detail)
}
}
fmt.Println("DWARF Telescope Health Check")
fmt.Println(strings.Repeat("=", 50))
// 1. Connect
scope := api.New(flagClientID)
scope.SetDebug(flagDebug)
connectStart := time.Now()
err := scope.Connect(flagIP)
elapsed := time.Since(connectStart)
check("WebSocket :9900", err == nil, fmt.Sprintf("%s (%dms)", flagIP+":9900", elapsed.Milliseconds()))
if err != nil {
fmt.Println("\nCannot connect. Aborting.")
os.Exit(1)
}
defer scope.Close()
// 2. Device state
state, err := scope.GetDeviceState()
check("Device state query", err == nil, fmt.Sprintf("%d bytes", len(state.String())))
if err == nil {
di := state.GetDeviceStateInfo()
// 3. Battery
batt := di.GetBatteryInfo().GetPercentage()
check("Battery", batt > 0, fmt.Sprintf("%d%%", batt))
// 4. Storage
stor := di.GetStorageInfo()
avail := stor.GetAvailableSize()
total := stor.GetTotalSize()
storOK := total > 0 && avail > 0 && stor.GetIsValid()
check("Storage", storOK, fmt.Sprintf("%d/%d GB (%s)", avail, total, func() string {
if storOK { return "valid" }
return "INVALID"
}()))
// 5. RGB state
rgb := di.GetRgbState().GetState()
check("RGB ring", true, fmt.Sprintf("state=%d", rgb))
// 6. Focus position
focus := state.GetFocusMotorStateInfo().GetFocusPosition().GetPos()
check("Focus motor", focus > 0, fmt.Sprintf("pos=%d", focus))
// 7. Tele camera
tele := state.GetTeleCameraStateInfo()
teleRes := fmt.Sprintf("%dx%d", tele.GetResolutionWidth(), tele.GetResolutionHeight())
check("Tele camera", tele.GetResolutionWidth() > 0, teleRes)
// 8. Wide camera
wide := state.GetWideCameraStateInfo()
wideRes := fmt.Sprintf("%dx%d", wide.GetResolutionWidth(), wide.GetResolutionHeight())
check("Wide camera", wide.GetResolutionWidth() > 0, wideRes)
// 9. Motor position (may be NEED_RESET)
pos, posErr := scope.MotorGetPosition(0)
if posErr != nil {
check("Motor position", false, posErr.Error())
} else if pos.GetCode() != 0 {
check("Motor position", false, fmt.Sprintf("code=%d (NEED_RESET)", pos.GetCode()))
} else {
check("Motor position", true, fmt.Sprintf("%.2f", pos.GetPosition()))
}
// 10. Temperature
cmosTemp := state.GetWideCameraStateInfo().GetCmosTemperature().GetTemperature()
check("CMOS temp", cmosTemp > -270, fmt.Sprintf("%d°C", cmosTemp))
}
fmt.Println(strings.Repeat("=", 50))
status := "HEALTHY"
if passed < checks {
status = "ISSUES DETECTED"
}
fmt.Printf("Result: %d/%d checks passed — %s\n", passed, checks, status)
},
}
}
// --- state --- // --- state ---
func cmdState() *cobra.Command { func cmdState() *cobra.Command {