Getting started with the BenchPod
From a pod in a box to a passing test in about fifteen minutes: plug it in, install the benchpod CLI, get the pod online, 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. Plug in the pod
There are three ways to connect a pod: Ethernet, USB-C to your computer, or Wi-Fi. Each one carries the full API, so tests, the CLI and the Claude or Codex plugin work the same over any of them. Ethernet is the one we recommend: the pod is online on its own, needs no computer next to it, and has the steadiest link. USB-C alone is the alternative for a desk with no network port nearby. You can change later.
| Where | Cable | Online on embeddedci.com |
|---|---|---|
| Anywhere with a network port (recommended) | Ethernet (USB-C powers the pod) | On its own |
| At a desk, no network port | One USB-C cable to your computer: power and the API | Through the CLI's USB bridge on that computer (step 4) |
| No cable nearby (v3 pods) | Wi-Fi, set up once over USB | On its own |
Plug an RJ45 cable from a switch into the pod and power it from USB-C. The pod takes a DHCP lease on its own and reaches embeddedci.com without help from any computer. For the first setup, a USB-C cable to your computer is handy (the console finds the pod with no network at all), but the pod does not need it afterward.
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. Get the pod online
Online means the pod reaches embeddedci.com, so the web app, CI and your other computers can use it. Over Ethernet (recommended) or Wi-Fi it gets there on its own. Over USB-C alone it has no internet of its own: the CLI's USB bridge on your computer carries its link.
Ethernet (recommended), or Wi-Fi
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.
USB-C only: the USB bridge
The bridge runs on the computer the pod is plugged into and passes the pod's connection to embeddedci.com through that computer's own internet access. Install it once as a background service: it starts at login and picks the pod up again after either one reboots, so the pod stays online in the web app whenever the computer is on and the pod is plugged into it.
# Once per computer: run the bridge as a background service (starts at login) $ benchpod usb-bridge install # Is it installed and running, and which pod does it carry? $ benchpod usb-bridge status # Remove the service (also clears the pod's cloud proxy) $ benchpod usb-bridge uninstall # Or run it in the foreground for this session only (Ctrl-C stops it) $ benchpod usb-bridge
On this computer itself the bridge is not needed: the CLI, pytest and the Claude or Codex plugin reach the pod over the cable with --connection usb. A pod with Ethernet or Wi-Fi needs no bridge.
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. It saves the pod's network address even when the pod is also on USB, so the default still works once you unplug it. A pod with only its USB network link is saved as usb. 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:
| Approach | Test addresses the pod as | Needs |
|---|---|---|
| Registered (recommended) | embeddedci:bench-01 | An embeddedci.com account; works from anywhere, including CI |
| Direct | 192.168.1.213 or usb | Nothing; 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: its network address, or usb for a pod # on this computer's USB-C cable. It waits until the pod is connected, # then prints the pytest line to use: $ benchpod register --connection 192.168.1.213 $ benchpod register --connection usb # 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 talks to the pod's API: give it the pod's IP, or usb for a pod on this computer's USB-C cable. A pod on USB-C alone connects through the USB bridge (step 4), so install that first. 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:
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 setupasks too, orbenchpod la voltage 3.3V; - a test: the
benchpod_la_voltagefixture inconftest.py(step 8), orBenchPod(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 trippedRun 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.elftest_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
- pytest framework: fixtures, flashing, sensor emulation, the full SDK.
- Run HIL in GitHub Actions: the same tests on real hardware from CI.
- MCP server: drive the bench from Claude, Codex or Cursor.
- CLI reference: every
benchpodcommand. - Troubleshooting & FAQ: what to check when something doesn't answer.