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, emulates and decodes an I2C sensor, and drives the analog front-end and CAN, straight from your tests. The same test runs against a pod on your desk or a remote pod in CI.
Install
pip install embeddedci # Python 3.10+ # optional extras (combine them, e.g. "embeddedci[cloud,pytest]"): pip install "embeddedci[pytest]" # installs pytest alongside the plugin pip install "embeddedci[cloud]" # the embeddedci:<name> cloud destination pip install "embeddedci[discovery]" # find a pod on the LAN via mDNS pip install "embeddedci[analysis]" # numpy for Capture.fft / dominant_frequency
Needs Python 3.10 or newer. The core package covers local network and USB pods; the pytest plugin registers itself on install. [cloud] adds the WebSocket client the embeddedci: destination needs, [discovery] adds mDNS for discover, [analysis] adds numpy for the FFT helpers, and [pytest] simply installs pytest too.
OpenOCD (required for flashing)
Flashing shells out to OpenOCD, which must be on your PATH (or passed as openocd_bin=). The pod runs a CMSIS-DAP probe in its firmware, and the SDK drives it through OpenOCD's cmsis-dap adapter with its TCP backend (cmsis_dap_tcp), which ships whole DAP transfers instead of per-bit toggles. That backend only exists in OpenOCD builds newer than 0.12.0. The stock packages (apt install openocd, brew install open-ocd) are 0.12.0 and lack it. The SDK checks before it flashes and raises a FlashError telling you to update.
# Homebrew's stable open-ocd is 0.12.0, so build it from HEAD brew install --HEAD open-ocd
Verify your OpenOCD has the CMSIS-DAP TCP backend:
openocd -c "adapter driver cmsis-dap" -c "cmsis-dap backend tcp" -c "cmsis-dap tcp port 4441" -c shutdown # must exit cleanly: an error on the 'backend tcp' line means the build is too old
Flash a build uploaded to embeddedci.com
A build uploaded with the upload-artifact action or the build_report fixture (see Cloud builds) is flashed by naming it instead of passing file: build is the build id the upload printed (or "latest"), repo and ref pick the newest build of a branch or tag, and artifact names a file when the build holds several (else its .elf, then .hex, then .bin).
# the newest build of main, uploaded by CI:
benchpod.flash(build="latest", repo="acme/firmware", ref="main", target_power=INTERNAL)
# or the exact build a workflow uploaded (its build_id output):
result = benchpod.flash(build=os.environ["BUILD_ID"], check=False)
assert result.ok, result.stdout
print(result.artifact["name"], result.artifact.get("sha"))On a cloud connection (embeddedci:<device>) embeddedci.com runs OpenOCD through the pod, so the runner needs no OpenOCD and a GitHub Actions OIDC session is enough; such a flash always verifies and resets. On a LAN or USB connection the file is downloaded and flashed by the local OpenOCD, which needs an API key or benchpod login. server_side= picks one explicitly, and FlashResult.artifact says which build was flashed.
Using the SDK directly
Outside of pytest, drive a pod with the BenchPod context manager:
from embeddedci.benchpod import BenchPod, INTERNAL, PIN1, PIN2
# la_voltage selects the LA I/O-bank voltage: the board's I/O voltage, 1.8 for a 1V8 board.
with BenchPod("192.168.1.213", la_voltage=3.3) as bp: # or "/dev/ttyACM0", "usb", "embeddedci:<name>"
bp.ping() # raises if the pod doesn't answer
result = bp.flash(
file="firmware.elf", target="target/stm32f1x.cfg",
swclk=PIN1, swdio=PIN2,
nreset=True, # a flag: the target's reset is wired to the pod's reset pin (J1 pin 22)
target_power=INTERNAL, # power the target from the internal 5 V eFuse first
)
assert result.ok # a failed flash raises FlashError; pass check=False to inspect it instead
assert not bp.target_status().efuse(INTERNAL).fault # eFuse not tripped
print(bp.power_status().rail(INTERNAL).current, "A") # INA monitor on that rail
bp.power_off(INTERNAL)Named constants avoid magic numbers: INTERNAL / EXTERNAL for the target-power eFuse, and PIN1 … PIN14 for logic-analyzer channels, all importable from embeddedci.benchpod (plain ints still work; an out-of-range value raises ValueError). Units are volts, seconds and hertz throughout, pod state comes back as typed results, and a pod, transport or server failure raises a BenchPodError subclass (see Errors).
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=usb # add the firmware image for tests that flash a target: # --benchpod-firmware=build/app.elf
import pytest
# The pod exposes 14 generic LA channels (pins.pin_1 .. pins.pin_14); it has no
# dedicated SWD/UART pins. Which DUT signal is on which channel is the pod's
# wiring profile (the web app's Wiring tab, or --benchpod-wiring=wiring.json),
# which is benchpod.wiring — so flash() needs no pin numbers at all.
@pytest.mark.hardware
def test_firmware_flashes(benchpod, firmware):
wiring = benchpod.wiring
assert benchpod.flash(
file=firmware, target="target/stm32f4x.cfg",
swclk=wiring.swd_swclk, swdio=wiring.swd_swdio, # the defaults, spelled out
target_power=wiring.efuse,
).ok
# benchpod_target powers the target on for the test and off at teardown:
@pytest.mark.hardware
def test_rail_is_healthy(benchpod_target, pins):
assert not benchpod_target.target_status().efuse(pins.efuse).fault
# skipped unless the connected pod advertises the capability:
@pytest.mark.benchpod_capability("dac_deep_replay")
def test_long_replay(benchpod_dac):
...Fixtures
| Fixture | What it gives you |
|---|---|
benchpod | A connected BenchPod for the session (skips if no connection is set). It selects the LA voltage from benchpod_la_voltage on connect and, for a cloud pod, holds its lease. |
benchpod_la_voltage | The board's I/O voltage (1.8 or 3.3), selected on the pod when the session connects. Override it once in conftest.py. The default returns None, which falls back to BENCHPOD_LA_VOLTAGE. |
benchpod_wiring | The pod's wiring profile (a Wiring): which role or named Signal is on each LA channel, plus the LA voltage, target-power rail, baud, I2C address and SWD target. On an embeddedci: connection it is the pod's profile from the server, which you edit in the web app's Wiring tab. On a LAN connection it comes from --benchpod-wiring, else the same server profile when you are signed in and the pod is on your account, else the defaults (see One wiring profile). This fixture is an input: it returns None unless you override it in conftest.py (like benchpod_la_voltage) to supply a profile in code. The profile in force is always benchpod.wiring, and benchpod.signal("TRIGGER") looks a named signal up. |
benchpod_target | A BenchPod whose target is powered on for the test and off at teardown: the rail from --benchpod-efuse, else the wiring profile's. |
pins / benchpod_pins | The pod's 14 generic logic-analyzer channels (pins.pin_1 … pins.pin_14) plus the pins.efuse rail. There are no role-named pins: which signal is on which channel lives in the wiring profile (benchpod_wiring). LA1–LA6 have pull-ups (4.7k/2.2k/10k), LA7/LA8 10k pull-downs, and LA9–LA14 neither. Check with pins.has_pullup(n) / pins.has_pulldown(n), and engage them with enable_pullup(n) / enable_pulldown(n) (enable_pullup(7) raises ValueError). They engage only at 3.3 V. |
firmware | The firmware path from --benchpod-firmware, for tests that flash a real target (skips without it). |
benchpod_sensor | A BenchPod that disarms any emulated I2C sensor at teardown. |
benchpod_dac | A BenchPod that stops any running DAC output (generate / replay) at teardown. |
benchpod_capabilities | The connected pod's resolved Capabilities (ADC scaling, DAC replay depth, feature flags). |
benchpod_waveforms | The cloud waveform library, deleting anything the test saved at teardown. Needs server access (a cloud connection or an API key) and skips otherwise. |
build_report | Opt-in reporting of the run as a GitHub-sourced build on embeddedci.com (upload the tested firmware, record the wiring). Active only inside GitHub Actions; a no-op elsewhere. |
benchpod_connection | The resolved connection string (skips if none is set). |
Markers: @pytest.mark.hardware labels a test that needs a real pod, and @pytest.mark.benchpod_capability("name") skips it unless the connected pod advertises that capability (e.g. dac_deep_replay).
Options
| Option / env | Default | Purpose |
|---|---|---|
--benchpod-connection / BENCHPOD_CONNECTION | — | Which pod to drive (see connection strings below). |
--benchpod-la-voltage / BENCHPOD_LA_VOLTAGE | — | Override the LA I/O-bank voltage (1.8 or 3.3) for one run, e.g. a CI job for a 1V8 board variant. Normally set once in conftest.py via the benchpod_la_voltage fixture. Precedence: flag → fixture → env var. |
--benchpod-firmware | — | Firmware image for the firmware fixture. |
--benchpod-wiring / BENCHPOD_WIRING | — | The pod's wiring profile as a .json or .toml file, which becomes benchpod.wiring, and wins over the profile stored on embeddedci.com. Without it, an embeddedci: connection, and a signed-in LAN run on a pod on your account, read the pod's profile from the server. |
BENCHPOD_WIRING_SOURCE | auto | local skips the profile stored on embeddedci.com and uses only the wiring file or the defaults (see One wiring profile). |
--benchpod-efuse | the profile's rail | Target-power rail: 1 = Internal 5 V (eFuse 1), 2 = External (eFuse 2). Without it, benchpod_target uses the wiring profile's rail (1 unless the profile says 2). |
--benchpod-api-key / BENCHPOD_API_KEY | — | API key (eci_…). Authenticates the embeddedci: destination from anywhere, and unlocks the cloud waveform library on a LAN/USB connection. |
--benchpod-api-base / BENCHPOD_API_BASE | https://www.embeddedci.com | EmbeddedCI server base URL. |
--benchpod-lease-wait | 600 | Seconds to wait for a busy cloud pod before failing with DeviceBusyError. |
--benchpod-no-lease | off | Skip the exclusive cloud lease on the pod (only when nothing else uses the pod). |
--benchpod-discover | off | With no connection set, find one pod on the LAN via mDNS (needs [discovery]). |
--benchpod-build-target / BENCHPOD_BUILD_TARGET | — | Platform id recorded by the build_report fixture (e.g. stm32f4). |
BENCHPOD_LIFT_DAC_LIMITS | off | 1 clears the pod's DAC output limits for the session and puts the same limits back at the end, even when tests fail. Only with the external output stage the limits protect switched off. |
BENCHPOD_STALL_TIMEOUT | 60 | Seconds with no SWD traffic before a flash attempt is aborted and retried. |
Connection strings
| Form | Transport |
|---|---|
192.168.1.213 or host:8080 | wifi/network (JSON over TCP, port 8080 default) |
/dev/ttyACM0, COM3 | USB console (CDC-ACM), explicit device path |
usb | USB console, auto-detected by probing the ports |
discover | find a single pod on the LAN via mDNS (needs the [discovery] extra) |
embeddedci:<name> | cloud: drive a pod by name through embeddedci.com (needs the [cloud] extra). Authenticates with an API key (BENCHPOD_API_KEY) from anywhere, or with the job's OIDC token inside GitHub Actions |
Resolution order: --benchpod-connection → the benchpod_connection ini option → the BENCHPOD_CONNECTION env var → --benchpod-discover. The CLI's saved connection only with --benchpod-connection=saved (see One saved connection). A cloud pod is shared, so the SDK holds an exclusive lease on it for the session; a run that finds it busy queues for up to --benchpod-lease-wait seconds.
Wiring and connection defaults
The SDK, the MCP server and the CLI read the same wiring profile and the same saved connection, so a pod you set up once behaves the same from each of them.
Both need embeddedci 2.10 or newer.
One wiring profile
bp.wiring is resolved once, when the session connects. The first match wins:
- the
wiring=argument (in pytest:--benchpod-wiring, then an overriddenbenchpod_wiringfixture); - the file named by
BENCHPOD_WIRING; - the profile stored on embeddedci.com for this pod, the one the Wiring tab of the pod's workspace shows. For
embeddedci:<name>that is the named pod; over the LAN it is the pod the pod itself reports it is registered as. It needs an API key (api_key=orBENCHPOD_API_KEY) or thebenchpod loginsession, with the pod on the same account.bp.wiring.sourceis thenserver; - the defaults.
Step 3 never fails the connection: when it cannot read the profile (on the LAN it gives up after about 3 seconds) it logs why and the defaults apply. Over USB the stored profile is not used, so pass the wiring yourself there. BenchPod(..., wiring_source="local") (the default is "auto") or BENCHPOD_WIRING_SOURCE=local skips step 3.
One saved connection
BenchPod() picks its connection from the argument, then BENCHPOD_CONNECTION, then the default connection the CLI saved (benchpod set-connection or benchpod discover --save) in ~/.config/benchpod-cli/config.json (or $XDG_CONFIG_HOME/benchpod-cli/config.json). The connection string saved names that default explicitly. Older releases raised ConnectionConfigError when no connection was given.
The pytest plugin never uses the saved connection on its own, so a test run only drives a pod you named: it takes --benchpod-connection, then the benchpod_connection ini option, then BENCHPOD_CONNECTION, then --benchpod-discover. Opt in with --benchpod-connection=saved; the hardware tests skip with a reason when nothing is saved.
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 (i2c_sensor_capture), so you can assert what the firmware actually did on the wire (see Emulating an I2C Sensor).
from embeddedci.benchpod import BenchPod, INTERNAL, PIN1, PIN2, PIN5, PIN6, PIN11, PIN12, Sensor
with BenchPod("192.168.1.213", la_voltage=3.3) as bp: # board I/O voltage; 1.8 for a 1V8 board
bp.flash(file="app.elf", target="target/stm32f4x.cfg",
swclk=PIN11, swdio=PIN12, target_power=INTERNAL)
# I2C is open-drain: enable the pod's pull-ups on SDA/SCL. Only LA1-LA6 have
# pull-ups (LA7/LA8 are pull-downs), so the I2C lines go on LA1/LA2.
bp.enable_pullup(PIN1, PIN2)
bp.enable_i2c_sensor(Sensor.BMP280, sda=PIN1, scl=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=PIN5, tx=PIN6,
delay=1.5, duration=6.0, until=r"APP_OK")
assert cap.match("APP_OK")The rest of the SDK
- Power & reset:
power_on(efuse, delay=1.5)(scheduled pod-side),target_status()(eFuse enabled / tripped),power_status()(rail volts and amps),reset_target(pulse=0.1). - UART:
capture_uart(...)for a fixed window,open_uart(...)for a live two-way session,power_cycle_and_capture(...)for a boot banner. - Logic analyzer:
capture_la(n, sample_rate_hz=...)returns anLaCaptureyou can.decode("i2c" | "uart" | "spi", ...);la_step(la, steps=, delay=)emits step/dir pulse trains. - Analog: named paths (
analog_path("cal1")),adc_read("ext").voltage,capture_adc(...)with calibrated volts,calibrate()to store the offset of the 4-20 mA input on the pod (J8 disconnected),current_out(0.012)to hold 12 mA on the 4-20 mA output (J9, which needs an external floating loop supply),dac_output("5v", volts=2.5),generate("sine", freq_hz=, amplitude=),replay(capture, fault=Fault(...)), andcapture_correlated(...)for ADC + LA from one hardware trigger. See Recording and Replaying Analog Signals. - Sensors and GPS:
enable_i2c_sensor(Sensor.BME280, humidity_pct=55)emulates a BMP280, BME280, SHT4x or MPU-6050 (i2c_sensor_types()lists each model's values);enable_gps("gps_rx", latitude_deg=..., speed_kmh=...)plays a GPS receiver printing NMEA on a second UART, andset_gps(...)moves the fix. - GPIO:
bp.gpio("READY", "input")orbp.signal("READY")claims a channel by number or wiring name and returns a pin you canread,set,wait_forandpulse;release_gpio(...)frees it.enable_pullup(...)engages the bias resistors. - SPI:
open_spi(hz=6_000_000)returns an SPI master session on four LA channels (transfers,flash_id(),flash_program(...));spi_flash("fw.bin")erases, writes and verifies an SPI NOR flash in one call. - CAN:
open_can(bitrate=500_000, mode=...)returns aCanBuswithwrite,expect, and firmware-side auto-responders for ECU simulation. - Control loop:
control_loop(curve=...)runs a tabulated transfer function in the FPGA, e.g. a solar-panel I-V curve.
from embeddedci.benchpod import BenchPod
with BenchPod("192.168.1.213", la_voltage=3.3) as bp: # board I/O voltage; 1.8 for a 1V8 board
print(bp.adc_read("ext").voltage) # one calibrated reading on the front SMA
cap = bp.capture_adc(8192, sample_rate_hz=100_000) # Capture: .volts, .mean(), .peak_to_peak()
with bp.replay(cap, dac_path="5v"): # loops on the DAC until the block exits
la = bp.capture_la(8192, sample_rate_hz=1_000_000)
with bp.open_can(bitrate=500_000, mode="internal") as can: # loopback self-test, no bus needed
can.write(0x123, [1, 2, 3])
assert can.expect(can_id=0x123, timeout=1.0).data == bytes([1, 2, 3])Everything exported from embeddedci.benchpod is covered by semantic versioning: within 2.x, names and signatures only grow. bp.command(...), bp.transport and bp.lowlevel are escape hatches outside that promise.
Errors
Invalid arguments raise ValueError before anything is sent. Everything else raises a BenchPodError subclass, all importable from embeddedci.benchpod. The refusal classes (PodLockedError, PodBusyError, PodLeasedError, PermissionDeniedError) also subclass what a refusal raised before them, so older except FirmwareError or except DeviceBusyError handlers still catch them.
| Exception | Raised when | Attributes |
|---|---|---|
BenchPodError | Base class of everything below. | |
ConnectionConfigError | No connection configured, an unparsable string, or discovery found zero or several pods. | |
TransportError | The pod or tunnel could not be reached or talked to. | |
TransportTimeout | The pod or tunnel did not answer in time (a TransportError and a TimeoutError). | |
ConnectionClosedError | The connection ended or reset before the reply (a TransportError). | |
FirmwareError | The pod replied with an error, e.g. "la voltage not set". | firmware_message, cmd |
PodLockedError | The pod's LAN policy keeps this command for the cloud or the USB console (a FirmwareError). | firmware_message, cmd |
PodBusyError | The pod is busy ("busy: …"), e.g. a capture or upload is running (a FirmwareError). | firmware_message, cmd |
PodLeasedError | A cloud job holds the pod: its refusal on the LAN, or the server's HTTP 409 (a PodBusyError, DeviceBusyError and TransportError). | holder, left_s, expires_at, status |
PermissionDeniedError | The pod's "forbidden: …" on a cloud tunnel, or a server HTTP 403 (a FirmwareError and TransportError). | status |
FlashError | OpenOCD failed, is missing, or lacks the TCP backend. | |
TargetUnreachableError | The probe worked but no target answered on SWD (a FlashError). | |
DeviceBusyError | A cloud pod's lease was not granted within lease_wait. | |
CloudAuthError | No session token: API key rejected, OIDC unavailable, or the exchange failed. | |
ServerApiError | An embeddedci server API call failed. | status |
ServerPermissionDeniedError | The server answered HTTP 403 to a ServerApi call (a ServerApiError and PermissionDeniedError). | status |
UartTimeout | UartSession.expect timed out. | text |
UartLinkError | The UART link died with an error while a UartSession waited or wrote (a UartTimeout and ConnectionClosedError). | text, cause |
UnsupportedFeatureError | The pod lacks the feature: its capabilities say so (raised before anything is sent), it answers "unknown cmd", or the digital-only board has no analog front end (a FirmwareError). | feature, firmware_version |
PinConflictError | An LA channel is already used by another function (a FirmwareError). | la, function |
PullConflictError | An engaged bias resistor can't work with the channel's function (a FirmwareError). | la |
TriggerTimeout | A triggered capture's condition never happened (a FirmwareError). | la, edge |
CanTimeout | CanBus.expect timed out. | frames |
API reference
Every name in embeddedci.benchpod, by area. The SDK README has the signatures, the fields of each type and longer examples.
BenchPod
BenchPod(connection=None, *, la_voltage=None, timeout=30.0, api_key=None, api_base=None, lease=True, lease_wait=600.0, lease_ttl=120, wiring=None, wiring_source="auto", ...) is a context manager. With no connection it uses BENCHPOD_CONNECTION, then the CLI's saved connection. An argument you leave out (a channel, the baud, the power rail, the SWD target) comes from the wiring profile.
| Area | Methods and properties |
|---|---|
| Connection and pod | ping, status, capabilities, refresh_capabilities, supports, close, leased, wiring, save_wiring, signal, fpga_image, usb_cc |
| Feature checks | can_gpio, can_trigger, can_spi, can_profile_power, can_calibrate, can_current_out, can_reset_target, can_analog |
| LA voltage | set_la_voltage, get_la_voltage |
| Power and reset | power_on, power_off, target_power, target_status, power_status, measure_power, power_profile, reset_target, set_reset, reset_state |
| Flashing and SPI | flash, spi_flash, open_spi, open_motor_emulator |
| UART | capture_uart, power_cycle_and_capture, open_uart |
| Bias resistors | enable_pullup, disable_pullup, enable_pulldown, disable_pulldown, set_pull, pull_state, enabled_pulls |
| GPIO and pins | gpio, gpio_pins, configure_gpio, set_gpio, read_gpio, wait_for_level, pin_levels, la_pins, release_gpio, la_step |
| I2C sensor | enable_i2c_sensor, set_i2c_sensor, disable_i2c_sensor, i2c_sensor_status, i2c_sensor_types, i2c_sensor_regs, i2c_sensor_capture |
| GPS receiver | enable_gps, set_gps, disable_gps, gps_status |
| Analog | analog_path, adc_read, dac_output, current_out, current_out_range, calibrate, calibration, clear_calibration |
| Captures | capture_adc, capture_la, capture_correlated, decode |
| DAC | generate, replay, replay_waveform, dac_stop, control_loop, loop_input, loop_probe |
| CAN | open_can, can_config, can_disable, can_status, can_term, can_write, can_read, can_respond, can_respond_clear |
| Cloud library and server | waveforms, save_capture_as_recording, server_api |
| Escape hatches (outside the stability promise) | command, transport, lowlevel |
Types
Results are frozen dataclasses that keep the firmware's reply in raw; sessions such as UartSession, SpiSession and CanBus are context managers.
| Area | Types |
|---|---|
| Power and reset | LaVoltage, TargetStatus, EfuseState, PowerStatus, RailPower, PowerProfile, PowerProfileSession, ResetState, UsbCcStatus |
| Pins and GPIO | PullState, LaPinState, GpioPin, Pin, Efuse |
| Flashing and SPI | FlashResult, SpiSession, SpiFlashInfo, SpiFlashResult, SpiStreamResult |
| UART and I2C | UartCapture, UartSession, Sensor, I2CTransaction, I2CMessage, I2CByte, UartFrame, SpiFrame |
| Analog and captures | AnalogPathState, AdcReading, DacOutput, CurrentOutput, Calibration, Capture, LaCapture, CorrelatedCapture, Trigger |
| DAC and control loop | DacHandle, ReplayHandle, Fault, Segment, ControlLoopHandle, IVPoint, LoopInputMap, LoopState, FpgaImage, FpgaImageInfo |
| CAN | CanBus, CanFrame, CanReadResult |
| Motor & battery emulator | MotorEmulator, BatteryModel, EmulatorCalibration |
| Wiring, cloud and CI | Wiring, Signal, Waveform, WaveformLibrary, ServerApi, BuildReporter, NoopBuildReporter, DeviceLease, ConnSpec, LowLevel, Capabilities |
| String options (Literal types) | DacPath, DacOutputPath, AnalogPath, AdcSource, CalibrateSource, LoopSource, Waveshape, ReplayMapping, DecodeProtocol, CanMode, FaultType, GpioMode, Edge, TriggerEdge |
Capabilities
bp.capabilities (and the benchpod_capabilities fixture) is parsed from the pod's status, or from the server's cap.* map for a cloud pod. A flag that was not reported keeps its default (False, 0 or ""), so a feature check is if bp.capabilities.spi_master:, and @pytest.mark.benchpod_capability("name") takes the same names. On the pod itself, bp.supports("spi_master") checks any flag, and bp.can_spi, bp.can_trigger, bp.can_analog and the other can_* properties name the common ones (can_config … can_disable stay the CAN bus commands). A call the pod cannot serve raises UnsupportedFeatureError.
| Area | Fields |
|---|---|
| Identity | board, firmware_version, board_rev, la_vccio_mv |
| ADC | adc_bits, adc_fullscale_mv, adc_channels, adc_offset_counts, adc_affine |
| DAC and replay | dac, dac_dc, dac_replay, dac_deep_replay, dac_control_loop, dac_loop_sources, dac_loop_input_map, dac_cotrig, dac_bits, dac_replay_bits, dac_replay_max_samples, dac_fullscale_mv, dac_channels, dac_limits |
| Features | scope, analyzer, serial, tunnel, command, ota, la_pins, gpio_read, capture_trigger, power_profile, capture_b64, nrst_pin, usb_cc, spi_master, spi_stream, calibrate, current_out, can, pod_current, sensor_types, gps, analog |
| Firmware updates and policies | flash_kb, blob_slots, ota_sig, sig_policy, sig_keys, sig_policy_cmd, lan_policy, lan_policy_cmd, tunnel_max_tier, lease_state, cloud_ca, cloud_proxy, ws_auth_v2 |
| Boot health | safe_mode, safe_reason, last_crash, reset_cause |
| Source | raw |
ServerApi
A thin client for the embeddedci server's HTTP API, for scripts that need the server rather than a pod. bp.server_api returns one bound to the connection's credentials, or build one with ServerApi(api_key=..., api_base=...). Failures raise ServerApiError.
| Method | Purpose |
|---|---|
list_devices() | The pods the credentials can see. |
resolve_device_id(name) | A pod name to its server id (raises when unknown). |
device_parameters(name_or_id) | The server's cap.* map for a pod; Capabilities.from_parameters parses it. |
wiring_profile(device_id) | The stored wiring profile, with the defaults and any warnings. |
put_wiring(device_id, wiring) | Store a wiring profile (the server validates it; errors name the fields). |
scope_capture_start(device_id, *, samples=256, sample_rate_mhz=1.0) | Start a server-side ADC capture; returns its id. |
dual_capture_start(device_id, *, adc_samples, adc_rate_mhz, la_samples, la_rate_mhz) | Start a server-side ADC + LA capture; returns its id. |
capture_snapshot(capture_id) | Fetch a server-side capture. |
replay_start(payload) | Arm a server-side DAC replay. |
build_artifact(*, build="", repo="", ref="", artifact="") | Resolve an uploaded build to the file a flash would use (job_id, pack_id, name, repo, ref, sha). |
download_build_artifact(art) | Download that file's bytes. |
flash_build(device_id, body, *, timeout=360.0) | Have embeddedci.com flash an uploaded build through a cloud pod. |
request(method, path, *, query=None, json_body=None, ...) | Any other endpoint; returns (status, body). |
Functions, constants and modules
parse_connection(raw)andresolve_connection(connection=None)turn a connection string (orBENCHPOD_CONNECTION) into aConnSpec.make_build_reporter(...)returns thebuild_reportfixture's reporter outside pytest.- Control-loop curves:
build_panel_curve,build_linear_curve,build_constant_curve,curve_output_at,input_percent_to_codeandencode_curve_b64url. - Constants:
INTERNAL,EXTERNAL,PIN1…PIN14,BMP280_ADDR_PRIMARY,BMP280_ADDR_SECONDARY. - Submodules:
decode(off-device decoders),i2c,can,control_loop,motor_emulator(the emulator's registers) anddsp(the record-to-replay processing the server applies).
OpenHTF
The embeddedci-openhtf package wraps the same SDK as an OpenHTF plug and ready-made phases, for stations that want OpenHTF's sequencing, limits and records. It talks to a pod directly over TCP or serial (no cloud account), and benchpod_plug(..., persistent=True) keeps one connection open across DUTs (persistent plugs with the same connection settings share it).
pip install embeddedci-openhtf # pulls in embeddedci + openhtf
import openhtf as htf
from embeddedci_openhtf import benchpod_plug, boot_banner_phase, flash_phase, power_phase
bench = benchpod_plug("192.168.1.50:8080", la_voltage=3.3) # or "/dev/ttyACM0"
test = htf.Test(
power_phase(bench, on=True),
flash_phase(bench, file="fw.elf", target="target/stm32f4x.cfg",
swclk=11, swdio=12, nreset=True), # records flash_ok, attaches openocd.log
boot_banner_phase(bench, rx=1, tx=2, expect="APP_OK", duration=5.0),
)
test.execute(test_start=lambda: "SN-0001")| Area | Names |
|---|---|
| Plug | BenchPodPlug, benchpod_plug, close_persistent_benchpods |
| Phase factories | power_phase, flash_phase, boot_banner_phase, signal_generate_phase, dac_output_phase, current_out_phase, current_in_phase, adc_read_phase, adc_capture_phase, loopback_measure_phase, control_loop_phase, dac_replay_phase, gpio_phase, measure_power_phase, la_delay_phase |
| Pin and power helpers | gpio, set_gpio, read_gpio, release_gpio, measure_power, la_delay |
| Analog helpers | signal_generate, signal_stop, analog_path, dac_output, current_out, adc_read, adc_capture, replay, replay_waveform, control_loop, fpga_image |
| Measurements | flash_ok_measurement, uart_matched_measurement, record_flash, record_uart, record_samples |