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:
596
dwarfctl/cmd/dwarfctl/main.go
Normal file
596
dwarfctl/cmd/dwarfctl/main.go
Normal file
@ -0,0 +1,596 @@
|
||||
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(
|
||||
cmdState(),
|
||||
cmdCamera(),
|
||||
cmdMotor(),
|
||||
cmdAstro(),
|
||||
cmdFocus(),
|
||||
cmdTrack(),
|
||||
cmdSystem(),
|
||||
cmdPower(),
|
||||
cmdMonitor(),
|
||||
)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// --- 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 (0=back, 1=forward)",
|
||||
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))
|
||||
fmt.Printf("motor %d reset (dir=%v)\n", 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
|
||||
}
|
||||
|
||||
// --- 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)
|
||||
}
|
||||
Reference in New Issue
Block a user