- 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
726 lines
19 KiB
Go
726 lines
19 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"math"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/antitbone/dwarfctl/internal/api"
|
|
"github.com/spf13/cobra"
|
|
)
|
|
|
|
var (
|
|
flagIP string
|
|
flagClientID string
|
|
flagDebug bool
|
|
)
|
|
|
|
func main() {
|
|
rootCmd := &cobra.Command{
|
|
Use: "dwarfctl",
|
|
Short: "DWARF telescope control CLI",
|
|
Long: "Open-source client for DWARF II / DWARFLAB smart telescopes.\nControls cameras, motors, astrophotography, focus, tracking, and power\nvia the telescope's WebSocket API (port 9900).",
|
|
}
|
|
rootCmd.PersistentFlags().StringVarP(&flagIP, "ip", "i", "", "telescope IP address (required)")
|
|
rootCmd.PersistentFlags().StringVar(&flagClientID, "client-id", "dwarfctl", "WebSocket client_id")
|
|
rootCmd.PersistentFlags().BoolVarP(&flagDebug, "debug", "d", false, "verbose WebSocket traffic log")
|
|
|
|
rootCmd.AddCommand(
|
|
cmdHealth(),
|
|
cmdState(),
|
|
cmdCamera(),
|
|
cmdMotor(),
|
|
cmdAstro(),
|
|
cmdFocus(),
|
|
cmdTrack(),
|
|
cmdSystem(),
|
|
cmdPower(),
|
|
cmdMonitor(),
|
|
cmdPano(),
|
|
)
|
|
|
|
rootCmd.MarkPersistentFlagRequired("ip")
|
|
if err := rootCmd.Execute(); err != nil {
|
|
os.Exit(1)
|
|
}
|
|
}
|
|
|
|
// dial connects to the telescope and returns a ready Telescope + cleanup.
|
|
func dial() (*api.Telescope, func()) {
|
|
scope := api.New(flagClientID)
|
|
scope.SetDebug(flagDebug)
|
|
if err := scope.Connect(flagIP); err != nil {
|
|
fmt.Fprintf(os.Stderr, "Error: connect to %s: %v\n", flagIP, err)
|
|
os.Exit(1)
|
|
}
|
|
return scope, func() { scope.Close() }
|
|
}
|
|
|
|
func mustCam(s string) api.Camera {
|
|
switch strings.ToLower(s) {
|
|
case "tele", "t", "0":
|
|
return api.CameraTele
|
|
case "wide", "w", "1":
|
|
return api.CameraWide
|
|
default:
|
|
fmt.Fprintf(os.Stderr, "Error: invalid camera '%s' (use: tele|wide)\n", s)
|
|
os.Exit(1)
|
|
return api.CameraTele
|
|
}
|
|
}
|
|
|
|
// --- 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 ---
|
|
|
|
func cmdState() *cobra.Command {
|
|
return &cobra.Command{
|
|
Use: "state",
|
|
Short: "Get device state info",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
state, err := scope.GetDeviceState()
|
|
if err != nil {
|
|
die("get state: %v", err)
|
|
}
|
|
fmt.Printf("Device State:\n")
|
|
printProto(state)
|
|
},
|
|
}
|
|
}
|
|
|
|
// --- camera ---
|
|
|
|
func cmdCamera() *cobra.Command {
|
|
c := &cobra.Command{
|
|
Use: "camera",
|
|
Short: "Camera control (open/close/photo/params)",
|
|
}
|
|
c.AddCommand(
|
|
&cobra.Command{
|
|
Use: "open [--cam tele|wide]",
|
|
Short: "Open camera",
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
cam := mustCam(flagCam(cmd))
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.OpenCamera(cam))
|
|
fmt.Println("camera opened")
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "close [--cam tele|wide]",
|
|
Short: "Close camera",
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
cam := mustCam(flagCam(cmd))
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.CloseCamera(cam))
|
|
fmt.Println("camera closed")
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "photo [--cam tele|wide] [--raw]",
|
|
Short: "Take a photo",
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
cam := mustCam(flagCam(cmd))
|
|
raw, _ := cmd.Flags().GetBool("raw")
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
if raw {
|
|
must(scope.TakeRawPhoto(cam))
|
|
} else {
|
|
must(scope.TakePhoto(cam))
|
|
}
|
|
fmt.Println("photo captured")
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "burst [--cam tele|wide] [start|stop]",
|
|
Short: "Start or stop burst capture",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cam := mustCam(flagCam(cmd))
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
switch args[0] {
|
|
case "start":
|
|
must(scope.StartBurst(cam))
|
|
case "stop":
|
|
must(scope.StopBurst(cam))
|
|
default:
|
|
die("usage: camera burst [start|stop]")
|
|
}
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "record [--cam tele|wide] [start|stop]",
|
|
Short: "Start or stop video recording",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cam := mustCam(flagCam(cmd))
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
switch args[0] {
|
|
case "start":
|
|
must(scope.StartRecord(cam))
|
|
case "stop":
|
|
must(scope.StopRecord(cam))
|
|
default:
|
|
die("usage: camera record [start|stop]")
|
|
}
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "exp [--cam tele|wide] <index>",
|
|
Short: "Set exposure index (0=1/10000s, see params_range.json)",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cam := mustCam(flagCam(cmd))
|
|
idx := parseInt32(args[0])
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.SetExposure(cam, idx))
|
|
fmt.Printf("exposure set to %d\n", idx)
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "gain [--cam tele|wide] <index>",
|
|
Short: "Set gain index (0-200+)",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
cam := mustCam(flagCam(cmd))
|
|
idx := parseInt32(args[0])
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.SetGain(cam, idx))
|
|
fmt.Printf("gain set to %d\n", idx)
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "params [--cam tele|wide]",
|
|
Short: "Get all camera parameters",
|
|
Run: func(cmd *cobra.Command, _ []string) {
|
|
cam := mustCam(flagCam(cmd))
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
params, err := scope.GetAllParams(cam)
|
|
must(err)
|
|
printProto(params)
|
|
},
|
|
},
|
|
)
|
|
// Add --cam to all subcommands
|
|
for _, sub := range c.Commands() {
|
|
sub.Flags().String("cam", "tele", "camera: tele|wide")
|
|
}
|
|
return c
|
|
}
|
|
|
|
// --- motor ---
|
|
|
|
func cmdMotor() *cobra.Command {
|
|
c := &cobra.Command{Use: "motor", Short: "Motor control (slew/joystick)"}
|
|
c.AddCommand(
|
|
&cobra.Command{
|
|
Use: "slew <angle-deg> <length>",
|
|
Short: "Slew via joystick vector (angle 0-360, length 0-1)",
|
|
Args: cobra.ExactArgs(2),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
angle := parseFloat(args[0])
|
|
length := parseFloat(args[1])
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.SlewJoystick(angle, length))
|
|
fmt.Printf("slew: angle=%.1f length=%.2f\n", angle, length)
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "stop [motor-id]",
|
|
Short: "Stop motors (id 0=RA, 1=DEC, default=both)",
|
|
Run: func(cmd *cobra.Command, args []string) {
|
|
id := int32(0)
|
|
if len(args) > 0 {
|
|
id = parseInt32(args[0])
|
|
}
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.MotorStop(id))
|
|
fmt.Println("motor stopped")
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "goto <motor-id> <position> <speed>",
|
|
Short: "Move motor to position at speed",
|
|
Args: cobra.ExactArgs(3),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
id := parseInt32(args[0])
|
|
pos := parseFloat(args[1])
|
|
speed := parseFloat(args[2])
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.MotorRunTo(id, pos, speed))
|
|
fmt.Printf("motor %d -> pos %.2f @ speed %.2f\n", id, pos, speed)
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "reset <motor-id> [direction]",
|
|
Short: "Reset motor to home (DISABLED — causes reboot)",
|
|
Args: cobra.RangeArgs(1, 2),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
id := parseInt32(args[0])
|
|
dir := false
|
|
if len(args) > 1 && args[1] == "1" {
|
|
dir = true
|
|
}
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.MotorReset(id, dir))
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "position <motor-id>",
|
|
Short: "Get motor position (experimental cmd 14001)",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
id := parseInt32(args[0])
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
pos, err := scope.MotorGetPosition(id)
|
|
must(err)
|
|
fmt.Printf("motor %d: id=%d code=%d position=%.2f\n", id, pos.GetId(), pos.GetCode(), pos.GetPosition())
|
|
},
|
|
},
|
|
)
|
|
return c
|
|
}
|
|
|
|
// --- astro ---
|
|
|
|
func cmdAstro() *cobra.Command {
|
|
c := &cobra.Command{Use: "astro", Short: "Astrophotography (calibrate/goto/stack)"}
|
|
c.AddCommand(
|
|
&cobra.Command{
|
|
Use: "calibrate [start|stop]",
|
|
Short: "Start or stop star calibration",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
switch args[0] {
|
|
case "start":
|
|
must(scope.StartCalibration())
|
|
fmt.Println("calibration started")
|
|
case "stop":
|
|
must(scope.StopCalibration())
|
|
fmt.Println("calibration stopped")
|
|
default:
|
|
die("usage: astro calibrate [start|stop]")
|
|
}
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "goto-dso <ra-deg> <dec-deg> [name]",
|
|
Short: "GoTo a deep-sky object by RA/Dec (degrees)",
|
|
Args: cobra.MinimumNArgs(2),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
ra := parseFloat(args[0])
|
|
dec := parseFloat(args[1])
|
|
name := ""
|
|
if len(args) > 2 {
|
|
name = strings.Join(args[2:], " ")
|
|
}
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.GotoDSO(ra, dec, name))
|
|
fmt.Printf("GoTo DSO: RA=%.4f° Dec=%.4f° (%s)\n", ra, dec, name)
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "goto-solar <index>",
|
|
Short: "GoTo solar system object (1=Mercury..7=Neptune, see planet table)",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
idx := parseInt32(args[0])
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.GotoSolar(idx))
|
|
fmt.Printf("GoTo solar system object %d\n", idx)
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "goto-stop",
|
|
Short: "Abort GoTo",
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.StopGoto())
|
|
fmt.Println("GoTo stopped")
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "stack [start|stop]",
|
|
Short: "Start or stop live stacking",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
switch args[0] {
|
|
case "start":
|
|
must(scope.StartLiveStacking())
|
|
case "stop":
|
|
must(scope.StopLiveStacking())
|
|
default:
|
|
die("usage: astro stack [start|stop]")
|
|
}
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "eq-solve [start|stop]",
|
|
Short: "Start or stop EQ solving (polar alignment)",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
switch args[0] {
|
|
case "start":
|
|
must(scope.StartEqSolving())
|
|
case "stop":
|
|
must(scope.StopEqSolving())
|
|
default:
|
|
die("usage: astro eq-solve [start|stop]")
|
|
}
|
|
},
|
|
},
|
|
)
|
|
return c
|
|
}
|
|
|
|
// --- focus ---
|
|
|
|
func cmdFocus() *cobra.Command {
|
|
c := &cobra.Command{Use: "focus", Short: "Focus control"}
|
|
c.AddCommand(
|
|
&cobra.Command{
|
|
Use: "auto",
|
|
Short: "Trigger autofocus",
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.AutoFocus())
|
|
fmt.Println("autofocus triggered")
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "step <0|1>",
|
|
Short: "Manual single-step focus (0=in, 1=out)",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
dir := uint32(parseInt32(args[0]))
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.ManualSingleStepFocus(dir))
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "astro-af [start|stop]",
|
|
Short: "Start or stop astro autofocus",
|
|
Args: cobra.ExactArgs(1),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
switch args[0] {
|
|
case "start":
|
|
must(scope.StartAstroAutoFocus())
|
|
case "stop":
|
|
must(scope.StopAstroAutoFocus())
|
|
}
|
|
},
|
|
},
|
|
)
|
|
return c
|
|
}
|
|
|
|
// --- track ---
|
|
|
|
func cmdTrack() *cobra.Command {
|
|
c := &cobra.Command{Use: "track", Short: "Tracking control"}
|
|
c.AddCommand(
|
|
&cobra.Command{
|
|
Use: "start <x> <y> <w> <h> [cam-id]",
|
|
Short: "Start tracking an object in a region",
|
|
Args: cobra.MinimumNArgs(4),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
x, y, w, h := parseInt32(args[0]), parseInt32(args[1]), parseInt32(args[2]), parseInt32(args[3])
|
|
cam := int32(0)
|
|
if len(args) > 4 {
|
|
cam = parseInt32(args[4])
|
|
}
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.StartTracking(x, y, w, h, cam))
|
|
fmt.Printf("tracking started: bbox(%d,%d,%d,%d) cam=%d\n", x, y, w, h, cam)
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "stop",
|
|
Short: "Stop tracking",
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.StopTracking())
|
|
fmt.Println("tracking stopped")
|
|
},
|
|
},
|
|
)
|
|
return c
|
|
}
|
|
|
|
// --- system ---
|
|
|
|
func cmdSystem() *cobra.Command {
|
|
c := &cobra.Command{Use: "system", Short: "System commands (time/location)"}
|
|
c.AddCommand(
|
|
&cobra.Command{
|
|
Use: "sync-time",
|
|
Short: "Sync telescope clock to this machine",
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
_, tz := time.Now().Zone()
|
|
must(scope.SetTime(uint64(time.Now().Unix()), float64(tz)))
|
|
fmt.Println("time synced")
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "set-location <lat> <lon> [alt]",
|
|
Short: "Set observing location",
|
|
Args: cobra.MinimumNArgs(2),
|
|
Run: func(_ *cobra.Command, args []string) {
|
|
lat := parseFloat(args[0])
|
|
lon := parseFloat(args[1])
|
|
alt := 0.0
|
|
if len(args) > 2 {
|
|
alt = parseFloat(args[2])
|
|
}
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.SetLocation(lat, lon, alt))
|
|
fmt.Printf("location set: %.4f, %.4f, %.0fm\n", lat, lon, alt)
|
|
},
|
|
},
|
|
)
|
|
return c
|
|
}
|
|
|
|
// --- power ---
|
|
|
|
func cmdPower() *cobra.Command {
|
|
c := &cobra.Command{Use: "power", Short: "Power & RGB control"}
|
|
c.AddCommand(
|
|
&cobra.Command{Use: "rgb-on", Short: "Turn on RGB ring light", Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial(); defer cleanup(); must(scope.RGBOn()); fmt.Println("RGB on")
|
|
}},
|
|
&cobra.Command{Use: "rgb-off", Short: "Turn off RGB ring light", Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial(); defer cleanup(); must(scope.RGBOff()); fmt.Println("RGB off")
|
|
}},
|
|
&cobra.Command{Use: "off", Short: "Power down telescope", Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial(); defer cleanup(); must(scope.PowerDown()); fmt.Println("powering down")
|
|
}},
|
|
&cobra.Command{Use: "reboot", Short: "Reboot telescope", Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial(); defer cleanup(); must(scope.Reboot()); fmt.Println("rebooting")
|
|
}},
|
|
)
|
|
return c
|
|
}
|
|
|
|
// --- panorama ---
|
|
|
|
func cmdPano() *cobra.Command {
|
|
c := &cobra.Command{Use: "pano", Short: "Panorama control"}
|
|
c.AddCommand(
|
|
&cobra.Command{
|
|
Use: "start",
|
|
Short: "Start panorama grid scan (homes motors first)",
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.StartPanoramaGrid())
|
|
fmt.Println("panorama grid started (motors homing)")
|
|
},
|
|
},
|
|
&cobra.Command{
|
|
Use: "stop",
|
|
Short: "Stop panorama",
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
must(scope.StopPanorama())
|
|
fmt.Println("panorama stopped")
|
|
},
|
|
},
|
|
)
|
|
return c
|
|
}
|
|
|
|
// --- monitor ---
|
|
|
|
func cmdMonitor() *cobra.Command { return &cobra.Command{
|
|
Use: "monitor",
|
|
Short: "Listen for telescope notifications (Ctrl-C to stop)",
|
|
Run: func(_ *cobra.Command, _ []string) {
|
|
scope, cleanup := dial()
|
|
defer cleanup()
|
|
ch := scope.Notifications()
|
|
fmt.Println("Monitoring notifications (Ctrl-C to stop)...")
|
|
for pkt := range ch {
|
|
fmt.Printf("[NOTIFY] cmd=%d module=%d type=%d data_len=%d\n",
|
|
pkt.GetCmd(), pkt.GetModuleId(), pkt.GetType(), len(pkt.GetData()))
|
|
}
|
|
},
|
|
}
|
|
}
|
|
|
|
// --- helpers ---
|
|
|
|
func flagCam(cmd *cobra.Command) string {
|
|
s, _ := cmd.Flags().GetString("cam")
|
|
return s
|
|
}
|
|
|
|
func parseInt32(s string) int32 {
|
|
v, err := strconv.ParseInt(s, 0, 32)
|
|
if err != nil {
|
|
die("invalid number '%s': %v", s, err)
|
|
}
|
|
return int32(v)
|
|
}
|
|
|
|
func parseFloat(s string) float64 {
|
|
v, err := strconv.ParseFloat(s, 64)
|
|
if err != nil {
|
|
die("invalid float '%s': %v", s, err)
|
|
}
|
|
if math.IsNaN(v) || math.IsInf(v, 0) {
|
|
die("invalid float '%s'", s)
|
|
}
|
|
return v
|
|
}
|
|
|
|
func die(format string, args ...any) {
|
|
fmt.Fprintf(os.Stderr, "Error: "+format+"\n", args...)
|
|
os.Exit(1)
|
|
}
|
|
|
|
func must(err error) {
|
|
if err != nil {
|
|
die("%v", err)
|
|
}
|
|
}
|
|
|
|
func printProto(v any) {
|
|
fmt.Printf(" %+v\n", v)
|
|
}
|