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

FixtureWhat it gives you
benchpodA 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_voltageThe 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_wiringThe 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_targetA 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_pinsThe 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.
firmwareThe firmware path from --benchpod-firmware, for tests that flash a real target (skips without it).
benchpod_sensorA BenchPod that disarms any emulated I2C sensor at teardown.
benchpod_dacA BenchPod that stops any running DAC output (generate / replay) at teardown.
benchpod_capabilitiesThe connected pod's resolved Capabilities (ADC scaling, DAC replay depth, feature flags).
benchpod_waveformsThe cloud waveform library, deleting anything the test saved at teardown. Needs server access (a cloud connection or an API key) and skips otherwise.
build_reportOpt-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_connectionThe 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 / envDefaultPurpose
--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_SOURCEautolocal skips the profile stored on embeddedci.com and uses only the wiring file or the defaults (see One wiring profile).
--benchpod-efusethe profile's railTarget-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_BASEhttps://www.embeddedci.comEmbeddedCI server base URL.
--benchpod-lease-wait600Seconds to wait for a busy cloud pod before failing with DeviceBusyError.
--benchpod-no-leaseoffSkip the exclusive cloud lease on the pod (only when nothing else uses the pod).
--benchpod-discoveroffWith 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_LIMITSoff1 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_TIMEOUT60Seconds with no SWD traffic before a flash attempt is aborted and retried.

Connection strings

FormTransport
192.168.1.213 or host:8080wifi/network (JSON over TCP, port 8080 default)
/dev/ttyACM0, COM3USB console (CDC-ACM), explicit device path
usbUSB console, auto-detected by probing the ports
discoverfind 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:

  1. the wiring= argument (in pytest: --benchpod-wiring, then an overridden benchpod_wiring fixture);
  2. the file named by BENCHPOD_WIRING;
  3. 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= or BENCHPOD_API_KEY) or the benchpod login session, with the pod on the same account. bp.wiring.source is then server;
  4. 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 an LaCapture you 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(...)), and capture_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, and set_gps(...) moves the fix.
  • GPIO: bp.gpio("READY", "input") or bp.signal("READY") claims a channel by number or wiring name and returns a pin you can read, set, wait_for and pulse; 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 a CanBus with write, 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.

ExceptionRaised whenAttributes
BenchPodErrorBase class of everything below.
ConnectionConfigErrorNo connection configured, an unparsable string, or discovery found zero or several pods.
TransportErrorThe pod or tunnel could not be reached or talked to.
TransportTimeoutThe pod or tunnel did not answer in time (a TransportError and a TimeoutError).
ConnectionClosedErrorThe connection ended or reset before the reply (a TransportError).
FirmwareErrorThe pod replied with an error, e.g. "la voltage not set".firmware_message, cmd
PodLockedErrorThe pod's LAN policy keeps this command for the cloud or the USB console (a FirmwareError).firmware_message, cmd
PodBusyErrorThe pod is busy ("busy: …"), e.g. a capture or upload is running (a FirmwareError).firmware_message, cmd
PodLeasedErrorA 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
PermissionDeniedErrorThe pod's "forbidden: …" on a cloud tunnel, or a server HTTP 403 (a FirmwareError and TransportError).status
FlashErrorOpenOCD failed, is missing, or lacks the TCP backend.
TargetUnreachableErrorThe probe worked but no target answered on SWD (a FlashError).
DeviceBusyErrorA cloud pod's lease was not granted within lease_wait.
CloudAuthErrorNo session token: API key rejected, OIDC unavailable, or the exchange failed.
ServerApiErrorAn embeddedci server API call failed.status
ServerPermissionDeniedErrorThe server answered HTTP 403 to a ServerApi call (a ServerApiError and PermissionDeniedError).status
UartTimeoutUartSession.expect timed out.text
UartLinkErrorThe UART link died with an error while a UartSession waited or wrote (a UartTimeout and ConnectionClosedError).text, cause
UnsupportedFeatureErrorThe 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
PinConflictErrorAn LA channel is already used by another function (a FirmwareError).la, function
PullConflictErrorAn engaged bias resistor can't work with the channel's function (a FirmwareError).la
TriggerTimeoutA triggered capture's condition never happened (a FirmwareError).la, edge
CanTimeoutCanBus.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.

AreaMethods and properties
Connection and podping, status, capabilities, refresh_capabilities, supports, close, leased, wiring, save_wiring, signal, fpga_image, usb_cc
Feature checkscan_gpio, can_trigger, can_spi, can_profile_power, can_calibrate, can_current_out, can_reset_target, can_analog
LA voltageset_la_voltage, get_la_voltage
Power and resetpower_on, power_off, target_power, target_status, power_status, measure_power, power_profile, reset_target, set_reset, reset_state
Flashing and SPIflash, spi_flash, open_spi, open_motor_emulator
UARTcapture_uart, power_cycle_and_capture, open_uart
Bias resistorsenable_pullup, disable_pullup, enable_pulldown, disable_pulldown, set_pull, pull_state, enabled_pulls
GPIO and pinsgpio, gpio_pins, configure_gpio, set_gpio, read_gpio, wait_for_level, pin_levels, la_pins, release_gpio, la_step
I2C sensorenable_i2c_sensor, set_i2c_sensor, disable_i2c_sensor, i2c_sensor_status, i2c_sensor_types, i2c_sensor_regs, i2c_sensor_capture
GPS receiverenable_gps, set_gps, disable_gps, gps_status
Analoganalog_path, adc_read, dac_output, current_out, current_out_range, calibrate, calibration, clear_calibration
Capturescapture_adc, capture_la, capture_correlated, decode
DACgenerate, replay, replay_waveform, dac_stop, control_loop, loop_input, loop_probe
CANopen_can, can_config, can_disable, can_status, can_term, can_write, can_read, can_respond, can_respond_clear
Cloud library and serverwaveforms, 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.

AreaTypes
Power and resetLaVoltage, TargetStatus, EfuseState, PowerStatus, RailPower, PowerProfile, PowerProfileSession, ResetState, UsbCcStatus
Pins and GPIOPullState, LaPinState, GpioPin, Pin, Efuse
Flashing and SPIFlashResult, SpiSession, SpiFlashInfo, SpiFlashResult, SpiStreamResult
UART and I2CUartCapture, UartSession, Sensor, I2CTransaction, I2CMessage, I2CByte, UartFrame, SpiFrame
Analog and capturesAnalogPathState, AdcReading, DacOutput, CurrentOutput, Calibration, Capture, LaCapture, CorrelatedCapture, Trigger
DAC and control loopDacHandle, ReplayHandle, Fault, Segment, ControlLoopHandle, IVPoint, LoopInputMap, LoopState, FpgaImage, FpgaImageInfo
CANCanBus, CanFrame, CanReadResult
Motor & battery emulatorMotorEmulator, BatteryModel, EmulatorCalibration
Wiring, cloud and CIWiring, 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.

AreaFields
Identityboard, firmware_version, board_rev, la_vccio_mv
ADCadc_bits, adc_fullscale_mv, adc_channels, adc_offset_counts, adc_affine
DAC and replaydac, 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
Featuresscope, 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 policiesflash_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 healthsafe_mode, safe_reason, last_crash, reset_cause
Sourceraw

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.

MethodPurpose
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) and resolve_connection(connection=None) turn a connection string (or BENCHPOD_CONNECTION) into a ConnSpec.
  • make_build_reporter(...) returns the build_report fixture's reporter outside pytest.
  • Control-loop curves: build_panel_curve, build_linear_curve, build_constant_curve, curve_output_at, input_percent_to_code and encode_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) and dsp (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")
AreaNames
PlugBenchPodPlug, benchpod_plug, close_persistent_benchpods
Phase factoriespower_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 helpersgpio, set_gpio, read_gpio, release_gpio, measure_power, la_delay
Analog helperssignal_generate, signal_stop, analog_path, dac_output, current_out, adc_read, adc_capture, replay, replay_waveform, control_loop, fpga_image
Measurementsflash_ok_measurement, uart_matched_measurement, record_flash, record_uart, record_samples