Getting started with the BenchPod

From a pod in a box to a passing test in about fifteen minutes: plug it in over USB, install the benchpod CLI, put the pod on your network, wire it to your board and run a pytest against it.

The quick path: benchpod setup

Plug in the pod (step 1), install the CLI (step 2), then run one command. It walks through steps 3 to 6 for you: finds the pod, puts it on your network, asks for your board's I/O voltage, signs you in and registers the pod, and saves the connection so later commands need no address. A step that is already done is skipped, so it is safe to run again.

$ benchpod setup

1/5  Find the pod
     Found a BenchPod: USB /dev/cu.usbmodem1103, no network address yet, firmware 3.8.0.

2/5  Network
     Plug the pod's Ethernet port into your network. Waiting up to 2m0s for a DHCP lease...
     The pod is on the network at 192.168.1.213:8080.

3/5  Board I/O voltage
     Which I/O voltage does your target board (DUT) use? The pod's LA pins, UART and SWD use it.
  1) 3.3 V (most boards, including the NUCLEO examples)
  2) 1.8 V
Choose 1-2: 1
     Set to 3.3 V.

4/5  embeddedci.com
     The pod is not registered. Register it to your embeddedci.com account (web app, CI, remote use)? [Y/n]
     Signing in to embeddedci.com first.
...
5/5  Save the connection

The pod is set up. Next steps:
  • Wire SWD to your target: SWCLK on LA11, SWDIO on LA12, and GND.
  ...

Then wire the pod to your board (step 7) and write a test (step 8). The steps below are what setup does, one at a time, for when you want to see or change each part. Flags:

# No prompts (for a script): Wi-Fi from a secret, a 1.8 V board
$ benchpod setup --ssid "my-bench-wifi" --password-stdin --la-voltage 1.8V --yes < wifi-password.txt

# Direct use only, no embeddedci.com account
$ benchpod setup --no-register

Prefer the browser? Wi-Fi can also be set from the web app (step 4), and the pod's workspace asks for the I/O voltage the first time you open it.

To watch the setup as you go, open Add a pod on the BenchPod page: it lists each step with the command to type and ticks it when the pod gets there. In Chrome or Edge it also reads the pod over USB (status, identity and wifi-show only: it never changes the pod); in other browsers it follows your account until the pod appears. You still run the commands, or benchpod setup, yourself.

1. Connect the pod over USB

Plug the pod into your computer with a USB-C data cable. USB powers the pod and carries its console, which is all you need until you wire it to your board in step 7. The console shows up as:

/dev/cu.usbmodem…

2. Install the CLI

brew install --cask embeddedci-com/tap/benchpod
benchpod --version

The binary isn't Apple-notarized, so the cask strips the quarantine flag on install. It also installs dfu-util, which flash-self needs.

# or the plain binary (the releases/latest/download URL is always the newest release)
curl -fsSL -o benchpod.tar.gz \
  https://github.com/embeddedci-com/benchpod-cli/releases/latest/download/benchpod_Darwin_arm64.tar.gz
tar -xzf benchpod.tar.gz benchpod
sudo mv benchpod /usr/local/bin/

On an Intel Mac use benchpod_Darwin_x86_64.tar.gz. If you download the archive with a browser instead, clear the quarantine flag once with xattr -d com.apple.quarantine ./benchpod.

Archives for every platform (amd64 and arm64) are attached to each benchpod-cli release. More in the CLI reference.

3. Find it with discover

benchpod discover looks for pods on your USB ports and on the LAN (mDNS), checks each one it finds, and says what to do next. You do not need an address yet. Run it again after every step below: it is the progress bar for this page.

$ benchpod discover

USB
  /dev/cu.usbmodem1103  stm32h563  fw v3.8.0  no network address  not registered

Network (mDNS)
  nothing advertising _benchpod._tcp on this subnet

1 BenchPod found.

/dev/cu.usbmodem1103 has no network address yet. Plug in Ethernet (it takes a DHCP lease on
its own), or join Wi-Fi with `benchpod set-wifi --ssid <ssid>`. Tests and
`benchpod register` both need the pod on the network.

Not registered yet. To drive this pod through embeddedci.com (and from CI):
  benchpod login       # authenticate this machine
  benchpod register    # claim the pod into your account
Or skip both and address the pod directly by IP. No account needed.

That is a healthy pod one step in: found on USB, running firmware, not on a network yet.

4. Put the pod on your network

