Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:<name>` | `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:<name>` is refused. |

Expand Down Expand Up @@ -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.

Expand Down
19 changes: 13 additions & 6 deletions cmd/benchpod-cli/blobs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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.",
Expand All @@ -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)")
Expand Down
160 changes: 160 additions & 0 deletions cmd/benchpod-cli/blobs_target.go
Original file line number Diff line number Diff line change
@@ -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
}
63 changes: 63 additions & 0 deletions cmd/benchpod-cli/blobs_target_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
}
}
10 changes: 8 additions & 2 deletions cmd/benchpod-cli/commands_serial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions cmd/benchpod-cli/flash.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading