Getting started with the BenchPod

From a pod in a box to a passing test. Plug it in over USB, install the benchpod CLI, check the firmware answers, then decide how your tests will address it — through embeddedci.com by name, or straight at its IP address with no account at all. Budget about fifteen minutes. If your pod was sent to you already set up, step 3 will say so and you can skip step 5 altogether.

1. Connect the pod over USB

Plug the pod into your machine with a USB-C data cable. It enumerates as a USB CDC-ACM console — /dev/ttyACM0 on Linux, /dev/cu.usbmodem… on macOS, a COM port on Windows. The USB link both powers the pod and carries its console, so this is all you need for steps 1–6. Wiring the pod to the board you want to test comes later, in step 7.

2. Install the CLI

On macOS and Linux the Homebrew tap is the shortest path:

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

Prefer a plain binary, or on Windows? Pre-built archives for macOS, Linux and Windows (amd64 + arm64) are attached to every benchpod-cli release. Download the one for your platform, extract benchpod (or benchpod.exe), and put it on your PATH. Full install notes are in the CLI reference.

3. Find it with discover

benchpod discover is the one command to reach for whenever you want to know whether a bench exists and whether it works. It looks in both places a pod can be — it probes the machine's USB ports, and it browses the LAN over mDNS — then checks each pod it finds on the network by actually calling its API. You do not need to know an address, or even have the pod on a network yet.

$ benchpod discover

USB
  /dev/cu.usbmodem1103  bench-pod  fw v1.4.2  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 into setup: found over USB, running firmware v1.4.2, and not yet on a network — which the report says in as many words, along with what to do about it. Re-run discover after any change; it is the progress bar for the rest of this page.

4. Put the pod on your network

USB is enough for bring-up, but a pod on the LAN is what you actually want: it is reachable from CI, from other machines, and from the cloud. Wired Ethernet is the pod's primary interface — plug an RJ45 cable into the pod and it takes a DHCP lease on its own. There is nothing to configure and no credentials to enter.

If discover still reports no network address after you plug the cable in, the pod has no lease — check the cable and that the switch port hands out DHCP. The pod is DHCP-only; there is no static-IP configuration.

No cable? Use Wi-Fi instead

The pod also has an on-board ESP32-C3 for Wi-Fi. It stays powered down until you provision it, and credentials can only be set over the USB console — so this is the one step that must happen while the cable is plugged in.

# Only if there is no cable. Runs over the pod's USB console;
# omit --password to be prompted rather than putting it in shell history.
$ benchpod set-wifi --ssid "my-bench-wifi"

# Confirm it joined:
$ benchpod show-network

The join runs the full bring-up ladder on the device and can take ~40 s. When it lands, show-network prints the IP, SSID, state and RSSI.

Confirm with discover

Ask again and the pod now shows up twice — once per transport — with the two entries matched up as one physical pod, and its API checked rather than assumed:

# Nothing to configure for wired — plug the cable in and ask again:
$ benchpod discover

USB
  /dev/cu.usbmodem1103  bench-pod  fw v1.4.2  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, 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 that suggestion — benchpod discover --save — and every later command can drop --connection entirely.

mDNS is link-local: it works on a flat bench or office subnet, but does not cross routers/VLANs and is usually blocked on CI runners. That is why discover also probes USB — a pod that is plugged in shows up even when the network half finds nothing. On a CI runner, or across subnets, address the pod directly with --connection <ip>.

5. Pick how your tests address the pod

There are two ways to use a BenchPod, and they differ only in how a test names it.

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

Option A — register the pod (recommended)

Registering claims the pod into your org and provisions the firmware to open its own connection to embeddedci.com on every boot. After that, tests refer to a stable name instead of an IP that DHCP may change, and CI runners that can't see your bench network still reach it.

# 1. Create the account first: sign in at https://www.embeddedci.com
#    (GitHub, Google, Microsoft or Atlassian — there is no separate signup form).
# 2. Authenticate the CLI (opens a browser for the device-login approval):
$ benchpod login

# 3. Claim the pod into your org. This needs the pod's network address:
$ benchpod register --connection 192.168.1.213

# Rename it afterwards on the BenchPod page, or pick the name up front:
$ benchpod register --connection 192.168.1.213 --device-name bench-01

Then run your tests against the name (the cloud destination needs pip install "embeddedci[cloud]"):

$ pytest --benchpod-connection=embeddedci:bench-01

Option B — no account, use the address

If you'd rather not create an account, skip login/register entirely and address the pod directly. Direct commands are unauthenticated and send no tokens — they assume a trusted local network (see Security & trusted repos).

# No account needed — talk to the pod on your LAN:
$ pytest --benchpod-connection=192.168.1.213

# or, over USB:
$ pytest --benchpod-connection=usb

# or set it once for the shell / CI job:
$ export BENCHPOD_CONNECTION=192.168.1.213

The trade-off is addressing: the runner must be on the same network as the pod, and the IP has to stay put (a DHCP reservation helps).

These show the connection only. A test that flashes needs two more flags — see step 8.

6. Confirm the setup

One last discover. It now reports the whole path end to end — the pod found on both transports, its API answering, and its cloud registration live from the pod itself:

$ benchpod discover

USB
  /dev/cu.usbmodem1103  bench-pod  fw v1.4.2  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.

Already registered — nothing to set up on the pod itself.
Sign in at https://www.embeddedci.com and it will be on your BenchPod page.
Tests address it by name: `pytest --benchpod-connection=embeddedci:<name>`.