USB is enough for bring-up, but a pod on the LAN is reachable from CI, from other computers and from embeddedci.com. Ethernet is the main interface; Wi-Fi is there for benches without a cable.

Plug an RJ45 cable into the pod. It takes a DHCP lease on its own: nothing to configure. If discover still says no network address, check the cable and that the switch port hands out DHCP. The pod has no static-IP setting.

Run discover again. The pod now shows up on both USB and the network, matched up as one pod:

$ benchpod discover

USB
  /dev/cu.usbmodem1103  stm32h563  fw v3.8.0  ip 192.168.1.213  not registered

Network (mDNS)
  INSTANCE         ADDRESS             HOSTNAME               ID
  BenchPod a1b2c3  192.168.1.213:8080  benchpod-a1b2c3.local  3q2-7fPmVYhE0Lc…
  BenchPod a1b2c3: API ok, not registered (`benchpod login` then `benchpod register`), same pod as /dev/cu.usbmodem1103

1 BenchPod found.

Next: `benchpod discover --save` stores 192.168.1.213:8080 as the default connection,
so later commands can omit --connection.

Take the suggestion (benchpod discover --save) and later commands can drop --connection. mDNS does not cross routers or VLANs and is usually blocked on CI runners; there, address the pod with --connection <ip>.

The saved connection is shared: with embeddedci 2.10 or newer, the Python SDK (BenchPod() with no connection) and the Claude or Codex plugin (connect with no argument) use it too. An explicit connection wins, then BENCHPOD_CONNECTION, then the saved one. A pytest run uses it only when you pass --benchpod-connection=saved.

5. Pick how your tests reach the pod

Skip this step if discover says registered: that pod is already on your account. Otherwise there are two ways, and they differ only in how a test names the pod:

ApproachTest addresses the pod asNeeds
Registered (recommended)embeddedci:bench-01An embeddedci.com account; works from anywhere, including CI
Direct192.168.1.213 or usbNothing; the test runner must reach the pod

Registering claims the pod into your account. It then connects to embeddedci.com on every boot, tests use a stable name instead of an IP that DHCP may change, and CI runners that can't see your bench network still reach it.

You need an account first: sign up, or sign in with GitHub or Google. Use one account for all your pods and your team; a pod is only visible to the account (and organization) it is registered to.

# 1. Authenticate the CLI. It opens a browser: approve it while signed in
#    with the account that should own the pod.
$ benchpod login

# 2. Claim the pod into your account. This needs the pod's network address.
#    It waits until the pod is connected, then prints the pytest line to use:
$ benchpod register --connection 192.168.1.213

# The pod is named after itself (benchpod-a1b2c3). Rename it on the
# BenchPod page, or pick the name up front:
$ benchpod register --connection 192.168.1.213 --device-name bench-01

register needs the pod's IP: it talks to the pod over the network, not USB. Then run your tests against the name. benchpod login only signs in the CLI; the Python SDK uses an API key, or the job's OIDC token inside GitHub Actions:

# The SDK authenticates with an API key (benchpod:control scope).
# Inside GitHub Actions it can use OIDC instead.
$ pip install "embeddedci[cloud]"
$ export BENCHPOD_API_KEY=eci_...
$ pytest --benchpod-connection=embeddedci:bench-01

6. Confirm the setup

One last discover. On a registered pod, with this computer signed in (benchpod login), it also checks that the pod is on your account:

$ benchpod discover

USB
  /dev/cu.usbmodem1103  stm32h563  fw v3.8.0  ip 192.168.1.213  registered, cloud connected

Network (mDNS)
  INSTANCE         ADDRESS             HOSTNAME               ID
  BenchPod a1b2c3  192.168.1.213:8080  benchpod-a1b2c3.local  3q2-7fPmVYhE0Lc…
  BenchPod a1b2c3: API ok, registered, cloud connected, same pod as /dev/cu.usbmodem1103

1 BenchPod found.

Registered to your account as bench-01: nothing to set up on the pod itself.
Tests address it by name: `pytest --benchpod-connection=embeddedci:bench-01`.

On the direct path there is no cloud line to expect: API ok is the whole verdict. For a summary of one pod, run benchpod status:

$ benchpod status
BenchPod at 192.168.1.213:8080
  Firmware      3.8.0 (latest)
  Gateware      48
  Board         stm32h563 v3
  I/O voltage   3.3 V
  Target power  off
  Network       192.168.1.213 (ready), Wi-Fi connected
  Cloud         registered, connected, on your account as bench-01

benchpod status --json prints the pod's raw status instead (gateware detail, PSRAM self-test, reset cause).

