From ffcbaf06653c66fbf8b961cb2f01f0624a3c8d32 Mon Sep 17 00:00:00 2001 From: Ian Gulliver Date: Tue, 7 Apr 2026 08:37:34 +0900 Subject: [PATCH] Merge info/load/test CLIs into unified picomap subcommand CLI --- .claude/skills/load/SKILL.md | 8 +- cmd/info/main.go | 76 ------------- cmd/{load => picomap}/main.go | 193 +++++++++++++++++++++++++++++----- cmd/test/main.go | 81 -------------- 4 files changed, 174 insertions(+), 184 deletions(-) delete mode 100644 cmd/info/main.go rename cmd/{load => picomap}/main.go (52%) delete mode 100644 cmd/test/main.go diff --git a/.claude/skills/load/SKILL.md b/.claude/skills/load/SKILL.md index 94f1b6e..33a7033 100644 --- a/.claude/skills/load/SKILL.md +++ b/.claude/skills/load/SKILL.md @@ -4,8 +4,10 @@ description: Build firmware, load it onto the Pico, and reboot. Use when the use user-invocable: true --- -Run `go run ./cmd/load/` from the project root. This builds both firmware targets, loads picomap onto the first device and picomap_test onto the second, and reboots both. +Run `go run ./cmd/picomap/ load` from the project root. This builds both firmware targets, loads picomap onto the first device and picomap_test onto the second, and reboots both. -After loading, run `go run ./cmd/info/` to verify both devices are responding. +To load a single target: `go run ./cmd/picomap/ load picomap` or `go run ./cmd/picomap/ load picomap_test`. -After modifying the load command itself (cmd/load/, lib/wire/, lib/picoserial/), run it twice: once to load the firmware, once to verify the load process still works end-to-end. +After loading, run `go run ./cmd/picomap/ info` to verify devices are responding. + +After modifying the CLI itself (cmd/picomap/, lib/), run load twice: once to load the firmware, once to verify the load process still works end-to-end. diff --git a/cmd/info/main.go b/cmd/info/main.go deleted file mode 100644 index 6605983..0000000 --- a/cmd/info/main.go +++ /dev/null @@ -1,76 +0,0 @@ -package main - -import ( - "encoding/hex" - "log/slog" - "net" - "os" - "sync" - "time" - - "github.com/theater/picomap/lib/client" -) - -type deviceResult struct { - dev string - info *client.ResponseInfo - err error -} - -func main() { - if err := run(); err != nil { - slog.Error("fatal", "err", err) - os.Exit(1) - } -} - -func run() error { - devs, err := client.ListSerial() - if err != nil { - return err - } - if len(devs) == 0 { - slog.Error("no devices found") - os.Exit(1) - } - - results := make([]deviceResult, len(devs)) - var wg sync.WaitGroup - for i, dev := range devs { - results[i].dev = dev - log := slog.With("dev", dev) - wg.Go(func() { - log.Info("connecting") - c, err := client.NewSerial(dev, 2*time.Second) - if err != nil { - results[i].err = err - return - } - log.Info("requesting info") - info, err := c.Info() - c.Close() - if err != nil { - results[i].err = err - return - } - log.Info("got info", "firmware", info.FirmwareName) - results[i].info = info - }) - } - wg.Wait() - - for _, r := range results { - if r.err != nil { - slog.Error("device error", "dev", r.dev, "err", r.err) - continue - } - slog.Info("device", - "dev", r.dev, - "board_id", hex.EncodeToString(r.info.BoardID[:]), - "mac", net.HardwareAddr(r.info.MAC[:]).String(), - "ip", net.IP(r.info.IP[:]).String(), - "firmware", r.info.FirmwareName) - } - - return nil -} diff --git a/cmd/load/main.go b/cmd/picomap/main.go similarity index 52% rename from cmd/load/main.go rename to cmd/picomap/main.go index b2d6faa..89a9ed8 100644 --- a/cmd/load/main.go +++ b/cmd/picomap/main.go @@ -1,9 +1,10 @@ package main import ( - "flag" + "encoding/hex" "fmt" "log/slog" + "net" "os" "os/exec" "path/filepath" @@ -14,24 +15,101 @@ import ( "github.com/theater/picomap/lib/picotool" ) -var target = flag.String("target", "all", "which firmware to load: picomap, picomap_test, or all") - func main() { - flag.Parse() - wd, err := os.Getwd() - if err != nil { - slog.Error("fatal", "err", err) + if len(os.Args) < 2 { + slog.Error("usage: picomap [args...]") os.Exit(1) } - buildDir := filepath.Join(wd, "firmware", "build") + cmd := os.Args[1] + args := os.Args[2:] - if err := run(buildDir); err != nil { + var err error + switch cmd { + case "info": + err = cmdInfo() + case "load": + err = cmdLoad(args) + case "test": + err = cmdTest(args) + default: + slog.Error("unknown command", "cmd", cmd) + os.Exit(1) + } + if err != nil { slog.Error("fatal", "err", err) os.Exit(1) } } -func build(buildDir string) error { +type deviceResult struct { + dev string + info *client.ResponseInfo + err error +} + +func cmdInfo() error { + devs, err := client.ListSerial() + if err != nil { + return err + } + if len(devs) == 0 { + return fmt.Errorf("no devices found") + } + + results := make([]deviceResult, len(devs)) + var wg sync.WaitGroup + for i, dev := range devs { + results[i].dev = dev + log := slog.With("dev", dev) + wg.Go(func() { + log.Info("connecting") + c, err := client.NewSerial(dev, 2*time.Second) + if err != nil { + results[i].err = err + return + } + log.Info("requesting info") + info, err := c.Info() + c.Close() + if err != nil { + results[i].err = err + return + } + log.Info("got info", "firmware", info.FirmwareName) + results[i].info = info + }) + } + wg.Wait() + + for _, r := range results { + if r.err != nil { + slog.Error("device error", "dev", r.dev, "err", r.err) + continue + } + slog.Info("device", + "dev", r.dev, + "board_id", hex.EncodeToString(r.info.BoardID[:]), + "mac", net.HardwareAddr(r.info.MAC[:]).String(), + "ip", net.IP(r.info.IP[:]).String(), + "firmware", r.info.FirmwareName) + } + + return nil +} + +func boardSerial(id [8]byte) string { + return fmt.Sprintf("%02X%02X%02X%02X%02X%02X%02X%02X", + id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7]) +} + +type deviceInfo struct { + dev string + serial string + uf2 string + name string +} + +func buildFirmware(buildDir string) error { slog.Info("configuring") cmake := exec.Command("cmake", "-S", filepath.Join(filepath.Dir(buildDir)), "-B", buildDir) cmake.Stdout = os.Stdout @@ -50,20 +128,19 @@ func build(buildDir string) error { return nil } -func boardSerial(id [8]byte) string { - return fmt.Sprintf("%02X%02X%02X%02X%02X%02X%02X%02X", - id[0], id[1], id[2], id[3], id[4], id[5], id[6], id[7]) -} +func cmdLoad(args []string) error { + target := "all" + if len(args) > 0 { + target = args[0] + } -type deviceInfo struct { - dev string - serial string - uf2 string - name string -} + wd, err := os.Getwd() + if err != nil { + return err + } + buildDir := filepath.Join(wd, "firmware", "build") -func run(buildDir string) error { - if err := build(buildDir); err != nil { + if err := buildFirmware(buildDir); err != nil { return err } @@ -84,7 +161,7 @@ func run(buildDir string) error { name string uf2 string } - switch *target { + switch target { case "all": targets = allTargets case "picomap": @@ -92,7 +169,7 @@ func run(buildDir string) error { case "picomap_test": targets = allTargets[1:] default: - return fmt.Errorf("unknown target %q", *target) + return fmt.Errorf("unknown target %q", target) } if len(devs) < len(targets) { @@ -159,6 +236,11 @@ func run(buildDir string) error { } } + uf2s := make([]string, len(targets)) + for i := range targets { + uf2s[i] = targets[i].uf2 + } + for i := range devices { log := slog.With("serial", devices[i].serial) wg.Go(func() { @@ -179,3 +261,66 @@ func run(buildDir string) error { slog.Info("done") return nil } + +func cmdTest(args []string) error { + if len(args) < 1 { + return fmt.Errorf("usage: picomap test ") + } + name := args[0] + + devs, err := client.ListSerial() + if err != nil { + return err + } + + var testDev string + for _, dev := range devs { + log := slog.With("dev", dev) + log.Info("connecting for info") + c, err := client.NewSerial(dev, 2*time.Second) + if err != nil { + log.Warn("connect error", "err", err) + continue + } + info, err := c.Info() + c.Close() + if err != nil { + log.Warn("info error", "err", err) + continue + } + log.Info("got info", "firmware", info.FirmwareName) + if info.FirmwareName == "picomap_test" { + testDev = dev + break + } + } + if testDev == "" { + return fmt.Errorf("no picomap_test device found") + } + + log := slog.With("dev", testDev) + log.Info("running test", "name", name) + + c, err := client.NewSerial(testDev, 10*time.Second) + if err != nil { + return err + } + defer c.Close() + + result, err := c.Test(name) + if err != nil { + return fmt.Errorf("remote: %w", err) + } + + for _, msg := range result.Messages { + log.Info("remote", "msg", msg) + } + + if result.Pass { + log.Info("PASS") + } else { + log.Error("FAIL") + os.Exit(1) + } + return nil +} diff --git a/cmd/test/main.go b/cmd/test/main.go deleted file mode 100644 index 1878cfa..0000000 --- a/cmd/test/main.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import ( - "log/slog" - "os" - "time" - - "github.com/theater/picomap/lib/client" -) - -func main() { - if len(os.Args) < 2 { - slog.Error("usage: test ") - os.Exit(1) - } - - if err := run(os.Args[1]); err != nil { - slog.Error("fatal", "err", err) - os.Exit(1) - } -} - -func run(name string) error { - devs, err := client.ListSerial() - if err != nil { - return err - } - - var testDev string - for _, dev := range devs { - log := slog.With("dev", dev) - log.Info("connecting for info") - c, err := client.NewSerial(dev, 2*time.Second) - if err != nil { - log.Warn("connect error", "err", err) - continue - } - info, err := c.Info() - c.Close() - if err != nil { - log.Warn("info error", "err", err) - continue - } - log.Info("got info", "firmware", info.FirmwareName) - if info.FirmwareName == "picomap_test" { - testDev = dev - break - } - } - if testDev == "" { - slog.Error("no picomap_test device found") - os.Exit(1) - } - - log := slog.With("dev", testDev) - log.Info("running test", "name", name) - - c, err := client.NewSerial(testDev, 10*time.Second) - if err != nil { - return err - } - defer c.Close() - - result, err := c.Test(name) - if err != nil { - slog.Error("remote error", "dev", testDev, "err", err) - os.Exit(1) - } - - for _, msg := range result.Messages { - log.Info("remote", "msg", msg) - } - - if result.Pass { - log.Info("PASS") - } else { - log.Error("FAIL") - os.Exit(1) - } - return nil -}