diff --git a/README.md b/README.md index 5226b64..4fbceea 100644 --- a/README.md +++ b/README.md @@ -165,7 +165,7 @@ Which command works over which connection: | Connection | Commands | |---------------------|----------| | Network address | Every pod command except the USB-only ones below. | -| USB | `status`, `la voltage`, `flash` (SWD), `lan-policy`, `sig-policy`, `cloud ca`, `cloud proxy` over the console; with firmware 3.9.0+ and no Wi-Fi configured, every other network command too, over the pod's USB network adapter. | +| USB | With firmware 3.9.0+ and no Wi-Fi configured, every network command over the pod's USB network adapter, including `status`, `la voltage`, `flash` (SWD) and `install-blobs`, which fall back to the console when the adapter is not up. `lan-policy`, `sig-policy`, `cloud ca` and `cloud proxy` always use the console (physical presence). | | `embeddedci:` | `status`, `ping`, `la voltage`, `la pullup`, `la status`, `la step`, `generate`, `lan-policy`, `sig-policy`, `cloud ca show`, `cloud ca clear`, `cloud proxy` (show, set, clear), `deregister`. | | USB only | `set-wifi`, `show-network`, `clear-wifi`, `identity`, `install-blobs`, `bootsel`, `dfu`, `usb-bridge`, `usb-reattach`; `flash-self` uses USB DFU. These always use the USB console: a saved address or `embeddedci:` default falls through to USB auto-detection, a device path still selects the port, and an explicit `--connection embeddedci:` is refused. | @@ -957,7 +957,8 @@ fit the pod's flash, the download's checksum does not match, or dfu-util fails. ### `benchpod install-blobs` -Install the gateware and ESP32-C3 images the pod's firmware goes with (USB). By +Install the gateware and ESP32-C3 images the pod's firmware goes with (USB: over the +pod's USB network adapter when it is up, firmware 3.9.0+, else its console). By default they come from the release that matches the firmware the pod runs. Each blob's signature is reported; for now a missing or failing one never stops it. diff --git a/cmd/benchpod-cli/blobs.go b/cmd/benchpod-cli/blobs.go index 1e83be7..506228a 100644 --- a/cmd/benchpod-cli/blobs.go +++ b/cmd/benchpod-cli/blobs.go @@ -137,7 +137,7 @@ func blobsToInstall(m blobsManifest, slots []serialconsole.BlobSlot, force bool) // installBlobs brings the pod's blob slots in line with the manifest at src, over the USB // console. only, when non-empty, limits it to those slot names. -func installBlobs(ctx context.Context, console *serialconsole.Console, src blobSource, force bool, only []string) error { +func installBlobs(ctx context.Context, console blobTarget, src blobSource, force bool, only []string) error { slots, err := console.Blobs(ctx) if err != nil { return err @@ -170,7 +170,7 @@ func installBlobs(ctx context.Context, console *serialconsole.Console, src blobS return err } manifest := checkBlobSignature(ctx, src, e, data) // report only - fmt.Fprintf(os.Stderr, "blobs: installing %s (%d KB) over USB...\n", e.Name, (e.Size+1023)/1024) + fmt.Fprintf(os.Stderr, "blobs: installing %s (%d KB) through %s...\n", e.Name, (e.Size+1023)/1024, console.Via()) last := -1 start := time.Now() rep, err := console.UploadSigned(ctx, e.Name, data, e.Version, manifest, func(done, total int) { @@ -211,7 +211,8 @@ func newInstallBlobsCmd(g *globalFlags) *cobra.Command { Short: "Install the gateware and ESP32-C3 images the pod's firmware goes with (over USB)", Long: "The pod keeps its iCE40 gateware images and its ESP32-C3 Wi-Fi image in the\n" + "W25Q flash next to the FPGA, not in its firmware. This sends the ones it lacks\n" + - "over the USB console. By default they come from the GitHub release that matches\n" + + "over USB: the pod's USB network adapter when it is up (firmware 3.9.0+), else its\n" + + "USB console. By default they come from the GitHub release that matches\n" + "the firmware the pod runs; --dir takes them from a local build (stm32h563/build/blobs).\n\n" + "Each blob's signature (blob-*.bin.sig) is checked and reported, and handed to\n" + "firmware that checks it too; for now a missing or failing one never stops an install.", @@ -227,20 +228,26 @@ func newInstallBlobsCmd(g *globalFlags) *cobra.Command { } defer cancel() defer console.Close() + // The pod's USB network when it is up (firmware 3.9.0+), else its console. + target, closeTarget, err := blobTargetFor(ctx, console, path) + if err != nil { + return err + } + defer closeTarget() src := blobSource{base: dir} if strings.TrimSpace(dir) == "" { tag := release if strings.TrimSpace(tag) == "" { - tag = podFirmwareRelease(ctx, console) + tag = target.Release(ctx) } src = releaseBlobSource(tag) } - fmt.Fprintf(os.Stderr, "blobs: pod on %s, blobs from %s\n", path, src) + fmt.Fprintf(os.Stderr, "blobs: pod on %s (through %s), blobs from %s\n", path, target.Via(), src) var sel []string if strings.TrimSpace(only) != "" { sel = strings.Split(only, ",") } - return installBlobs(ctx, console, src, force, sel) + return installBlobs(ctx, target, src, force, sel) }, } cmd.Flags().StringVar(&dir, "dir", "", "take the blobs from this directory (holding blobs-manifest.json)") diff --git a/cmd/benchpod-cli/blobs_target.go b/cmd/benchpod-cli/blobs_target.go new file mode 100644 index 0000000..b98240e --- /dev/null +++ b/cmd/benchpod-cli/blobs_target.go @@ -0,0 +1,160 @@ +package main + +import ( + "context" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "fmt" + "strings" + + "github.com/embeddedci-com/benchpod-cli/internal/serialconsole" + "github.com/embeddedci-com/benchpod-cli/internal/tcpclient" +) + +// blobTarget is where install-blobs reads the pod's W25Q slots and sends blobs: the pod's network +// API (over its USB network adapter, firmware 3.9.0+: blob_status and ota_* with a target) or its +// USB console (blobs and upload-*, older firmware). +type blobTarget interface { + Blobs(ctx context.Context) ([]serialconsole.BlobSlot, error) + UploadSigned(ctx context.Context, target string, data []byte, version int, manifest []byte, + progress func(done, total int)) (serialconsole.SigReport, error) + // Release is the tag of the firmware the pod runs ("v3.9.0"), "" if unknown. + Release(ctx context.Context) string + // Via names the way in for messages: "/dev/cu.usbmodem1", "10.237.8.85:8080". + Via() string +} + +// consoleBlobs is the USB console. +type consoleBlobs struct { + *serialconsole.Console + path string +} + +func (c consoleBlobs) Release(ctx context.Context) string { return podFirmwareRelease(ctx, c.Console) } +func (c consoleBlobs) Via() string { return c.path } + +// lanBlobChunk is the raw bytes per ota_data line (as the LAN OTA in hwe2e sends). +const lanBlobChunk = 768 + +// lanBlobs is the pod's JSON API on one connection. +type lanBlobs struct { + lan lanSession + addr string +} + +func (l lanBlobs) Via() string { return l.addr } + +func (l lanBlobs) Release(ctx context.Context) string { + var st struct { + Version string `json:"version"` + } + if l.lan.cmd(ctx, map[string]any{"cmd": "status"}, &st) != nil || st.Version == "" { + return "" + } + if strings.HasPrefix(st.Version, "v") { + return st.Version + } + return "v" + st.Version +} + +func (l lanBlobs) Blobs(ctx context.Context) ([]serialconsole.BlobSlot, error) { + var rep struct { + Blobs []struct { + Name string `json:"name"` + State string `json:"state"` + Size int `json:"size"` + Version int `json:"version"` + SHA256 string `json:"sha256"` + Present bool `json:"present"` + } `json:"blobs"` + } + if err := l.lan.cmd(ctx, map[string]any{"cmd": "blob_status"}, &rep); err != nil { + if pe, ok := tcpclient.AsPodError(err); ok && strings.Contains(pe.Reason, "unknown cmd") { + return nil, serialconsole.ErrNoBlobSlots + } + return nil, err + } + slots := make([]serialconsole.BlobSlot, 0, len(rep.Blobs)) + for _, b := range rep.Blobs { + sha := b.SHA256 + if !b.Present { + sha = "" + } + slots = append(slots, serialconsole.BlobSlot{Name: b.Name, State: b.State, Size: b.Size, Version: b.Version, SHA256: sha}) + } + return slots, nil +} + +// otaSession is the pod's ota_* reply. +type otaSession struct { + State string `json:"state"` + Error string `json:"error"` + Sig string `json:"sig"` + SigKey string `json:"sig_key"` +} + +func (l lanBlobs) UploadSigned(ctx context.Context, target string, data []byte, version int, manifest []byte, + progress func(done, total int)) (serialconsole.SigReport, error) { + var rep serialconsole.SigReport + if len(data) == 0 { + return rep, fmt.Errorf("upload %s: empty image", target) + } + sum := sha256.Sum256(data) + begin := map[string]any{"cmd": "ota_begin", "size": len(data), "sha256": hex.EncodeToString(sum[:]), + "target": target, "version": version} + if len(manifest) == serialconsole.ManifestLen { + begin["sig"] = base64.RawURLEncoding.EncodeToString(manifest) + rep.Supported, rep.Sent = true, true + } + var st otaSession + if err := l.lan.cmd(ctx, begin, &st); err != nil { + return rep, fmt.Errorf("upload %s: begin: %w", target, err) + } + rep.Result, rep.KeyID = st.Sig, st.SigKey + abort := func() { + if !l.lan.s.Broken() { + _ = l.lan.cmd(ctx, map[string]any{"cmd": "ota_abort"}, nil) + } + } + for off := 0; off < len(data); off += lanBlobChunk { + end := min(off+lanBlobChunk, len(data)) + if err := l.lan.cmd(ctx, map[string]any{"cmd": "ota_data", "offset": off, + "data": base64.RawURLEncoding.EncodeToString(data[off:end])}, nil); err != nil { + abort() + return rep, fmt.Errorf("upload %s at %d: %w", target, off, err) + } + if progress != nil { + progress(end, len(data)) + } + } + if err := l.lan.cmd(ctx, map[string]any{"cmd": "ota_end"}, &st); err != nil { + abort() + return rep, fmt.Errorf("upload %s: end: %w", target, err) + } + if st.State != "verified" { + abort() + return rep, fmt.Errorf("upload %s: verify: %s", target, valueOr(st.Error, st.State)) + } + if err := l.lan.cmd(ctx, map[string]any{"cmd": "ota_commit"}, &st); err != nil { + return rep, fmt.Errorf("upload %s: commit: %w", target, err) + } + if st.State == "error" { + return rep, fmt.Errorf("upload %s: commit: %s", target, valueOr(st.Error, "failed")) + } + return rep, nil +} + +// blobTargetFor picks the pod's USB network over its console when it is up (the console's +// status names the address); the caller closes the returned closer. +func blobTargetFor(ctx context.Context, console *serialconsole.Console, path string) (blobTarget, func(), error) { + if u, err := console.USBNetwork(ctx); err == nil && u.On && u.Link { + addr := u.PodIP + ":8080" + if dialUSBPod(addr) == nil { + if lan, err := openLAN(ctx, addr); err == nil { + return lanBlobs{lan: lan, addr: addr}, func() { lan.s.Close() }, nil + } + } + } + return consoleBlobs{Console: console, path: path}, func() {}, nil +} diff --git a/cmd/benchpod-cli/blobs_target_test.go b/cmd/benchpod-cli/blobs_target_test.go new file mode 100644 index 0000000..26f36a4 --- /dev/null +++ b/cmd/benchpod-cli/blobs_target_test.go @@ -0,0 +1,63 @@ +package main + +import ( + "bytes" + "context" + "io" + "strings" + "testing" + "time" + + "github.com/embeddedci-com/benchpod-cli/internal/serialconsole" +) + +// textConsole answers `status` with the given lines and a prompt, like the firmware's console. +type textConsole struct { + status string + out bytes.Buffer +} + +func (c *textConsole) Read(p []byte) (int, error) { + if c.out.Len() == 0 { + time.Sleep(5 * time.Millisecond) + return 0, nil + } + return c.out.Read(p) +} + +func (c *textConsole) Write(p []byte) (int, error) { + line := strings.TrimSpace(string(bytes.ReplaceAll(p, []byte{0x08}, nil))) + if line != "" { + c.out.WriteString(line + "\r\n") + if line == "status" { + c.out.WriteString(c.status) + } + c.out.WriteString("> ") + } + return len(p), nil +} + +func (c *textConsole) Close() error { return nil } + +var _ io.ReadWriteCloser = (*textConsole)(nil) + +// New CLI + firmware without a usable USB network: install-blobs keeps today's console path. +func TestBlobTargetFallsBackToTheConsole(t *testing.T) { + cases := map[string]string{ + "firmware before 3.9.0": " device : benchpod\r\n ip : 192.168.1.220\r\n", + "Wi-Fi configured": " device : benchpod\r\n usb : net off (Wi-Fi configured) pod 10.237.8.85 host 10.237.8.86 link down rx 0 (drop 0) tx 0 (full 0)\r\n", + } + for name, status := range cases { + c := serialconsole.NewConsole(&textConsole{status: status}) + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + target, closeT, err := blobTargetFor(ctx, c, "/dev/cu.usbmodem1") + cancel() + if err != nil { + t.Fatalf("%s: %v", name, err) + } + closeT() + if _, ok := target.(consoleBlobs); !ok || target.Via() != "/dev/cu.usbmodem1" { + t.Fatalf("%s: target %T via %s, want the console", name, target, target.Via()) + } + } +} diff --git a/cmd/benchpod-cli/commands_serial.go b/cmd/benchpod-cli/commands_serial.go index b50f73e..b61ba74 100644 --- a/cmd/benchpod-cli/commands_serial.go +++ b/cmd/benchpod-cli/commands_serial.go @@ -113,13 +113,19 @@ func runSetWifi(g *globalFlags, ssid, pw string, skipBlobs bool) (string, error) func ensureEspBlob(console *serialconsole.Console) error { ctx, cancel := context.WithTimeout(context.Background(), blobsInstallTimeout) defer cancel() - slots, err := console.Blobs(ctx) + // Before Wi-Fi is set the pod's USB network is still on (firmware 3.9.0+): use it when up. + target, closeTarget, err := blobTargetFor(ctx, console, "the USB console") + if err != nil { + return err + } + defer closeTarget() + slots, err := target.Blobs(ctx) missing, err := espBlobMissing(slots, err) if err != nil || !missing { return err } fmt.Fprintln(os.Stderr, "set-wifi: the pod has no ESP32-C3 image for Wi-Fi yet; installing it over USB first") - return installBlobs(ctx, console, releaseBlobSource(podFirmwareRelease(ctx, console)), false, nil) + return installBlobs(ctx, target, releaseBlobSource(target.Release(ctx)), false, nil) } // espBlobMissing reads the pod's blob listing for set-wifi: whether the ESP32-C3 image needs diff --git a/cmd/benchpod-cli/flash.go b/cmd/benchpod-cli/flash.go index b5b3d70..60804ff 100644 --- a/cmd/benchpod-cli/flash.go +++ b/cmd/benchpod-cli/flash.go @@ -11,6 +11,7 @@ import ( "time" "github.com/embeddedci-com/benchpod-cli/internal/openocd" + "github.com/embeddedci-com/benchpod-cli/internal/serialconsole" "github.com/embeddedci-com/benchpod-cli/internal/serverapi" "github.com/embeddedci-com/benchpod-cli/internal/tcpclient" "github.com/spf13/cobra" @@ -144,6 +145,7 @@ func runFlash(g *globalFlags, f *flashFlags) error { if err != nil { return err } + spec = g.preferUSBNetwork(spec, "flash") fromBuild := f.fromBuild() if fromBuild && strings.TrimSpace(f.file) != "" { return fmt.Errorf("pass either --file or an uploaded build (--build, --repo, --ref, --artifact), not both") @@ -352,6 +354,7 @@ func openDAP(ctx context.Context, g *globalFlags, swclk, swdio int, powerEfuse i if err != nil { return nil, false, err } + spec = g.preferUSBNetwork(spec, "flash") // the probe over the USB network is much faster if spec.IsNetwork() { client := &tcpclient.Client{Addr: spec.Addr} // Ask the pod whether it has the dedicated reset pin BEFORE arming the @@ -397,6 +400,11 @@ func openDAP(ctx context.Context, g *globalFlags, swclk, swdio int, powerEfuse i if powerEfuse != 0 { if err := console.TargetPower(ctx, powerEfuse, true); err != nil { console.Close() + if errors.Is(err, serialconsole.ErrMovedToNetwork) || errors.Is(err, serialconsole.ErrUnknownCommand) { + return nil, false, fmt.Errorf("flash: this pod's console has no power command (firmware 3.9.0+ switches " + + "target power over the network API) and its USB network is not usable; drop --target-power, " + + "or connect over the network") + } return nil, false, fmt.Errorf("flash: enable target-power eFuse %d over %s: %w", powerEfuse, path, err) } fmt.Fprintf(os.Stderr, "flash: target-power eFuse %d on\n", powerEfuse) diff --git a/cmd/benchpod-cli/flash_self.go b/cmd/benchpod-cli/flash_self.go index df501bf..724d458 100644 --- a/cmd/benchpod-cli/flash_self.go +++ b/cmd/benchpod-cli/flash_self.go @@ -478,6 +478,9 @@ func checkImageFitsPod(ctx context.Context, dfuPath, file string) error { return nil } +// skipUSBNetForBlobs keeps installBlobsAfterFlash on the console (tests). +var skipUSBNetForBlobs = false + // flashedPodSerial is the USB serial number of the pod flash-self rebooted into DFU over its // console ("" when it was put in DFU by hand): it finds the pod again after the update. var flashedPodSerial string @@ -526,14 +529,28 @@ func installBlobsAfterFlash(g *globalFlags, src blobSource) error { defer console.Close() ctx, cancel := context.WithTimeout(context.Background(), blobsInstallTimeout) defer cancel() - if _, err := console.Blobs(ctx); err != nil { + target := blobTarget(consoleBlobs{Console: console, path: "the USB console"}) + if u, err := console.USBNetwork(ctx); err == nil && u.On && !skipUSBNetForBlobs { + // The freshly booted pod's USB network may need a few seconds on the computer's side. + for i := 0; i < 10; i++ { + if t, closeT, err := blobTargetFor(ctx, console, "the USB console"); err == nil { + if _, ok := t.(lanBlobs); ok { + target = t + defer closeT() + break + } + } + time.Sleep(time.Second) + } + } + if _, err := target.Blobs(ctx); err != nil { if errors.Is(err, serialconsole.ErrNoBlobSlots) { fmt.Fprintln(os.Stderr, "flash-self: this firmware keeps its blobs built in; nothing to install") return nil } return fmt.Errorf("flash-self: the firmware is flashed, but reading the pod's blob slots failed, so its blobs are not installed (retry with `benchpod install-blobs`): %w", err) } - if err := installBlobs(ctx, console, src, false, nil); err != nil { + if err := installBlobs(ctx, target, src, false, nil); err != nil { return fmt.Errorf("flash-self: the firmware is flashed, but installing its blobs failed (retry with `benchpod install-blobs`): %w", err) } return nil diff --git a/cmd/benchpod-cli/lavoltage.go b/cmd/benchpod-cli/lavoltage.go index b351742..9a8dffd 100644 --- a/cmd/benchpod-cli/lavoltage.go +++ b/cmd/benchpod-cli/lavoltage.go @@ -118,7 +118,10 @@ func runLAVoltage(g *globalFlags, mv int, out, warn io.Writer) error { // cannot tell). It never prompts or fails. A variable so tests can replace it. var accountDeviceForConnection = func(g *globalFlags) string { spec, err := g.resolveTarget() - if err != nil || spec.IsSerial() { + if err != nil { + return "" + } + if spec = g.preferUSBNetwork(spec, "la voltage"); spec.IsSerial() { return "" } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) @@ -158,6 +161,7 @@ func laVoltageDo(g *globalFlags, mv int) (string, laVoltageReply, error) { if err != nil { return "", laVoltageReply{}, err } + spec = g.preferUSBNetwork(spec, what) var rep laVoltageReply if spec.IsSerial() { console, path, ctx, cancel, err := openLAVoltageConsole(g, spec.Device, g.effectiveTimeout(15*time.Second)) @@ -167,6 +171,14 @@ func laVoltageDo(g *globalFlags, mv int) (string, laVoltageReply, error) { defer cancel() defer console.Close() rep.MV, err = console.LAVoltage(ctx, mv) + if errors.Is(err, serialconsole.ErrMovedToNetwork) || + (errors.Is(err, serialconsole.ErrUnknownCommand) && consoleHasUSBNet(ctx, console)) { + // Firmware 3.9.0+ moved la-voltage from the console to the JSON API; preferUSBNetwork + // already found the USB network unusable, so say why. + _, nerr := g.networkTarget(what) + return "", rep, fmt.Errorf("%s: this pod's console has no la-voltage (firmware 3.9.0+ sets it over the "+ + "network API) and its USB network is not usable: %v", what, nerr) + } if err != nil { return "", rep, laVoltageError(what, err) } @@ -215,6 +227,19 @@ func laVoltageError(what string, err error) error { return fmt.Errorf("%s: %w", what, err) } +// consoleHasUSBNet: the pod's console reports a USB network adapter (firmware 3.9.0+), which +// tells a console that moved a command to the JSON API from one too old to have it. +func consoleHasUSBNet(ctx context.Context, c laVoltageConsole) bool { + u, ok := c.(interface { + USBNetwork(context.Context) (serialconsole.USBNet, error) + }) + if !ok { + return false + } + _, err := u.USBNetwork(ctx) + return err == nil +} + // formatMV is 3300 -> "3.3 V". func formatMV(mv int) string { return strings.TrimSuffix(strings.TrimRight(fmt.Sprintf("%.3f", float64(mv)/1000), "0"), ".") + " V" diff --git a/cmd/benchpod-cli/lavoltage_test.go b/cmd/benchpod-cli/lavoltage_test.go index fce6caf..fbee24b 100644 --- a/cmd/benchpod-cli/lavoltage_test.go +++ b/cmd/benchpod-cli/lavoltage_test.go @@ -156,6 +156,12 @@ func TestLAVoltageUSB(t *testing.T) { if err := runLAVoltage(&globalFlags{connection: "usb"}, 0, &bytes.Buffer{}, &bytes.Buffer{}); err == nil || err.Error() != laVoltageMissing { t.Fatalf("old firmware: %v", err) } + // A 3.9.0 console moved la-voltage to the network API, and the USB network was not usable. + fc.err = serialconsole.ErrMovedToNetwork + if err := runLAVoltage(&globalFlags{connection: "usb"}, 0, &bytes.Buffer{}, &bytes.Buffer{}); err == nil || + !strings.Contains(err.Error(), "network API") || !strings.Contains(err.Error(), "USB network is not usable") { + t.Fatalf("3.9.0 console without a usable USB network: %v", err) + } } func TestLAVoltageArgIsAUsageError(t *testing.T) { diff --git a/cmd/benchpod-cli/podclient.go b/cmd/benchpod-cli/podclient.go index 78b205d..0eec714 100644 --- a/cmd/benchpod-cli/podclient.go +++ b/cmd/benchpod-cli/podclient.go @@ -163,7 +163,10 @@ const wiringLookupTimeout = 6 * time.Second // fails (then it writes one warning line to warn). A variable so tests can replace it. var loadPodWiring = func(g *globalFlags, warn io.Writer) *podWiring { spec, err := g.resolveTarget() - if err != nil || spec.IsSerial() { + if err != nil { + return nil + } + if spec = g.preferUSBNetwork(spec, "wiring"); spec.IsSerial() { return nil } ctx, cancel := context.WithTimeout(context.Background(), wiringLookupTimeout) diff --git a/cmd/benchpod-cli/spiflash.go b/cmd/benchpod-cli/spiflash.go index 3a13397..f1a7436 100644 --- a/cmd/benchpod-cli/spiflash.go +++ b/cmd/benchpod-cli/spiflash.go @@ -71,7 +71,7 @@ func (f *spiFlags) fillFromWiring(g *globalFlags) { log.Printf("spi-flash: pins from the flags") return } - if spec, err := g.resolveTarget(); err != nil || !spec.IsNetwork() { + if spec, err := g.resolveTarget(); err != nil || !g.preferUSBNetwork(spec, "spi-flash").IsNetwork() { return // the connection error comes from the command itself } w := loadPodWiring(g, os.Stderr) diff --git a/cmd/benchpod-cli/usbnet.go b/cmd/benchpod-cli/usbnet.go index 5650b9e..cc8c15e 100644 --- a/cmd/benchpod-cli/usbnet.go +++ b/cmd/benchpod-cli/usbnet.go @@ -98,6 +98,20 @@ func (g *globalFlags) usbNetAddr(device string) (string, error) { return lan, nil } +// preferUSBNetwork turns a USB spec into the pod's address on its USB network adapter when that +// is up (on 3.9.0+ with USB networking off, its Ethernet or Wi-Fi address when that answers), for a +// command that works over both; any other spec, or a USB pod without a way in over the network +// (older firmware, nothing answers), is returned unchanged and the command uses the console. +func (g *globalFlags) preferUSBNetwork(spec ConnSpec, cmd string) ConnSpec { + if !spec.IsSerial() { + return spec + } + if net, err := g.networkTarget(cmd); err == nil && net.IsNetwork() { + return net + } + return spec +} + // usbNote is where networkTarget says which way it took (stderr; tests capture it). var usbNote io.Writer = os.Stderr diff --git a/internal/serialconsole/fake.go b/internal/serialconsole/fake.go new file mode 100644 index 0000000..7228855 --- /dev/null +++ b/internal/serialconsole/fake.go @@ -0,0 +1,7 @@ +package serialconsole + +import "io" + +// NewConsole wraps an already-open port (or a test fake answering like the firmware) as a +// Console. The CLI opens real ports with Open/OpenBenchpod; this is for tests outside the package. +func NewConsole(rw io.ReadWriteCloser) *Console { return newConsole(rw) } diff --git a/internal/serialconsole/lavoltage.go b/internal/serialconsole/lavoltage.go index eee7bd3..8578c8e 100644 --- a/internal/serialconsole/lavoltage.go +++ b/internal/serialconsole/lavoltage.go @@ -52,6 +52,9 @@ func parseLAVoltageReply(raw string) (int, error) { s = strings.ReplaceAll(s, "\r", "\n") for _, ln := range strings.Split(s, "\n") { ln = strings.TrimSpace(strings.TrimLeft(ln, "> \t\x08")) + if strings.Contains(ln, movedLine) { + return 0, ErrMovedToNetwork + } if strings.Contains(ln, "unknown command") { return 0, ErrUnknownCommand } diff --git a/internal/serialconsole/lavoltage_test.go b/internal/serialconsole/lavoltage_test.go index 8aa8d62..c2feb2f 100644 --- a/internal/serialconsole/lavoltage_test.go +++ b/internal/serialconsole/lavoltage_test.go @@ -16,6 +16,7 @@ type laVoltagePod struct { v2 bool // a v2 board: no 1.8 V inUse string // pins in use: a change is refused old bool + moved bool // firmware 3.9.0+: la-voltage moved to the JSON API writes []string } @@ -27,6 +28,8 @@ func (p *laVoltagePod) console() *Console { switch { case p.old: fmt.Fprintf(out, " unknown command '%s' (try 'help')\r\n", f[0]) + case p.moved: + fmt.Fprintf(out, "%s: moved to the network API (JSON la_voltage) in firmware 3.9.0; update the benchpod CLI to 0.6.0 or later (or embeddedci to 2.12.0), which uses it over USB\r\n", f[0]) case len(f) == 1 && p.mv == 0: out.WriteString(" LA VCCIO = UNSET (st=0)\r\n") case len(f) == 1: @@ -78,3 +81,15 @@ func TestLAVoltageRefusals(t *testing.T) { t.Fatalf("old firmware: %v", err) } } + +// Firmware 3.9.0 answers la-voltage with the moved line; firmware before it runs the command. +func TestLAVoltageAcrossFirmwareGenerations(t *testing.T) { + _, err := (&laVoltagePod{moved: true}).console().LAVoltage(testContext(t), 3300) + if !errors.Is(err, ErrMovedToNetwork) { + t.Fatalf("3.9.0 console: err = %v, want ErrMovedToNetwork", err) + } + got, err := (&laVoltagePod{}).console().LAVoltage(testContext(t), 3300) + if err != nil || got != 3300 { + t.Fatalf("older firmware: %d, %v", got, err) + } +} diff --git a/internal/serialconsole/power_test.go b/internal/serialconsole/power_test.go index ae7110d..a0a7a6d 100644 --- a/internal/serialconsole/power_test.go +++ b/internal/serialconsole/power_test.go @@ -13,6 +13,7 @@ import ( // powerPod answers `power` like the firmware's console (console.c). type powerPod struct { old bool + moved bool // firmware 3.9.0+: power moved to the JSON API on [3]bool writes []string } @@ -23,6 +24,8 @@ func (p *powerPod) console() *Console { out.WriteString(line + "\r\n[net] dhcp renew\r\n") f := strings.Fields(line) switch { + case p.moved: + fmt.Fprintf(out, "%s: moved to the network API (JSON target_power) in firmware 3.9.0; update the benchpod CLI to 0.6.0 or later (or embeddedci to 2.12.0), which uses it over USB\r\n", f[0]) case p.old || f[0] != "power" || len(f) < 3: fmt.Fprintf(out, " unknown command '%s' (try 'help')\r\n", f[0]) case f[1] != "1" && f[1] != "2": @@ -66,3 +69,12 @@ func TestTargetPowerRefusals(t *testing.T) { t.Fatalf("old firmware: %v", err) } } + +func TestTargetPowerAcrossFirmwareGenerations(t *testing.T) { + if err := (&powerPod{moved: true}).console().TargetPower(testContext(t), 1, true); !errors.Is(err, ErrMovedToNetwork) { + t.Fatalf("3.9.0 console: err = %v, want ErrMovedToNetwork", err) + } + if err := (&powerPod{}).console().TargetPower(testContext(t), 1, true); err != nil { + t.Fatalf("older firmware: %v", err) + } +} diff --git a/internal/serialconsole/serialconsole.go b/internal/serialconsole/serialconsole.go index 357a75c..1ef0f64 100644 --- a/internal/serialconsole/serialconsole.go +++ b/internal/serialconsole/serialconsole.go @@ -557,6 +557,8 @@ func parsePowerReply(raw string, efuse int, want string) error { for _, ln := range strings.Split(s, "\n") { ln = strings.TrimSpace(strings.TrimLeft(ln, "> \t\x08")) switch { + case strings.Contains(ln, movedLine): + return ErrMovedToNetwork case strings.Contains(ln, "unknown command"): return ErrUnknownCommand case ln == ok: diff --git a/internal/serialconsole/usbnet.go b/internal/serialconsole/usbnet.go index dd086a8..407cac4 100644 --- a/internal/serialconsole/usbnet.go +++ b/internal/serialconsole/usbnet.go @@ -23,6 +23,13 @@ type USBNet struct { LANIP string // the status `ip` line: the pod's Ethernet (else Wi-Fi) address, "" when it has none } +// ErrMovedToNetwork is a console command firmware 3.9.0 moved to the JSON API: the console answers +// ": moved to the network API (JSON ) ..." instead of running it. +var ErrMovedToNetwork = errors.New("firmware 3.9.0 moved this command from the USB console to the network API") + +// movedLine is the marker of that answer. +const movedLine = "moved to the network API" + // ErrNoUSBNet means the pod's console reports no USB network adapter: firmware older than 3.9.0. var ErrNoUSBNet = errors.New("the pod's firmware has no USB network adapter (needs firmware 3.9.0 or later)")