On the web, open the pod from your BenchPod page: the workspace shows a Set up this pod checklist that ticks itself off from the pod's real state (online, on your network, I/O voltage confirmed, wiring saved, first flash, first test run), with a link to the step for each open item. It hides once everything is done, or when you close it. The first flash and first test run are recorded when they go through embeddedci.com (the web app, or an embeddedci: connection); a pod only used over the LAN does not tick them.

7. Wire the pod to your board

The pod has 14 identical logic-analyzer channels (LA1 to LA14) and no role-named pins: you choose which channel carries which signal. For a first flash you need three wires:

BenchPod J1 header pinoutA 2 by 11 header shown with pin 1 at the bottom right, on the ADC and DAC side of the pod. Even pins 2 to 14 run down the left and are LA2, LA4, LA6, LA8, LA10, LA12 and LA14; odd pins 1 to 13 run down the right and are LA1, LA3, LA5, LA7, LA9, LA11 and LA13. Pins 15 and 16 are ground, pin 17 is plus 3.3 volts, pin 18 is plus 1.8 volts, pin 19 is CAN plus and pin 20 is CAN minus. Pin 21 is LA_REF, a reference output at the LA bank's configured voltage, and pin 22 is NRST_CONTROL, the pod's dedicated target-reset line. LA1 to LA6 have pull-up resistors, LA7 and LA8 have pull-downs, and LA9 to LA14 have neither. LA13 and LA14 need firmware 3.1 or newer.pull-upspull-downsNRST_CONTROL2221LA_REFCAN−2019CAN++1V81817+3V3GND1615GNDLA141413LA13LA121211LA11LA10109LA9LA887LA7LA665LA5LA443LA3LA221LA1▼ ADC / DAC side of the podpin 1 (LA1) is the square pad (bottom right, at the ADC/DAC end)LA1–LA6 pull-ups, LA7/LA8 pull-downs (3.3 V only); LA9–LA14 neitherLA13 / LA14 need firmware 3.1 or newerLA_REF (21) is the LA reference; NRST_CONTROL (22) drives resetJ1, looking down at the pod: LA1 at the bottom right
Pod header (J1)          DUT
  pin 11  LA11   <-->   SWCLK
  pin 12  LA12   <-->   SWDIO
  pin 15  GND    <-->   GND

Pin 1 is the square pad at the bottom right, at the ADC/DAC end of the board. SWD can go on any two channels; LA11 and LA12 are the defaults. Open-drain buses such as I2C go on LA1 to LA6, which have pull-ups (at 3.3 V only). The full table is on the pin-out page.

Set the board's I/O voltage

The LA bank runs at 1.8 V or 3.3 V and boots with no voltage set: the pod will not drive your board's pins at a guess, so it refuses flashing, UART, logic capture, pull resistors and sensor emulation until one is chosen. Match your board's I/O. Any of these sets it:

  • the web app: the first time you open the pod's workspace it asks What I/O voltage does your board use? (later: the Wiring tab);
  • the CLI: benchpod setup asks too, or benchpod la voltage 3.3V;
  • a test: the benchpod_la_voltage fixture in conftest.py (step 8), or BenchPod(conn, la_voltage=3.3) in a script;
  • the Claude or Codex plugin: set_la_voltage.

The web app saves the answer in the pod's wiring profile, and embeddedci.com applies it again every time the pod connects, so it survives a power cycle. The CLI, a test and the plugin set it on the pod itself, until the pod restarts; that is why a test suite sets it every time it connects.

Record it in the wiring profile

The wiring profile says which DUT signal is on which channel (UART, I2C, SWD, SPI, named signals such as TRIGGER), plus the LA voltage and the target-power rail. Edit it on the pod's Wiring tab on your BenchPod page. Its defaults already put SWCLK on LA11 and SWDIO on LA12, so the three wires above need no changes: the setup checklist's Keep the defaults saves them as they are.

The web app tabs and the Python SDK start from the profile. On an embeddedci: connection the SDK reads it from the server.

It is one profile for every tool. On the LAN, when you are signed in (an API key or benchpod login) and the pod is on your account, the SDK and the Claude or Codex plugin (embeddedci 2.10 or newer) and the CLI's flash and spi-flash use the same stored profile. Over USB, pass the wiring yourself. Wiring you pass always wins, such as --benchpod-wiring=wiring.json (the Wiring tab's Copy as JSON button gives you the contents). Set BENCHPOD_WIRING_SOURCE=local to use only a local file or the defaults in the SDK and the plugin.