registered, cloud connected is what a finished Option A setup looks like — and equally what a pre-registered pod looked like back in step 3, because they end in the same place. If it says not registered, the pod never got the provisioning from step 5; if it says API unreachable, something answered mDNS but the firmware is not serving port 8080. On the direct path (Option B) there is no cloud line to expect — API ok is the whole verdict. Once registered, the pod also shows up on your BenchPod page, where you can rename it.

For the full firmware detail behind that summary — gateware version, PSRAM self-test, reset cause, stack and heap headroom — benchpod status prints the firmware's own status over either transport.

7. Wire the pod to your board

Everything so far worked with just the USB cable. To flash and measure a board, the pod has to be physically connected to it. The pod has 12 identical logic-analyzer channels (LA1–LA12) and no role-named pins — there is no pin marked SWCLK. You choose which channel carries which signal, and that choice goes into the test. For a first flash you need three wires:

BenchPod J1 header pinoutA 2 by 10 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. LA1 to LA6 have pull-up resistors, LA7 and LA8 have pull-downs, and LA9 to LA12 have neither. LA13 and LA14 are not exposed by the firmware.pull-upspull-downsCAN−2019CAN++1V81817+3V3GND1615GNDLA141413LA13LA121211LA11LA10109LA9LA887LA7LA665LA5LA443LA3LA221LA1▼ ADC / DAC side of the podpin 1 (LA1) is the square pad — bottom right, at the ADC/DAC endLA1–LA6 pull-ups, LA7/LA8 pull-downs (3.3 V only); LA9–LA12 neitherLA13 / LA14 reach the FPGA but are not exposed by the firmwareJ1, looking down at the pod — LA1 at the bottom right

The header is a 2×10 with the even pins down one side and the odd pins down the other. Pin 1 is the square pad at the bottom right, at the ADC/DAC end of the board, so LA numbering climbs as you go up. Note that the header carries fourteen LA lines but the firmware exposes LA1–LA12; LA13 and LA14 are routed to the FPGA and not addressable today. Where every other connector sits is on the pin-out page. For a first flash, three wires:

Pod header (J1)          DUT
  pin 11  LA11   <-->   SWCLK
  pin 12  LA12   <-->   SWDIO
  pin 15  GND    <-->   GND

# Write the mapping down — it goes straight into the test.

SWD can go on any pair of channels — LA11/LA12 above is just a choice. The one thing to get right is the logic level: the pod's LA bank is switchable between 1.8 V and 3.3 V and has no default, so pick the one your DUT uses or the pod refuses every channel operation. If you are also wiring an open-drain bus such as I2C, put it on LA1LA6: those are the channels with pull-ups, and they engage only at 3.3 V because they are referenced to +3V3 rather than to the bank rail. The pin-out page has the full table of which channels are biased and which way.

8. Write your first test

Install the Python package and let the pytest plugin do the wiring:

$ pip install embeddedci
# test_bench.py
import pytest

# This bench's wiring, from the step above. The pod has no role-named pins, so
# the mapping lives with the test.
SWCLK, SWDIO = 11, 12


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(benchpod_target, firmware):
    # benchpod_target has already powered the target on for this test.
    assert benchpod_target.flash(
        file=firmware, target="target/stm32f4x.cfg",
        swclk=SWCLK, swdio=SWDIO,
    ).ok

Run it with the connection you settled on in step 5, plus the two values the plugin cannot guess — your board's logic level and the firmware to flash:

$ pytest \
    --benchpod-connection=192.168.1.213 \
    --benchpod-la-voltage=3.3 \
    --benchpod-firmware=build/app.elf

--benchpod-la-voltage sets the LA bank voltage, which the pod needs before anything that touches a channel — flashing, UART capture, logic capture, pull-ups, I2C emulation. The pod chooses none at boot, so until something sets one it refuses those commands with la voltage not set. The setting lives in RAM and lasts until the pod reboots, so a run can appear to work without the flag when an earlier run or the web UI already set it — pass it every time so your suite does not depend on what happened to run before it. --benchpod-firmware feeds the firmware fixture; without it, tests that use that fixture skip rather than fail, so a suite stays green on a runner with no image to flash. Both also read from BENCHPOD_LA_VOLTAGE and the equivalent env vars, which is how you usually set them in CI.

test_pod_is_alive should pass as soon as the connection is right — it needs no wiring, no firmware and no OpenOCD, so it is the honest first green tick. test_target_flashes is the real one. The benchpod fixture is a connected pod for the session, benchpod_target powers the target on for the test and off at teardown, and pins (used in richer tests) exposes the channels by number. Without any connection configured the hardware fixtures skip rather than fail.

The full fixture list, every flag, OpenOCD verification and richer examples (I2C sensor emulation, UART capture, power cycling) are in the pytest framework reference.

9. Get your firmware into the flash dropdown

Everything above flashes a file from your own disk. The BenchPod page in the web app can also flash a build straight onto the pod, and that dropdown is filled by builds published to embeddedci.com — which is how you flash a colleague's firmware, or last Tuesday's, from the browser without a checkout.

You do not need our build pipeline for this. Keep building in GitHub Actions exactly 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/embeddedci-github-action/upload-artifact@main
        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"

The action uploads the firmware and every sibling build output it finds — the .elf, .bin and .hex next to it — and records the wiring, so the flash dialog opens with your channels already filled in. It needs no device and no BenchPod on the runner, so it is safe on every push. It outputs build_id if a later step needs it.

Once a build is published, open the BenchPod page, pick it in the flash dialog, and flash. To run the tests themselves on hardware from CI as well, see Run HIL in GitHub Actions.

Where to next