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
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,10 @@ $ benchpod setup --la-voltage 3.3V
Default connection set to network/TCP 192.168.1.221:8080 (saved to ~/.config/benchpod-cli/config.json).
```

When the pod runs firmware older than the latest release, step 1 also prints the update
command (`benchpod flash-self --enter-dfu`, then replug USB-C); setup does not flash on its own.


| Flag | Default | Purpose |
|--------------------|---------|-------------------------------------------------------------------------|
| `--la-voltage` | (ask) | Board I/O voltage to set: `1.8V` or `3.3V`. Without it, a voltage already set is kept and an unset one is asked for. |
Expand Down
14 changes: 9 additions & 5 deletions cmd/benchpod-cli/commands_discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,19 +192,23 @@ func checkPod(p *discoveredPod) {
}

// correlate marks each network pod that is the same physical device as one of
// the pods found over USB, matched on the address the pod reports for itself or,
// for a registered pod, on its cloud device id. The id catches a pod on Ethernet
// and Wi-Fi at once, whose USB console reports one address while mDNS answers on
// the other. Without this a single pod plugged in and on the LAN reads as two.
// the pods found over USB, matched on the address the pod reports for itself, on its
// public key (USB `identity` vs the mDNS TXT id) or, for a registered pod, on its
// cloud device id. The key and the id catch a pod on Ethernet and Wi-Fi at once,
// whose USB console reports one address while mDNS answers on the other; the key
// also works before the pod is registered. Without this a single pod plugged in and
// on the LAN reads as two.
func correlate(netPods []discoveredPod, serialPods []serialconsole.SerialPod) {
for i := range netPods {
host, _, err := net.SplitHostPort(netPods[i].addr)
if err != nil {
host = netPods[i].addr
}
id := strings.TrimSpace(netPods[i].cloud.DeviceID)
key := strings.TrimSpace(netPods[i].id)
for _, sp := range serialPods {
if (sp.Addressed() && sp.IP == host) || (id != "" && strings.TrimSpace(sp.DeviceID) == id) {
if (sp.Addressed() && sp.IP == host) || (id != "" && strings.TrimSpace(sp.DeviceID) == id) ||
(key != "" && strings.TrimSpace(sp.PublicKey) == key) {
netPods[i].sameAs = sp.Device
break
}
Expand Down
15 changes: 15 additions & 0 deletions cmd/benchpod-cli/commands_discover_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ func TestCorrelateMatchesADualHomedPodByDeviceID(t *testing.T) {
}
}

// A pod straight out of the box on Ethernet and Wi-Fi has no device id yet; its public key
// (USB `identity`, mDNS TXT id) is what makes it one pod rather than two.
func TestCorrelateMatchesAnUnregisteredDualHomedPodByPublicKey(t *testing.T) {
netPods := []discoveredPod{{addr: "192.168.1.221:8080", id: "uuoGf1v6n9yeBZZVkey", cloud: cloudState{known: true}}}
correlate(netPods, []serialconsole.SerialPod{{Device: "/dev/ttyACM0", IP: "192.168.1.220", PublicKey: "uuoGf1v6n9yeBZZVkey"}})
if netPods[0].sameAs != "/dev/ttyACM0" {
t.Fatalf("got %q", netPods[0].sameAs)
}
other := []discoveredPod{{addr: "192.168.1.221:8080", id: "someone-else", cloud: cloudState{known: true}}}
correlate(other, []serialconsole.SerialPod{{Device: "/dev/ttyACM0", IP: "192.168.1.220", PublicKey: "uuoGf1v6n9yeBZZVkey"}})
if other[0].sameAs != "" {
t.Fatalf("different keys were matched: %q", other[0].sameAs)
}
}

func TestCorrelateIgnoresUnaddressedSerialPod(t *testing.T) {
// A pod with no lease reports 0.0.0.0; it must never match a network entry,
// which would otherwise collapse two distinct pods into one.
Expand Down
12 changes: 12 additions & 0 deletions cmd/benchpod-cli/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ type setupEnv interface {
accountDevices() map[string]string
register(addr string) error
save(target string) error
// latestFirmware is the latest firmware release tag, "" when it could not be read.
latestFirmware() string
}

// errNeedsAnswer is returned under --yes when a step needs an answer that no flag gave.
Expand Down Expand Up @@ -210,6 +212,14 @@ func runSetup(env setupEnv, ui *setupUI, o setupOpts) error {
}
p := pods[0]
fmt.Fprintf(out, " Found %s.\n", describeSetupPod(p))
if latest := env.latestFirmware(); latest != "" && p.firmware != "" && versionLess(p.firmware, latest) {
// A nudge, not a step: flashing over USB DFU needs the pod replugged afterwards, so the
// user runs it when it suits them.
fmt.Fprintf(out, " Firmware %s is out of date: %s is the latest. Update it with the pod on USB:\n"+
" benchpod flash-self --enter-dfu\n"+
" (unplug and replug USB-C when it finishes), or later from the web app's BenchPod page.\n",
strings.TrimPrefix(p.firmware, "v"), strings.TrimPrefix(latest, "v"))
}

// 2. Network.
step(2, "Network")
Expand Down Expand Up @@ -653,3 +663,5 @@ func (e *realSetupEnv) saveLaMV(deviceID string, mv int) (bool, error) {
}

func (e *realSetupEnv) save(target string) error { return runSetConnection(e.g, target) }

func (e *realSetupEnv) latestFirmware() string { return latestFirmwareRelease() }
34 changes: 34 additions & 0 deletions cmd/benchpod-cli/setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ type fakeSetupEnv struct {
afterReg cloudState // cloud state after register
profileMV int // la_mv in the stored wiring profile, 0 = none
saveErr error // what saveLaMV fails with
latest string // latest firmware release, "" = unknown

calls []string
wifiSSID string
Expand Down Expand Up @@ -89,6 +90,8 @@ func (f *fakeSetupEnv) save(target string) error {
return nil
}

func (f *fakeSetupEnv) latestFirmware() string { return f.latest }

func (f *fakeSetupEnv) did(call string) bool {
for _, c := range f.calls {
if c == call {
Expand Down Expand Up @@ -386,3 +389,34 @@ func TestSetupSavesTheVoltageAfterRegistering(t *testing.T) {
t.Fatalf("calls %v profile %d\n%s", env.calls, env.profileMV, out)
}
}

func TestSetupNudgesAnOutdatedFirmware(t *testing.T) {
newEnv := func(fw, latest string) *fakeSetupEnv {
return &fakeSetupEnv{
pods: []setupPod{{device: "/dev/cu.usbmodem1", addr: "192.168.1.221:8080", firmware: fw, cloud: registeredMine}},
laMV: 3300,
cloud: registeredMine,
who: "you@example.com",
account: map[string]string{"dev-1": "benchpod-baea06"},
latest: latest,
}
}
out, err := runSetupWith(t, newEnv("v3.6.0", "v3.8.0"), "", setupOpts{laVoltageMV: 3300, yes: true})
if err != nil {
t.Fatalf("setup: %v\n%s", err, out)
}
for _, want := range []string{"Firmware 3.6.0 is out of date: 3.8.0 is the latest", "benchpod flash-self --enter-dfu"} {
if !strings.Contains(out, want) {
t.Errorf("missing %q\n%s", want, out)
}
}
for _, env := range []*fakeSetupEnv{newEnv("v3.8.0", "v3.8.0"), newEnv("v3.6.0", "")} {
out, err := runSetupWith(t, env, "", setupOpts{laVoltageMV: 3300, yes: true})
if err != nil {
t.Fatalf("setup: %v\n%s", err, out)
}
if strings.Contains(out, "out of date") {
t.Errorf("nudged without cause\n%s", out)
}
}
}
15 changes: 14 additions & 1 deletion internal/serialconsole/probe.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ type SerialPod struct {
Registered bool
CloudState string // "connected", "connecting", "backoff", ... when registered
DeviceID string // the provisioned device id, when registered

// PublicKey is the pod's Ed25519 identity (base64url), the same key the pod advertises as
// the mDNS TXT "id". It matches a USB pod to its network entry when the two report
// different addresses (USB shows the wired IP, mDNS may answer on Wi-Fi) and the pod is not
// registered yet, so there is no device id to match on. "" on firmware without `identity`.
PublicKey string
}

// Addressed reports whether the pod holds a usable IP address (it has a DHCP
Expand Down Expand Up @@ -71,12 +77,19 @@ func ProbeSerial(preferred string, probeTimeout time.Duration) (pods []SerialPod
ctx, cancel := context.WithTimeout(context.Background(), probeTimeout)
raw, _ := c.Status(ctx)
cancel()
_ = c.Close()
if !strings.Contains(strings.ToLower(raw), benchpodMarker) {
_ = c.Close()
skipped = append(skipped, name)
continue
}
pod := parseSerialPod(name, raw)
// Read-only: `identity` prints the pod's public key and changes nothing.
ctx, cancel = context.WithTimeout(context.Background(), probeTimeout)
if id, idErr := c.Identity(ctx); idErr == nil {
pod.PublicKey = id.PublicKey
}
cancel()
_ = c.Close()
key := podIdentity(pod)
if seen[key] {
continue
Expand Down
Loading