Install OpenOCD for flashing

SWD flashing goes through OpenOCD with the cmsis_dap_tcp backend, which only exists in builds newer than 0.12.0. Stock packages lack it, and flashing stops with an error saying so. Nothing before this step needs OpenOCD.

# Homebrew's stable open-ocd is 0.12.0, so build it from HEAD
brew install --HEAD open-ocd

8. Write your first test

Install the Python package (Python 3.10 or newer):

$ pip install "embeddedci[pytest]"

Next to your tests, a conftest.py sets your board's I/O voltage once for the whole suite. The benchpod fixture applies it every time it connects, so a run never depends on what an earlier run or the web app left behind:

# conftest.py
import pytest

@pytest.fixture(scope="session")
def benchpod_la_voltage():
    return 3.3  # the board's I/O voltage: 1.8 for a 1V8 board
# test_bench.py
import pytest


def test_pod_is_alive(benchpod):
    """Needs no wiring at all: a good first green tick."""
    assert benchpod.ping()


@pytest.mark.hardware
def test_target_flashes_and_boots(benchpod_target, firmware):
    # No pin numbers anywhere: the SWD channels, the UART pair, the baud rate
    # and the power rail all come from the wiring profile (step 7).
    assert benchpod_target.flash(file=firmware, target="target/stm32f4x.cfg").ok

    # Power-cycle the board and watch its console across the reboot.
    boot = benchpod_target.power_cycle_and_capture(duration=5.0, until="APP_OK")
    assert boot.matched, boot.text


@pytest.mark.hardware
def test_boot_stays_inside_its_power_budget(benchpod_target):
    profile = benchpod_target.measure_power(3.0)
    assert profile.peak_current < 0.25       # amps, measured on the rail
    assert not profile.fault                 # the eFuse never tripped

Run it with the connection from step 5 and the firmware to flash:

$ pytest \
    --benchpod-connection=192.168.1.213 \
    --benchpod-wiring=wiring.json \
    --benchpod-firmware=build/app.elf

test_pod_is_alive passes as soon as the connection is right: no wiring, firmware or OpenOCD needed. The other two are the real tests. Neither mentions a pin number, and both assert on what the board did: reach APP_OK after a real power cycle, and stay inside its power budget.

Tests that need --benchpod-firmware skip without it, and every hardware test skips when no connection is set, so the suite stays green on a runner without a pod. In CI, set the connection with BENCHPOD_CONNECTION. On an embeddedci: connection, or a signed-in LAN run on a pod on your account, the wiring comes from the server and --benchpod-wiring is not needed.

Every fixture and flag, and examples for I2C sensor emulation, UART capture and power cycling, are in the pytest framework reference.

9. Get your firmware into the flash dropdown

The BenchPod page can also flash a build straight onto the pod from the browser: a colleague's firmware, or last Tuesday's. Keep building in GitHub Actions as you do now, and add one step that publishes the result:

# .github/workflows/firmware.yml
permissions:
  id-token: write        # lets the job mint the OIDC token embeddedci authenticates with
  contents: read

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      # ... build your firmware however you already do ...

      - uses: embeddedci-com/submit-job-action/upload-artifact@v1
        with:
          firmware: build/app.elf
          build_target: stm32f4                    # platform id shown on the Builds page
          openocd_target: target/stm32f4x.cfg      # pre-fills the flash dialog
          swclk: "11"
          swdio: "12"
          efuse: "1"

It uploads the firmware with its sibling .elf, .bin and .hex and records the wiring, so the flash dialog opens with your channels filled in. It needs no pod on the runner. The job signs in with its GitHub OIDC token, so the workflow needs permissions: id-token: write and the repository must be trusted under BenchPod → GitHub Actions. To run the tests on hardware from CI as well, see Run HIL in GitHub Actions.

10. Explore the bench from Claude or Codex

Knowing what to assert means poking at the board first. The BenchPod plugin lets Claude or Codex drive this bench: flash a build, power-cycle it, read the console, capture a waveform, profile the rail.

Settings → Plugins → Add → Add from a repository, and enter embeddedci-com/benchpod-mcp. The plugin works in the Code tab.

Then ask it something you would otherwise do by hand: “connect to the BenchPod at 192.168.1.213, flash this build, power-cycle the board and tell me what the console says”. The tools are the SDK, so what it finds translates straight into a pytest test. Screenshots, other clients and the full tool list are on the MCP server page.

Where to next