BenchPod pytest framework
The embeddedci Python package is a pytest-friendly client for a BenchPod. It powers a target board, flashes it over SWD, captures its UART, and can emulate and decode an I2C sensor — straight from your tests. The same test runs against a pod on your desk or a remote pod in CI.
Install
pip install embeddedci # for the cloud (embeddedci:<device>) destination, add the cloud extra: pip install "embeddedci[cloud]"
The [cloud] extra pulls in a WebSocket client used only by the embeddedci: destination; local wifi/serial use needs nothing extra.
OpenOCD (required for flashing)
Flashing shells out to OpenOCD, which must be on your PATH. The pod's SWD probe is driven through OpenOCD's remote_bitbang adapter in SWD mode — and SWD support for remote_bitbang only exists in OpenOCD master (post-0.12.0). The stock packages (apt install openocd, brew install open-ocd) are 0.12.0 and fail with Can't change session's transport.
# xPack OpenOCD ships an OpenOCD master snapshot with SWD remote_bitbang: npm install -g @xpack-dev-tools/openocd # or grab a release tarball: # https://github.com/xpack-dev-tools/openocd-xpack/releases
Verify your OpenOCD can do SWD over remote_bitbang:
openocd -c "adapter driver remote_bitbang" -c "transport list" -c "exit" # must list: jtag swd (if it only lists 'jtag', it's too old)
Using the SDK directly
Outside of pytest, drive a pod with the BenchPod context manager:
from embeddedci import benchpod
with benchpod.BenchPod("192.168.1.213") as bp: # or "/dev/ttyACM0", or "serial"
bp.ping()
bp.power_on(benchpod.INTERNAL)
result = bp.flash(
file="firmware.elf", target="target/stm32f1x.cfg",
swclk=benchpod.PIN1, swdio=benchpod.PIN2, nreset=benchpod.PIN3,
target_power=benchpod.INTERNAL,
)
assert result.ok
bp.power_off(benchpod.INTERNAL)Named constants avoid magic numbers: benchpod.INTERNAL / EXTERNAL for the target-power eFuse, and benchpod.PIN1 … PIN12 for logic-analyzer channels (plain ints still work and are validated).
Using it in pytest
Installing the package registers a pytest plugin. Point it at a pod and use the fixtures:
pytest --benchpod-connection=192.168.1.213 # or: export BENCHPOD_CONNECTION=serial
import pytest
# The pod exposes 12 generic LA channels (pins.pin_1 .. pins.pin_12); it has no
# dedicated SWD/UART pins. Map your bench's wiring inline — any DUT signal can be
# on any channel. Here this bench has SWCLK on LA11 and SWDIO on LA12.
@pytest.mark.hardware
def test_firmware_flashes(benchpod, pins, firmware):
assert benchpod.flash(
file=firmware, target="target/stm32f4x.cfg",
swclk=pins.pin_11, swdio=pins.pin_12,
target_power=pins.efuse,
).ok
# benchpod_target powers the target on for the test and off at teardown:
def test_with_powered_target(benchpod_target):
...Fixtures
| Fixture | What it gives you |
|---|---|
benchpod | A connected BenchPod for the session (skips if no connection is set). |
benchpod_target | A BenchPod whose target is powered on for the test and off at teardown. |
pins | The pod's 12 generic logic-analyzer channels (pins.pin_1 … pins.pin_12) plus the pins.efuse rail. There are no role-named pins — map your bench's wiring (which signal is on which channel) in the test. Pull-ups exist only on LA1-8; check with pins.has_pullup(n). |
firmware | The firmware path from --benchpod-firmware, for tests that flash a real target. |
Connection strings
| Form | Transport |
|---|---|
192.168.1.213 or host:8080 | wifi/network (JSON over TCP, port 8080 default) |
/dev/ttyACM0, COM3 | serial (USB CDC-ACM console) |
serial / usb | serial, auto-detected by USB VID |
embeddedci:<device-name> | cloud — drive a named device through embeddedci.com (CI; see GitHub Actions HIL) |
Resolution order: --benchpod-connection → the benchpod_connection ini option → the BENCHPOD_CONNECTION env var.
Emulating an I2C sensor + capturing UART
The pod can pretend to be an I2C sensor (a BMP280) on two channels while you capture the DUT's UART — so you can flash an app, power-cycle it, and assert on its boot output with and without the sensor present. It also decodes the bus, so you can assert what the firmware actually did on the wire.
from embeddedci import benchpod
with benchpod.BenchPod("192.168.1.213") as bp:
bp.flash(file="app.elf", target="target/stm32f4x.cfg",
swclk=benchpod.PIN11, swdio=benchpod.PIN12, target_power=benchpod.INTERNAL)
# I2C is open-drain: enable the pod's pull-ups on SDA/SCL. Pull-ups exist only
# on LA1-8 (LA1/2=4.7k, LA3/4=2.2k, LA5-8=10k), so the I2C lines go on LA1/LA2.
bp.enable_pullup(benchpod.PIN1, benchpod.PIN2)
bp.enable_i2c_sensor(benchpod.Sensor.BMP280, sda=benchpod.PIN1, scl=benchpod.PIN2,
temperature_c=22.5, pressure_pa=101000)
# Power-cycle while capturing UART so the boot banner lands in the window.
cap = bp.power_cycle_and_capture(rx=benchpod.PIN5, tx=benchpod.PIN6,
delay=1.5, duration=6.0, until=r"APP_OK")
assert cap.match("APP_OK")