embeddedci 2.7.0__tar.gz → 2.8.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {embeddedci-2.7.0 → embeddedci-2.8.0}/CHANGELOG.md +22 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/PKG-INFO +183 -4
- {embeddedci-2.7.0 → embeddedci-2.8.0}/README.md +182 -3
- {embeddedci-2.7.0 → embeddedci-2.8.0}/pyproject.toml +1 -1
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/__init__.py +2 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/client.py +55 -4
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/errors.py +29 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/pytest_plugin.py +19 -10
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/transport/cloud.py +16 -8
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/transport/tcp.py +13 -9
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/api_surface.json +13 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_cloud.py +62 -3
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_dac_limits_fixture.py +39 -0
- embeddedci-2.8.0/tests/test_docs_coverage.py +166 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_transport_failures.py +4 -4
- embeddedci-2.8.0/tests/test_unsupported_feature.py +93 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/.gitignore +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/LICENSE +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/docs/event-uart-design.md +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/examples/README.md +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/examples/test_bmp280.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/__init__.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/can.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/capabilities.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/capture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/ci.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/cloud_auth.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/connection.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/constants.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/control_loop.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/decode.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/discovery.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/dsp.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/flash.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/gpio.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/i2c.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/lease.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/lowlevel.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/motor_emulator.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/power.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/protocol.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/replay.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/results.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/sensor.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/server_api.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/spi.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/state.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/transport/__init__.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/transport/base.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/transport/serial.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/uart.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/waveforms.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/benchpod/wiring.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/py.typed +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/src/embeddedci/upload_build.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/conftest.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/README.md +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/conftest.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/e2e_helpers.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/test_e2e_cloud.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/test_e2e_dut.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/test_e2e_gpio.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/test_e2e_la13_14.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/test_e2e_pod.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/test_e2e_rev3.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/test_e2e_spi.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/e2e/test_e2e_usb.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/examples/test_bmp280_hil.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/examples/test_can_ecu_demo.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/examples/test_can_loopback_hil.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/examples/test_smoke.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_adc_fft_hw.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_api_surface.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_can.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_capabilities.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_capture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_capture_replay_hw.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_ci.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_client_v2.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_connection.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_control_loop.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_control_loop_hw.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_current_out_waveforms.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_dac_path_ranges.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_decode.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_decode_parity.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_dsp.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_flash_bridge.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_gpio.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_i2c_decode.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_i2c_parity.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_image_switching.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_integration.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_la_voltage_fixture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_lease.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_motor_emulator.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_pins_bias.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_protocol.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_refusal_errors.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_replay.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_sensor.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_serial_autodetect.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_serial_json.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_serial_text_console.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_spi.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_tcp_transport.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_timed_power.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_timing.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_trigger_power.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_uart_capture.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_uart_session.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_upload_build.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_waveforms.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/test_wiring.py +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/testdata/i2c_decode_vectors.json +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/testdata/spi_decode_vectors.json +0 -0
- {embeddedci-2.7.0 → embeddedci-2.8.0}/tests/testdata/uart_decode_vectors.json +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog — `embeddedci`
|
|
2
2
|
|
|
3
|
+
## 2.8.0
|
|
4
|
+
|
|
5
|
+
- `UnsupportedFeatureError` (with `feature` and `firmware_version`): the pod cannot do what was
|
|
6
|
+
asked. Raised before anything is sent when the pod's capabilities lack the feature (it used to
|
|
7
|
+
be a plain `BenchPodError`), and for the pod's own `unknown cmd` and the digital board's "no
|
|
8
|
+
analog front end" refusal (they used to be plain `FirmwareError`s). It subclasses
|
|
9
|
+
`FirmwareError`, so existing handlers still catch it.
|
|
10
|
+
- Capability checks on `BenchPod`: `supports(flag)` for any `Capabilities` flag, and the
|
|
11
|
+
properties `can_gpio`, `can_trigger`, `can_spi`, `can_profile_power`, `can_calibrate`,
|
|
12
|
+
`can_current_out`, `can_reset_target` and `can_analog`, e.g. to skip a test on a pod without the
|
|
13
|
+
feature. (`can_config` … `can_disable` remain the CAN bus commands.)
|
|
14
|
+
- pytest plugin: `--benchpod-api-key`, `--benchpod-api-base` and the lease options also reach the
|
|
15
|
+
connection the `BENCHPOD_LIFT_DAC_LIMITS=1` session fixture opens. A cloud run authenticated
|
|
16
|
+
only by the flag used to fail to lift (and restore) the DAC limits there.
|
|
17
|
+
|
|
18
|
+
## 2.7.1
|
|
19
|
+
|
|
20
|
+
- Over the cloud, a UART console (`open_uart`) can stay open while a capture, power profile or
|
|
21
|
+
DAC replay runs, as on the LAN. Each streaming tunnel tells the server what it is for
|
|
22
|
+
(`?kind=uart|capture|dac|flash`). A server without kind support still answers HTTP 409
|
|
23
|
+
("device is busy") for the second tunnel, as before.
|
|
24
|
+
|
|
3
25
|
## 2.7.0
|
|
4
26
|
|
|
5
27
|
- `UartSession.write` on a link that is gone (a reset connection, an unplugged pod, a closed
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: embeddedci
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.8.0
|
|
4
4
|
Summary: BenchPod SDK and pytest plugin for EmbeddedCI hardware-in-the-loop testing
|
|
5
5
|
Project-URL: Homepage, https://embeddedci.com
|
|
6
6
|
Project-URL: Repository, https://github.com/embeddedci-com/embeddedci-python
|
|
@@ -73,7 +73,8 @@ No pin numbers: the SWD channels, the UART pair, the baud and the power rail com
|
|
|
73
73
|
[Analog paths](#analog-paths-dc-output-and-single-readings) · [Captures](#captures) ·
|
|
74
74
|
[DAC](#dac-generator-replay-and-faults) · [Control loop](#in-fabric-dac-control-loop) ·
|
|
75
75
|
[CAN](#can) · [Cloud](#cloud-embeddedcidevice-name) · [Build reporting](#build-reporting) ·
|
|
76
|
-
[Errors](#errors) · [Escape hatches](#escape-hatches) · [
|
|
76
|
+
[Errors](#errors) · [Escape hatches](#escape-hatches) · [API reference](#api-reference) ·
|
|
77
|
+
[Releasing](#releasing-maintainers)
|
|
77
78
|
|
|
78
79
|
## Install
|
|
79
80
|
|
|
@@ -232,6 +233,8 @@ automatically.)
|
|
|
232
233
|
| `BENCHPOD_API_KEY` | no `api_key` argument / `--benchpod-api-key` | EmbeddedCI API key (`eci_…`) for the cloud destination and server features |
|
|
233
234
|
| `BENCHPOD_API_BASE` | no `api_base` argument / `--benchpod-api-base` | server base URL (default `https://www.embeddedci.com`) |
|
|
234
235
|
| `BENCHPOD_BUILD_TARGET` | no `--benchpod-build-target` | platform id recorded by the `build_report` fixture |
|
|
236
|
+
| `BENCHPOD_WIRING` | no `wiring` argument / `--benchpod-wiring` / `benchpod_wiring` fixture | wiring profile file (`.json`/`.toml`), see [Wiring profile](#wiring-profile) |
|
|
237
|
+
| `BENCHPOD_LIFT_DAC_LIMITS` | pytest sessions | `1` clears the pod's DAC output limits for the session and restores the same limits at the end, even when tests fail (only with the output stage switched off) |
|
|
235
238
|
| `BENCHPOD_STALL_TIMEOUT` | flashing | seconds with no SWD traffic before a flash attempt is aborted and retried (default 60) |
|
|
236
239
|
|
|
237
240
|
## API conventions and stability
|
|
@@ -261,6 +264,24 @@ automatically.)
|
|
|
261
264
|
Plain ints work too (`efuse=1`, `swclk=11`); they are validated. The target's reset line is not an
|
|
262
265
|
LA channel — it has a dedicated pin (see [Flashing](#flashing)).
|
|
263
266
|
|
|
267
|
+
**Feature checks.** Pods differ by board, firmware and gateware. `bp.supports("spi_master")` says
|
|
268
|
+
whether the pod has any `Capabilities` flag (`False` for a flag this SDK does not know), and the
|
|
269
|
+
properties `can_gpio`, `can_trigger`, `can_spi`, `can_profile_power`, `can_calibrate`,
|
|
270
|
+
`can_current_out`, `can_reset_target` and `can_analog` name the common ones (`can_config` …
|
|
271
|
+
`can_disable` stay the CAN bus commands). A call the pod cannot serve raises
|
|
272
|
+
`UnsupportedFeatureError` (see [Errors](#errors)), before anything is sent when the capabilities
|
|
273
|
+
already say so:
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
import pytest
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def test_flash_spi_nor(benchpod):
|
|
280
|
+
if not benchpod.can_spi:
|
|
281
|
+
pytest.skip("this pod has no SPI master")
|
|
282
|
+
benchpod.spi_flash("fw.bin")
|
|
283
|
+
```
|
|
284
|
+
|
|
264
285
|
**Stability promise.** Everything in `embeddedci.benchpod.__all__` follows semantic versioning:
|
|
265
286
|
within 2.x names and signatures only grow. `BenchPod.command()`, `BenchPod.transport` and
|
|
266
287
|
`BenchPod.lowlevel` are escape hatches **outside** the promise. Migrating from 0.x/1.x? The
|
|
@@ -343,7 +364,7 @@ The `benchpod` fixture is a `BenchPod` instance, not the module — import const
|
|
|
343
364
|
| `--benchpod-efuse` | — | the profile's rail | target-power rail for `benchpod_target` and `pins.efuse` (1 internal, 2 external); `benchpod_target` otherwise follows the wiring profile, `pins.efuse` is 1 |
|
|
344
365
|
| `--benchpod-firmware` | — | — | firmware image for the `firmware` fixture |
|
|
345
366
|
| `--benchpod-discover` | — | off | when no connection is configured, find one pod via mDNS (needs `[discovery]`) |
|
|
346
|
-
| `--benchpod-api-key` | `BENCHPOD_API_KEY` | — | API key for the cloud destination and the waveform library |
|
|
367
|
+
| `--benchpod-api-key` | `BENCHPOD_API_KEY` | — | API key for the cloud destination and the waveform library; it, `--benchpod-api-base` and the lease options also reach the short connection that lifts the DAC limits (`BENCHPOD_LIFT_DAC_LIMITS=1`) |
|
|
347
368
|
| `--benchpod-api-base` | `BENCHPOD_API_BASE` | `https://www.embeddedci.com` | embeddedci server |
|
|
348
369
|
| `--benchpod-lease-wait` | — | `600` | seconds to wait for a busy cloud device |
|
|
349
370
|
| `--benchpod-no-lease` | — | off | don't lock the cloud device (only when nothing else can use it) |
|
|
@@ -378,6 +399,13 @@ benchpod_connection = 192.168.1.213
|
|
|
378
399
|
`benchpod` is shared by the whole session, so state a test leaves behind (a running DAC, engaged
|
|
379
400
|
pull-ups, an armed sensor) carries into the next one — use the teardown fixtures.
|
|
380
401
|
|
|
402
|
+
**DAC output limits.** A pod can carry DAC output limits that protect an external output stage (a
|
|
403
|
+
solar simulator module on the 5 V path, say); it then refuses what they forbid. With the stage
|
|
404
|
+
switched off, run the suite with `BENCHPOD_LIFT_DAC_LIMITS=1`: the plugin clears the limits once
|
|
405
|
+
when the session starts and puts the same limits back at the end, even when tests fail. It opens
|
|
406
|
+
its own short connection, so it applies whichever fixture the suite uses. Without the variable
|
|
407
|
+
nothing changes.
|
|
408
|
+
|
|
381
409
|
### Markers
|
|
382
410
|
|
|
383
411
|
* `@pytest.mark.hardware` — labels tests that need a real pod, for selection
|
|
@@ -657,7 +685,7 @@ won't engage one while the LA bank is at 1.8 V (`PullState.available` is false).
|
|
|
657
685
|
| LA3, LA4 | pull-up | 2.2k |
|
|
658
686
|
| LA5, LA6 | pull-up | 10k |
|
|
659
687
|
| LA7, LA8 | pull-**down** | 10k |
|
|
660
|
-
| LA9–
|
|
688
|
+
| LA9–LA14 | none | — |
|
|
661
689
|
|
|
662
690
|
```python
|
|
663
691
|
from embeddedci.benchpod import PIN1, PIN2, PIN7
|
|
@@ -1332,6 +1360,7 @@ Invalid arguments raise `ValueError` before anything is sent. Everything else ra
|
|
|
1332
1360
|
| `PodBusyError` (a `FirmwareError`) | the pod is busy (`busy: …`), e.g. a capture or upload is running | `firmware_message`, `cmd` |
|
|
1333
1361
|
| `PodLeasedError` (a `PodBusyError`, `DeviceBusyError` and `TransportError`) | a cloud job holds the pod (its refusal on the LAN, or the server's HTTP 409) | `holder`, `left_s`, `expires_at`, `status` |
|
|
1334
1362
|
| `PermissionDeniedError` (a `FirmwareError` and `TransportError`) | the pod's `forbidden: …` on a cloud tunnel, or a server HTTP 403 (`ServerPermissionDeniedError`, also a `ServerApiError`, from `ServerApi`) | `status` |
|
|
1363
|
+
| `UnsupportedFeatureError` (a `FirmwareError`) | 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 | `feature`, `firmware_version`, `firmware_message`, `cmd` |
|
|
1335
1364
|
| `UartTimeout` | `UartSession.expect` timed out | `text` |
|
|
1336
1365
|
| `UartLinkError` (a `UartTimeout` and `ConnectionClosedError`) | the UART link died with an error while `UartSession` waited | `text`, `cause` |
|
|
1337
1366
|
| `PinConflictError` | an LA channel is already used by another function (a `FirmwareError`) | `la`, `function` |
|
|
@@ -1351,6 +1380,156 @@ These sit below the stable API and are **not** covered by the stability promise:
|
|
|
1351
1380
|
`cal_switch_status()`, `dac_set(code, divider=)`. Prefer the named paths — these can leave the
|
|
1352
1381
|
front end in a state no named path describes.
|
|
1353
1382
|
|
|
1383
|
+
## API reference
|
|
1384
|
+
|
|
1385
|
+
Every name in `embeddedci.benchpod.__all__`, in one place. The sections above show how the pieces
|
|
1386
|
+
fit together; this is the index. `tests/test_docs_coverage.py` fails when a public name is missing
|
|
1387
|
+
from this README, so the list stays complete.
|
|
1388
|
+
|
|
1389
|
+
### `BenchPod`
|
|
1390
|
+
|
|
1391
|
+
`BenchPod(connection=None, *, la_voltage=None, timeout=30.0, transport=None, api_base=None,
|
|
1392
|
+
api_key=None, cloud_token=None, cloud_audience=None, cloud_user_token=None, lease=True,
|
|
1393
|
+
lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is idempotent.
|
|
1394
|
+
|
|
1395
|
+
| Area | Methods and properties |
|
|
1396
|
+
|---|---|
|
|
1397
|
+
| Connection and device | `ping()`, `status()`, `capabilities`, `refresh_capabilities()` (drop the cached capabilities and read them again), `close()`, `leased` (true while this client holds the cloud lease), `wiring`, `save_wiring(wiring=None)` (store a cloud device's profile on embeddedci.com), `signal(name)`, `fpga_image(image)`, `usb_cc()` |
|
|
1398
|
+
| LA voltage | `set_la_voltage(volts)`, `get_la_voltage()` |
|
|
1399
|
+
| Power and reset | `power_on(efuse=None, *, delay=None)`, `power_off(efuse=None, *, delay=None)`, `target_power(efuse=None, *, on, delay=None)`, `target_status()`, `power_status()`, `measure_power(duration, *, efuse=None, rate_hz=500.0, keep_samples=0)`, `power_profile(*, efuse=None, rate_hz=500.0, keep_samples=4096, max_duration=60.0)`, `reset_target(*, pulse=0.1)`, `set_reset(asserted)`, `reset_state()` |
|
|
1400
|
+
| Flashing | `flash(...)`, `spi_flash(image, addr=0, *, erase=True, verify=True, hold_reset=False, sck=, mosi=, miso=, cs=)`, `open_spi(...)`, `open_motor_emulator(...)` |
|
|
1401
|
+
| UART | `capture_uart(...)`, `power_cycle_and_capture(...)`, `open_uart(...)` |
|
|
1402
|
+
| Bias resistors | `enable_pullup(*las)`, `disable_pullup(*las)`, `enable_pulldown(*las)`, `disable_pulldown(*las)` (LA7/LA8), `set_pull(la, enabled)`, `pull_state(la)`, `enabled_pulls()` |
|
|
1403
|
+
| GPIO and pins | `gpio(la, mode="output", *, level=None)`, `gpio_pins(las, mode="output", *, level=None)`, `configure_gpio(las, mode="output", *, level=None)`, `set_gpio(la, level)`, `read_gpio(la)`, `wait_for_level(la, level, *, timeout, poll=0.005)` (`False` when `timeout` passes first), `pin_levels()`, `la_pins()`, `release_gpio(*las)`, `la_step(la, *, steps, delay, dir_la=None, direction=0)` |
|
|
1404
|
+
| I2C sensor | `enable_i2c_sensor(...)`, `set_i2c_sensor(...)`, `disable_i2c_sensor()`, `i2c_sensor_status()`, `i2c_sensor_regs()`, `i2c_sensor_capture(...)` |
|
|
1405
|
+
| Analog | `analog_path(path)`, `adc_read(source="ext")`, `dac_output(path, *, volts=None)`, `current_out(current)`, `current_out_range()`, `calibrate(source="current_in")`, `calibration()`, `clear_calibration()` |
|
|
1406
|
+
| Captures | `capture_adc(...)`, `capture_la(...)`, `capture_correlated(...)`, `decode(source, protocol="i2c", ...)` |
|
|
1407
|
+
| DAC | `generate(...)`, `replay(...)`, `replay_waveform(...)`, `dac_stop()`, `control_loop(...)`, `loop_input(...)`, `loop_probe()` |
|
|
1408
|
+
| CAN | `open_can(*, bitrate=500000, mode="normal", term=False, fd=False)`, `can_config(...)`, `can_disable()`, `can_status()`, `can_term(on)`, `can_write(...)`, `can_read(...)`, `can_respond(...)`, `can_respond_clear()` |
|
|
1409
|
+
| Cloud library and server | `waveforms`, `save_capture_as_recording(...)`, `server_api` (the `ServerApi`; needs an API key or a cloud session) |
|
|
1410
|
+
| Escape hatches | `command(cmd)`, `transport`, `lowlevel` (see [Escape hatches](#escape-hatches)) |
|
|
1411
|
+
|
|
1412
|
+
### Types
|
|
1413
|
+
|
|
1414
|
+
Results are frozen dataclasses unless noted. A result built from a firmware reply keeps that reply in
|
|
1415
|
+
`raw` and has a `from_reply()` classmethod (`from_dict()`, `from_json()` or `from_chunks()` for the
|
|
1416
|
+
few built from other shapes); `to_dict()` goes the other way where a type is sent back.
|
|
1417
|
+
|
|
1418
|
+
| Type | Returned by / used for | Fields and members |
|
|
1419
|
+
|---|---|---|
|
|
1420
|
+
| `LaVoltage` | `get_la_voltage`, `set_la_voltage` | `voltage`, `readback`, `is_set` |
|
|
1421
|
+
| `TargetStatus` | `target_status` | `internal`, `external` (each an `EfuseState`), `supported`, `efuse(n)` |
|
|
1422
|
+
| `EfuseState` | `TargetStatus.efuse` | `enabled`, `fault`, `valid` |
|
|
1423
|
+
| `PowerStatus` | `power_status` | `internal`, `external` (each a `RailPower`), `pod`, `total_current`, `rail(n)` |
|
|
1424
|
+
| `RailPower` | `PowerStatus.rail` | `ok`, `bus_voltage`, `current` |
|
|
1425
|
+
| `PowerProfile` | `measure_power`, `PowerProfileSession.result` | see [Power profiles](#power-profiles) |
|
|
1426
|
+
| `PowerProfileSession` | `power_profile` (a context manager) | `start()`, `stop()`, `status()`, `result` |
|
|
1427
|
+
| `ResetState` | `reset_target`, `set_reset`, `reset_state` | `asserted`, `supported` |
|
|
1428
|
+
| `UsbCcStatus` | `usb_cc` | `orientation`, `advertised`, `advertised_current` (A), `cc1_voltage`, `cc2_voltage` (V) |
|
|
1429
|
+
| `PullState` | `pull_state`, `set_pull` | `la`, `enabled`, `direction`, `ohms`, `available` |
|
|
1430
|
+
| `LaPinState` | `la_pins`, `configure_gpio` | `la`, `function`, `gpio`, `level`, `pull`, `pull_ohms`, `pull_on`, `in_use` |
|
|
1431
|
+
| `GpioPin` (class) | `gpio`, `gpio_pins`, `signal` | `name`, `configure()`, `set(level)`, `high()`, `low()`, `activate()`, `deactivate()`, `is_active()`, `read()`, `wait_for(level, *, timeout)`, `pulse(width, *, count=1)`, `state()`, `release()` |
|
|
1432
|
+
| `FlashResult` | `flash` | `ok`, `returncode`, `stdout`, `stderr`, `target_unreachable`, `stalled` |
|
|
1433
|
+
| `SpiSession` (class) | `open_spi` | `flash_id()`, `flash_read(addr, length)`, `flash_erase(addr, length)`, `flash_chip_erase()` (whole chip; seconds taken), `flash_write(addr, data)`, `flash_program(data, addr=0, *, erase=True, verify=True)` (erase, write and verify a whole image), `transfer(data)`, `stream(data, *, head=b"", hold_cs=False)`, `close()` |
|
|
1434
|
+
| `SpiFlashInfo` | `SpiSession.flash_id` | `jedec_id`, `present`, `size`, `status` |
|
|
1435
|
+
| `SpiFlashResult` | `spi_flash`, `SpiSession.flash_program` | `jedec_id`, `addr`, `length`, `erased`, `verified`, `seconds` |
|
|
1436
|
+
| `SpiStreamResult` | `SpiSession.stream` | `sent`, `seconds` |
|
|
1437
|
+
| `UartCapture` | `capture_uart`, `power_cycle_and_capture` | `text`, `lines`, `matched`, `match(pattern)`, `contains(needle)` (also `needle in capture`) |
|
|
1438
|
+
| `UartSession` (class) | `open_uart` (a context manager) | `write(data)`, `read(*, timeout=0.0)`, `read_until(pattern, *, timeout)`, `expect(pattern, *, timeout)`, `text`, `lines`, `closed`, `error`, `close()` |
|
|
1439
|
+
| `Sensor` (enum) | `enable_i2c_sensor` | `Sensor.BMP280` |
|
|
1440
|
+
| `I2CTransaction` | `i2c_sensor_capture`, `decode("i2c")` | `messages`, `complete`, `address`, `to(address)` |
|
|
1441
|
+
| `I2CMessage` | `I2CTransaction.messages` | `address`, `read`, `address_ack`, `data`, `values` |
|
|
1442
|
+
| `I2CByte` | `I2CMessage.data` | `value`, `ack` |
|
|
1443
|
+
| `UartFrame` | `decode("uart")` | `index`, `start_us`, `end_us`, `value`, `hex`, `text`, `ok`, `error` |
|
|
1444
|
+
| `SpiFrame` | `decode("spi")` | `index`, `start_us`, `end_us`, `mosi`, `miso`, `mosi_hex`, `miso_hex` |
|
|
1445
|
+
| `AnalogPathState` | `analog_path` | `path`, `dac_mux_register`, `cal_relay_register` |
|
|
1446
|
+
| `AdcReading` | `adc_read` | `source`, `voltage`, `count`, `span`, `offset`, `current` |
|
|
1447
|
+
| `DacOutput` | `dac_output` | `path`, `voltage`, `code` |
|
|
1448
|
+
| `CurrentOutput` | `current_out`, `current_out_range` | `current`, `code`, `min_current`, `max_current` |
|
|
1449
|
+
| `Calibration` | `calibrate`, `calibration`, `clear_calibration` | `source`, `calibrated`, `offset`, `a`, `b`, `count`, `span`, `samples` |
|
|
1450
|
+
| `Capture` | `capture_adc` | `counts`, `volts`, `currents`, `sample_rate_hz`, `source`, `trigger`, `duration`, `times()`, `mean()`, `min()`, `max()`, `rms()`, `rms_ac()`, `peak_to_peak()`, `first_crossing()`, `crossing_times()`, `fft()`, `dominant_frequency()` |
|
|
1451
|
+
| `LaCapture` | `capture_la` | `words`, `channels`, `sample_rate_hz`, `trigger`, `duration`, `channel(la)`, `level_at()`, `edges()`, `edge_times()`, `first_edge()`, `delay()`, `frequency()`, `duty_cycle()`, `pulse_widths()`, `decode()` |
|
|
1452
|
+
| `CorrelatedCapture` | `capture_correlated` | `adc` (a `Capture`), `la` (a `LaCapture`), sharing one hardware trigger |
|
|
1453
|
+
| `Trigger` | `capture_adc`, `capture_la`, `capture_correlated` | `la`, `edge` |
|
|
1454
|
+
| `DacHandle` (class) | `generate` (a context manager) | `stop()` |
|
|
1455
|
+
| `ReplayHandle` (class) | `replay`, `replay_waveform` (a context manager) | a `DacHandle` that also knows what it replays: `stop()` |
|
|
1456
|
+
| `Fault` | `replay(fault=...)` | `type`, `start`, `width`, `level`, `to_dict()` |
|
|
1457
|
+
| `Segment` | `WaveformLibrary.save_segments` | `shape` (`ramp`, `hold`, `step`), `duration`, `v_start`, `v_end`, `to_dict()` |
|
|
1458
|
+
| `ControlLoopHandle` (class) | `control_loop` (a context manager) | `probe()`, `set_input(...)`, `stop()` |
|
|
1459
|
+
| `IVPoint` | `loop_probe`, `ControlLoopHandle.probe` | `i`, `v`, `input_code`, `source`, `tripped`, `loop_input` |
|
|
1460
|
+
| `LoopInputMap` | `control_loop(input_map=...)` | `mv_per_unit`, `range_min`, `range_max`, `mv_at_zero`, `trip`, `to_request()` |
|
|
1461
|
+
| `LoopState` | `loop_input` | `source`, `input_code`, `step`, `output_code` |
|
|
1462
|
+
| `FpgaImage` (enum) | `fpga_image` | `LOOP`, `DEEP_REPLAY` |
|
|
1463
|
+
| `FpgaImageInfo` | `fpga_image` | `image`, `version`, `features` |
|
|
1464
|
+
| `CanBus` (class) | `open_can` | `write()`, `read()`, `read_until()`, `expect()`, `collect()`, `assert_periodic()`, `add_responder()`, `clear_responders()`, `simulate_ecu()`, `set_term()`, `status()`, `close()` |
|
|
1465
|
+
| `CanFrame` | `CanBus.read`, `CanBus.expect` | `id`, `data`, `ext`, `rtr`, `ts`, `dlc` |
|
|
1466
|
+
| `CanReadResult` | `can_read` | `frames`, `overflow` |
|
|
1467
|
+
| `MotorEmulator` (class) | `open_motor_emulator` | `configure()`, `boards()`, `probe()`, `read()`, `read_signed(reg)`, `write()`, `status()`, `trips()`, `clear_trips()`, `arm()`, `set_control()`, `set_pwm()`, `set_duties(a, b, c)`, `set_shape(shape)`, `set_protection()`, `set_battery()`, `battery_state()`, `set_pv_calibration(offset, gain)`, `sample()`, `time()`, `start_log()`, `stop_log()`, `read_log()`, `eeprom_read(addr, length)`, `eeprom_write(addr, data)`, `close()` |
|
|
1468
|
+
| `BatteryModel` | `MotorEmulator.set_battery` | `capacity_ah`, `ocv`, `r0_ohm`, `r1_ohm`, `tau_s`, `soc`, `v_min`, `v_max`, `invert_current` |
|
|
1469
|
+
| `EmulatorCalibration` | `open_motor_emulator(calibration=...)` | `codes_per_amp`, `codes_per_volt`, `offsets`, `sample_rate_hz`, `amps()`, `volts()`, `current_code()`, `bus_code()`, `resistance_code()` |
|
|
1470
|
+
| `Wiring` | `wiring`, `save_wiring` | see [Wiring profile](#wiring-profile); also `defaults()`, `load(path)`, `coerce(value)` (a `Wiring`, dict or file path), `from_dict(data, *, strict=True)` (`strict=False` keeps unknown keys in `extra`), `to_dict()`, `with_changes(**changes)`, `assignments()`, `pins()`, `la(name)`, `signal(name)`, `describe()`, `warnings()`, `la_voltage`, `i2c_address` |
|
|
1471
|
+
| `Signal` | `Wiring.signals` | `name`, `la`, `direction`, `active_low`, `description`, `to_dict()` |
|
|
1472
|
+
| `Waveform` | `WaveformLibrary` | `id`, `name`, `kind`, `sample_count`, `sample_rate_hz`, `bits`, `full_scale_v`, `recording_size_bytes`, `created_at`, `dac_path`, `segments`, `unit`, `is_recording` |
|
|
1473
|
+
| `WaveformLibrary` (class) | `waveforms` | `list()`, `get()`, `find()`, `save_recording()`, `save_waveform()`, `save_segments()`, `rename()`, `delete()`, `preview()`, `download_recording()`, `samples_b64(waveform_id)` |
|
|
1474
|
+
| `BuildReporter`, `NoopBuildReporter` (classes) | `build_report`, `make_build_reporter` | `active`, `build_id`, `record_wiring()`, `upload_artifact()`, `upload_artifacts()`, `upload_logs()`, `set_result()`, `finalize()` |
|
|
1475
|
+
| `DeviceLease` (class) | a cloud `BenchPod` holds one | `acquire(*, wait_timeout=600.0, poll_interval=5.0)`, `held`, `lease_id`, `release()` |
|
|
1476
|
+
| `ConnSpec` | `parse_connection`, `resolve_connection` | `kind`, `addr`, `device`, `device_name`, `is_wifi()`, `is_serial()`, `is_cloud()` |
|
|
1477
|
+
| `LowLevel` (class) | `lowlevel` | see [Escape hatches](#escape-hatches) |
|
|
1478
|
+
| `Pin` / `Efuse` (enums) | channel and rail arguments | `PIN1` … `PIN14`, `INTERNAL`, `EXTERNAL` |
|
|
1479
|
+
|
|
1480
|
+
### `Capabilities`
|
|
1481
|
+
|
|
1482
|
+
`bp.capabilities` (and the `benchpod_capabilities` fixture) parses the pod's `status` reply
|
|
1483
|
+
(`Capabilities.from_status`) or the server's `cap.*` map for a cloud device
|
|
1484
|
+
(`Capabilities.from_parameters`); `merge(other)` overlays one on another. A flag that was not
|
|
1485
|
+
reported keeps its default (`False`, `0` or `""`), so a feature check is just
|
|
1486
|
+
`if bp.capabilities.spi_master:`. `@pytest.mark.benchpod_capability("name")` takes the same names.
|
|
1487
|
+
|
|
1488
|
+
| Group | Fields |
|
|
1489
|
+
|---|---|
|
|
1490
|
+
| Identity | `board`, `firmware_version`, `board_rev` (`"v2"`, `"v3"`, `"unknown"`, or `""`), `la_vccio_mv` (the LA bank voltage the pod reports) |
|
|
1491
|
+
| ADC | `adc_bits`, `adc_fullscale_mv`, `adc_channels`, `adc_offset_counts`, `adc_affine` (the front-end fit), `adc_max_count`, `counts_to_volts(count)` |
|
|
1492
|
+
| DAC and replay | `dac`, `dac_dc`, `dac_replay`, `dac_deep_replay`, `dac_control_loop`, `dac_loop_sources` (gateware v29+), `dac_loop_input_map` (v30+), `dac_cotrig` (v27+), `dac_bits`, `dac_replay_bits`, `dac_replay_max_samples`, `dac_fullscale_mv`, `dac_channels`, `dac_limits` (output limits on the DAC paths) |
|
|
1493
|
+
| Features | `scope`, `analyzer`, `serial`, `tunnel`, `command`, `ota`, `la_pins`, `gpio_read` (direct pin reads, v35+), `capture_trigger` (v35+), `power_profile`, `capture_b64` (faster ADC read-back, used automatically), `nrst_pin` (the dedicated reset pin, rev3), `usb_cc` (rev3), `spi_master` (v45+), `spi_stream`, `calibrate`, `current_out`, `can`, `pod_current` (`PowerStatus.pod`), `analog` (`False` on the digital-only board, `None` when not announced) |
|
|
1494
|
+
| Firmware updates and policies | `flash_kb` (MCU flash in KiB, 0 when not reported), `blob_slots`, `ota_sig`, `sig_policy` (`"audit"`, `"required"`, ...), `sig_keys`, `sig_policy_cmd`, `lan_policy` (`"open"`, `"locked"`, `"off"`), `lan_policy_cmd`, `tunnel_max_tier`, `lease_state` (the pod refuses LAN writes while a cloud job holds it), `cloud_ca`, `cloud_proxy`, `ws_auth_v2` (server only) |
|
|
1495
|
+
| Boot health | `safe_mode`, `safe_reason`, `last_crash`, `reset_cause`, `boot_warning()` (a one-line warning after safe mode or a crash, else `None`) |
|
|
1496
|
+
| Source | `raw` (the map it was parsed from) |
|
|
1497
|
+
|
|
1498
|
+
### `ServerApi`
|
|
1499
|
+
|
|
1500
|
+
A thin client for the embeddedci server's HTTP API (`{api_base}/api/...`), for scripts that need
|
|
1501
|
+
the server rather than a pod. `bp.server_api` returns one bound to the connection's credentials;
|
|
1502
|
+
build one yourself with `ServerApi(*, api_base=None, api_key=None, token_provider=None,
|
|
1503
|
+
timeout=60.0, lease_id=None)`. Errors raise `ServerApiError` (`status`), and an HTTP 403 raises
|
|
1504
|
+
`ServerPermissionDeniedError`.
|
|
1505
|
+
|
|
1506
|
+
| Method | Purpose |
|
|
1507
|
+
|---|---|
|
|
1508
|
+
| `list_devices()` | the pods the credentials can see |
|
|
1509
|
+
| `resolve_device_id(name)` | a device name to its server id (raises when unknown) |
|
|
1510
|
+
| `device_parameters(name_or_id)` | the server's `cap.*` map for a device (`Capabilities.from_parameters` parses it) |
|
|
1511
|
+
| `wiring_profile(device_id)` | the stored wiring profile: `{"profile", "stored", "defaults", "warnings"}` |
|
|
1512
|
+
| `put_wiring(device_id, wiring)` | store a wiring profile (the server validates it; errors name the fields) |
|
|
1513
|
+
| `scope_capture_start(device_id, *, samples=256, sample_rate_mhz=1.0)` | start a server-side ADC capture; returns its id |
|
|
1514
|
+
| `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 |
|
|
1515
|
+
| `capture_snapshot(capture_id)` | fetch a server-side capture |
|
|
1516
|
+
| `replay_start(payload)` | arm a server-side DAC replay (`POST /dac/replay/start`) |
|
|
1517
|
+
| `request(method, path, *, query=None, json_body=None, raw_body=None, content_type=None, parse_json=True)` | any other endpoint; returns `(status, body)` |
|
|
1518
|
+
|
|
1519
|
+
### Functions, constants and modules
|
|
1520
|
+
|
|
1521
|
+
| Name | Purpose |
|
|
1522
|
+
|---|---|
|
|
1523
|
+
| `parse_connection(raw)` | parse a connection string into a `ConnSpec` (raises `ConnectionConfigError`) |
|
|
1524
|
+
| `resolve_connection(connection=None)` | the same, falling back to `BENCHPOD_CONNECTION` |
|
|
1525
|
+
| `make_build_reporter(*, api_base=None, audience=None, target="", name="")` | a `BuildReporter` inside GitHub Actions, else a `NoopBuildReporter` |
|
|
1526
|
+
| `build_panel_curve(voc_code, sharpness=4.0, points=256)`, `build_linear_curve(max_code, rising=True, points=256)`, `build_constant_curve(value_code, points=256)` | control-loop curves (see [In-fabric DAC control loop](#in-fabric-dac-control-loop)) |
|
|
1527
|
+
| `curve_output_at(curve, input_code)`, `input_percent_to_code(percent)` | evaluate a curve the way the gateware does; an input percentage as a code |
|
|
1528
|
+
| `encode_curve_b64url(codes)` | 16-bit DAC codes as little-endian base64url, the wire form of a curve |
|
|
1529
|
+
| `INTERNAL`, `EXTERNAL`, `PIN1` … `PIN14`, `BMP280_ADDR_PRIMARY`, `BMP280_ADDR_SECONDARY` | named constants (see [API conventions](#api-conventions-and-stability)) |
|
|
1530
|
+
| `DacPath`, `DacOutputPath`, `AnalogPath`, `AdcSource`, `CalibrateSource` (`"current_in"`), `LoopSource`, `Waveshape`, `ReplayMapping`, `DecodeProtocol`, `CanMode`, `FaultType`, `GpioMode`, `Edge`, `TriggerEdge` | the `Literal` string types |
|
|
1531
|
+
| `decode`, `i2c`, `can`, `control_loop`, `motor_emulator`, `dsp` | submodules: off-device decoders, I2C helpers, CAN types, curve helpers, the emulator's registers, and `dsp`, a pure-Python mirror of the server's record-to-replay processing |
|
|
1532
|
+
|
|
1354
1533
|
## Examples
|
|
1355
1534
|
|
|
1356
1535
|
* [`examples/test_bmp280.py`](https://github.com/embeddedci-com/embeddedci-python/blob/main/packages/embeddedci/examples/test_bmp280.py)
|
|
@@ -35,7 +35,8 @@ No pin numbers: the SWD channels, the UART pair, the baud and the power rail com
|
|
|
35
35
|
[Analog paths](#analog-paths-dc-output-and-single-readings) · [Captures](#captures) ·
|
|
36
36
|
[DAC](#dac-generator-replay-and-faults) · [Control loop](#in-fabric-dac-control-loop) ·
|
|
37
37
|
[CAN](#can) · [Cloud](#cloud-embeddedcidevice-name) · [Build reporting](#build-reporting) ·
|
|
38
|
-
[Errors](#errors) · [Escape hatches](#escape-hatches) · [
|
|
38
|
+
[Errors](#errors) · [Escape hatches](#escape-hatches) · [API reference](#api-reference) ·
|
|
39
|
+
[Releasing](#releasing-maintainers)
|
|
39
40
|
|
|
40
41
|
## Install
|
|
41
42
|
|
|
@@ -194,6 +195,8 @@ automatically.)
|
|
|
194
195
|
| `BENCHPOD_API_KEY` | no `api_key` argument / `--benchpod-api-key` | EmbeddedCI API key (`eci_…`) for the cloud destination and server features |
|
|
195
196
|
| `BENCHPOD_API_BASE` | no `api_base` argument / `--benchpod-api-base` | server base URL (default `https://www.embeddedci.com`) |
|
|
196
197
|
| `BENCHPOD_BUILD_TARGET` | no `--benchpod-build-target` | platform id recorded by the `build_report` fixture |
|
|
198
|
+
| `BENCHPOD_WIRING` | no `wiring` argument / `--benchpod-wiring` / `benchpod_wiring` fixture | wiring profile file (`.json`/`.toml`), see [Wiring profile](#wiring-profile) |
|
|
199
|
+
| `BENCHPOD_LIFT_DAC_LIMITS` | pytest sessions | `1` clears the pod's DAC output limits for the session and restores the same limits at the end, even when tests fail (only with the output stage switched off) |
|
|
197
200
|
| `BENCHPOD_STALL_TIMEOUT` | flashing | seconds with no SWD traffic before a flash attempt is aborted and retried (default 60) |
|
|
198
201
|
|
|
199
202
|
## API conventions and stability
|
|
@@ -223,6 +226,24 @@ automatically.)
|
|
|
223
226
|
Plain ints work too (`efuse=1`, `swclk=11`); they are validated. The target's reset line is not an
|
|
224
227
|
LA channel — it has a dedicated pin (see [Flashing](#flashing)).
|
|
225
228
|
|
|
229
|
+
**Feature checks.** Pods differ by board, firmware and gateware. `bp.supports("spi_master")` says
|
|
230
|
+
whether the pod has any `Capabilities` flag (`False` for a flag this SDK does not know), and the
|
|
231
|
+
properties `can_gpio`, `can_trigger`, `can_spi`, `can_profile_power`, `can_calibrate`,
|
|
232
|
+
`can_current_out`, `can_reset_target` and `can_analog` name the common ones (`can_config` …
|
|
233
|
+
`can_disable` stay the CAN bus commands). A call the pod cannot serve raises
|
|
234
|
+
`UnsupportedFeatureError` (see [Errors](#errors)), before anything is sent when the capabilities
|
|
235
|
+
already say so:
|
|
236
|
+
|
|
237
|
+
```python
|
|
238
|
+
import pytest
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def test_flash_spi_nor(benchpod):
|
|
242
|
+
if not benchpod.can_spi:
|
|
243
|
+
pytest.skip("this pod has no SPI master")
|
|
244
|
+
benchpod.spi_flash("fw.bin")
|
|
245
|
+
```
|
|
246
|
+
|
|
226
247
|
**Stability promise.** Everything in `embeddedci.benchpod.__all__` follows semantic versioning:
|
|
227
248
|
within 2.x names and signatures only grow. `BenchPod.command()`, `BenchPod.transport` and
|
|
228
249
|
`BenchPod.lowlevel` are escape hatches **outside** the promise. Migrating from 0.x/1.x? The
|
|
@@ -305,7 +326,7 @@ The `benchpod` fixture is a `BenchPod` instance, not the module — import const
|
|
|
305
326
|
| `--benchpod-efuse` | — | the profile's rail | target-power rail for `benchpod_target` and `pins.efuse` (1 internal, 2 external); `benchpod_target` otherwise follows the wiring profile, `pins.efuse` is 1 |
|
|
306
327
|
| `--benchpod-firmware` | — | — | firmware image for the `firmware` fixture |
|
|
307
328
|
| `--benchpod-discover` | — | off | when no connection is configured, find one pod via mDNS (needs `[discovery]`) |
|
|
308
|
-
| `--benchpod-api-key` | `BENCHPOD_API_KEY` | — | API key for the cloud destination and the waveform library |
|
|
329
|
+
| `--benchpod-api-key` | `BENCHPOD_API_KEY` | — | API key for the cloud destination and the waveform library; it, `--benchpod-api-base` and the lease options also reach the short connection that lifts the DAC limits (`BENCHPOD_LIFT_DAC_LIMITS=1`) |
|
|
309
330
|
| `--benchpod-api-base` | `BENCHPOD_API_BASE` | `https://www.embeddedci.com` | embeddedci server |
|
|
310
331
|
| `--benchpod-lease-wait` | — | `600` | seconds to wait for a busy cloud device |
|
|
311
332
|
| `--benchpod-no-lease` | — | off | don't lock the cloud device (only when nothing else can use it) |
|
|
@@ -340,6 +361,13 @@ benchpod_connection = 192.168.1.213
|
|
|
340
361
|
`benchpod` is shared by the whole session, so state a test leaves behind (a running DAC, engaged
|
|
341
362
|
pull-ups, an armed sensor) carries into the next one — use the teardown fixtures.
|
|
342
363
|
|
|
364
|
+
**DAC output limits.** A pod can carry DAC output limits that protect an external output stage (a
|
|
365
|
+
solar simulator module on the 5 V path, say); it then refuses what they forbid. With the stage
|
|
366
|
+
switched off, run the suite with `BENCHPOD_LIFT_DAC_LIMITS=1`: the plugin clears the limits once
|
|
367
|
+
when the session starts and puts the same limits back at the end, even when tests fail. It opens
|
|
368
|
+
its own short connection, so it applies whichever fixture the suite uses. Without the variable
|
|
369
|
+
nothing changes.
|
|
370
|
+
|
|
343
371
|
### Markers
|
|
344
372
|
|
|
345
373
|
* `@pytest.mark.hardware` — labels tests that need a real pod, for selection
|
|
@@ -619,7 +647,7 @@ won't engage one while the LA bank is at 1.8 V (`PullState.available` is false).
|
|
|
619
647
|
| LA3, LA4 | pull-up | 2.2k |
|
|
620
648
|
| LA5, LA6 | pull-up | 10k |
|
|
621
649
|
| LA7, LA8 | pull-**down** | 10k |
|
|
622
|
-
| LA9–
|
|
650
|
+
| LA9–LA14 | none | — |
|
|
623
651
|
|
|
624
652
|
```python
|
|
625
653
|
from embeddedci.benchpod import PIN1, PIN2, PIN7
|
|
@@ -1294,6 +1322,7 @@ Invalid arguments raise `ValueError` before anything is sent. Everything else ra
|
|
|
1294
1322
|
| `PodBusyError` (a `FirmwareError`) | the pod is busy (`busy: …`), e.g. a capture or upload is running | `firmware_message`, `cmd` |
|
|
1295
1323
|
| `PodLeasedError` (a `PodBusyError`, `DeviceBusyError` and `TransportError`) | a cloud job holds the pod (its refusal on the LAN, or the server's HTTP 409) | `holder`, `left_s`, `expires_at`, `status` |
|
|
1296
1324
|
| `PermissionDeniedError` (a `FirmwareError` and `TransportError`) | the pod's `forbidden: …` on a cloud tunnel, or a server HTTP 403 (`ServerPermissionDeniedError`, also a `ServerApiError`, from `ServerApi`) | `status` |
|
|
1325
|
+
| `UnsupportedFeatureError` (a `FirmwareError`) | 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 | `feature`, `firmware_version`, `firmware_message`, `cmd` |
|
|
1297
1326
|
| `UartTimeout` | `UartSession.expect` timed out | `text` |
|
|
1298
1327
|
| `UartLinkError` (a `UartTimeout` and `ConnectionClosedError`) | the UART link died with an error while `UartSession` waited | `text`, `cause` |
|
|
1299
1328
|
| `PinConflictError` | an LA channel is already used by another function (a `FirmwareError`) | `la`, `function` |
|
|
@@ -1313,6 +1342,156 @@ These sit below the stable API and are **not** covered by the stability promise:
|
|
|
1313
1342
|
`cal_switch_status()`, `dac_set(code, divider=)`. Prefer the named paths — these can leave the
|
|
1314
1343
|
front end in a state no named path describes.
|
|
1315
1344
|
|
|
1345
|
+
## API reference
|
|
1346
|
+
|
|
1347
|
+
Every name in `embeddedci.benchpod.__all__`, in one place. The sections above show how the pieces
|
|
1348
|
+
fit together; this is the index. `tests/test_docs_coverage.py` fails when a public name is missing
|
|
1349
|
+
from this README, so the list stays complete.
|
|
1350
|
+
|
|
1351
|
+
### `BenchPod`
|
|
1352
|
+
|
|
1353
|
+
`BenchPod(connection=None, *, la_voltage=None, timeout=30.0, transport=None, api_base=None,
|
|
1354
|
+
api_key=None, cloud_token=None, cloud_audience=None, cloud_user_token=None, lease=True,
|
|
1355
|
+
lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is idempotent.
|
|
1356
|
+
|
|
1357
|
+
| Area | Methods and properties |
|
|
1358
|
+
|---|---|
|
|
1359
|
+
| Connection and device | `ping()`, `status()`, `capabilities`, `refresh_capabilities()` (drop the cached capabilities and read them again), `close()`, `leased` (true while this client holds the cloud lease), `wiring`, `save_wiring(wiring=None)` (store a cloud device's profile on embeddedci.com), `signal(name)`, `fpga_image(image)`, `usb_cc()` |
|
|
1360
|
+
| LA voltage | `set_la_voltage(volts)`, `get_la_voltage()` |
|
|
1361
|
+
| Power and reset | `power_on(efuse=None, *, delay=None)`, `power_off(efuse=None, *, delay=None)`, `target_power(efuse=None, *, on, delay=None)`, `target_status()`, `power_status()`, `measure_power(duration, *, efuse=None, rate_hz=500.0, keep_samples=0)`, `power_profile(*, efuse=None, rate_hz=500.0, keep_samples=4096, max_duration=60.0)`, `reset_target(*, pulse=0.1)`, `set_reset(asserted)`, `reset_state()` |
|
|
1362
|
+
| Flashing | `flash(...)`, `spi_flash(image, addr=0, *, erase=True, verify=True, hold_reset=False, sck=, mosi=, miso=, cs=)`, `open_spi(...)`, `open_motor_emulator(...)` |
|
|
1363
|
+
| UART | `capture_uart(...)`, `power_cycle_and_capture(...)`, `open_uart(...)` |
|
|
1364
|
+
| Bias resistors | `enable_pullup(*las)`, `disable_pullup(*las)`, `enable_pulldown(*las)`, `disable_pulldown(*las)` (LA7/LA8), `set_pull(la, enabled)`, `pull_state(la)`, `enabled_pulls()` |
|
|
1365
|
+
| GPIO and pins | `gpio(la, mode="output", *, level=None)`, `gpio_pins(las, mode="output", *, level=None)`, `configure_gpio(las, mode="output", *, level=None)`, `set_gpio(la, level)`, `read_gpio(la)`, `wait_for_level(la, level, *, timeout, poll=0.005)` (`False` when `timeout` passes first), `pin_levels()`, `la_pins()`, `release_gpio(*las)`, `la_step(la, *, steps, delay, dir_la=None, direction=0)` |
|
|
1366
|
+
| I2C sensor | `enable_i2c_sensor(...)`, `set_i2c_sensor(...)`, `disable_i2c_sensor()`, `i2c_sensor_status()`, `i2c_sensor_regs()`, `i2c_sensor_capture(...)` |
|
|
1367
|
+
| Analog | `analog_path(path)`, `adc_read(source="ext")`, `dac_output(path, *, volts=None)`, `current_out(current)`, `current_out_range()`, `calibrate(source="current_in")`, `calibration()`, `clear_calibration()` |
|
|
1368
|
+
| Captures | `capture_adc(...)`, `capture_la(...)`, `capture_correlated(...)`, `decode(source, protocol="i2c", ...)` |
|
|
1369
|
+
| DAC | `generate(...)`, `replay(...)`, `replay_waveform(...)`, `dac_stop()`, `control_loop(...)`, `loop_input(...)`, `loop_probe()` |
|
|
1370
|
+
| CAN | `open_can(*, bitrate=500000, mode="normal", term=False, fd=False)`, `can_config(...)`, `can_disable()`, `can_status()`, `can_term(on)`, `can_write(...)`, `can_read(...)`, `can_respond(...)`, `can_respond_clear()` |
|
|
1371
|
+
| Cloud library and server | `waveforms`, `save_capture_as_recording(...)`, `server_api` (the `ServerApi`; needs an API key or a cloud session) |
|
|
1372
|
+
| Escape hatches | `command(cmd)`, `transport`, `lowlevel` (see [Escape hatches](#escape-hatches)) |
|
|
1373
|
+
|
|
1374
|
+
### Types
|
|
1375
|
+
|
|
1376
|
+
Results are frozen dataclasses unless noted. A result built from a firmware reply keeps that reply in
|
|
1377
|
+
`raw` and has a `from_reply()` classmethod (`from_dict()`, `from_json()` or `from_chunks()` for the
|
|
1378
|
+
few built from other shapes); `to_dict()` goes the other way where a type is sent back.
|
|
1379
|
+
|
|
1380
|
+
| Type | Returned by / used for | Fields and members |
|
|
1381
|
+
|---|---|---|
|
|
1382
|
+
| `LaVoltage` | `get_la_voltage`, `set_la_voltage` | `voltage`, `readback`, `is_set` |
|
|
1383
|
+
| `TargetStatus` | `target_status` | `internal`, `external` (each an `EfuseState`), `supported`, `efuse(n)` |
|
|
1384
|
+
| `EfuseState` | `TargetStatus.efuse` | `enabled`, `fault`, `valid` |
|
|
1385
|
+
| `PowerStatus` | `power_status` | `internal`, `external` (each a `RailPower`), `pod`, `total_current`, `rail(n)` |
|
|
1386
|
+
| `RailPower` | `PowerStatus.rail` | `ok`, `bus_voltage`, `current` |
|
|
1387
|
+
| `PowerProfile` | `measure_power`, `PowerProfileSession.result` | see [Power profiles](#power-profiles) |
|
|
1388
|
+
| `PowerProfileSession` | `power_profile` (a context manager) | `start()`, `stop()`, `status()`, `result` |
|
|
1389
|
+
| `ResetState` | `reset_target`, `set_reset`, `reset_state` | `asserted`, `supported` |
|
|
1390
|
+
| `UsbCcStatus` | `usb_cc` | `orientation`, `advertised`, `advertised_current` (A), `cc1_voltage`, `cc2_voltage` (V) |
|
|
1391
|
+
| `PullState` | `pull_state`, `set_pull` | `la`, `enabled`, `direction`, `ohms`, `available` |
|
|
1392
|
+
| `LaPinState` | `la_pins`, `configure_gpio` | `la`, `function`, `gpio`, `level`, `pull`, `pull_ohms`, `pull_on`, `in_use` |
|
|
1393
|
+
| `GpioPin` (class) | `gpio`, `gpio_pins`, `signal` | `name`, `configure()`, `set(level)`, `high()`, `low()`, `activate()`, `deactivate()`, `is_active()`, `read()`, `wait_for(level, *, timeout)`, `pulse(width, *, count=1)`, `state()`, `release()` |
|
|
1394
|
+
| `FlashResult` | `flash` | `ok`, `returncode`, `stdout`, `stderr`, `target_unreachable`, `stalled` |
|
|
1395
|
+
| `SpiSession` (class) | `open_spi` | `flash_id()`, `flash_read(addr, length)`, `flash_erase(addr, length)`, `flash_chip_erase()` (whole chip; seconds taken), `flash_write(addr, data)`, `flash_program(data, addr=0, *, erase=True, verify=True)` (erase, write and verify a whole image), `transfer(data)`, `stream(data, *, head=b"", hold_cs=False)`, `close()` |
|
|
1396
|
+
| `SpiFlashInfo` | `SpiSession.flash_id` | `jedec_id`, `present`, `size`, `status` |
|
|
1397
|
+
| `SpiFlashResult` | `spi_flash`, `SpiSession.flash_program` | `jedec_id`, `addr`, `length`, `erased`, `verified`, `seconds` |
|
|
1398
|
+
| `SpiStreamResult` | `SpiSession.stream` | `sent`, `seconds` |
|
|
1399
|
+
| `UartCapture` | `capture_uart`, `power_cycle_and_capture` | `text`, `lines`, `matched`, `match(pattern)`, `contains(needle)` (also `needle in capture`) |
|
|
1400
|
+
| `UartSession` (class) | `open_uart` (a context manager) | `write(data)`, `read(*, timeout=0.0)`, `read_until(pattern, *, timeout)`, `expect(pattern, *, timeout)`, `text`, `lines`, `closed`, `error`, `close()` |
|
|
1401
|
+
| `Sensor` (enum) | `enable_i2c_sensor` | `Sensor.BMP280` |
|
|
1402
|
+
| `I2CTransaction` | `i2c_sensor_capture`, `decode("i2c")` | `messages`, `complete`, `address`, `to(address)` |
|
|
1403
|
+
| `I2CMessage` | `I2CTransaction.messages` | `address`, `read`, `address_ack`, `data`, `values` |
|
|
1404
|
+
| `I2CByte` | `I2CMessage.data` | `value`, `ack` |
|
|
1405
|
+
| `UartFrame` | `decode("uart")` | `index`, `start_us`, `end_us`, `value`, `hex`, `text`, `ok`, `error` |
|
|
1406
|
+
| `SpiFrame` | `decode("spi")` | `index`, `start_us`, `end_us`, `mosi`, `miso`, `mosi_hex`, `miso_hex` |
|
|
1407
|
+
| `AnalogPathState` | `analog_path` | `path`, `dac_mux_register`, `cal_relay_register` |
|
|
1408
|
+
| `AdcReading` | `adc_read` | `source`, `voltage`, `count`, `span`, `offset`, `current` |
|
|
1409
|
+
| `DacOutput` | `dac_output` | `path`, `voltage`, `code` |
|
|
1410
|
+
| `CurrentOutput` | `current_out`, `current_out_range` | `current`, `code`, `min_current`, `max_current` |
|
|
1411
|
+
| `Calibration` | `calibrate`, `calibration`, `clear_calibration` | `source`, `calibrated`, `offset`, `a`, `b`, `count`, `span`, `samples` |
|
|
1412
|
+
| `Capture` | `capture_adc` | `counts`, `volts`, `currents`, `sample_rate_hz`, `source`, `trigger`, `duration`, `times()`, `mean()`, `min()`, `max()`, `rms()`, `rms_ac()`, `peak_to_peak()`, `first_crossing()`, `crossing_times()`, `fft()`, `dominant_frequency()` |
|
|
1413
|
+
| `LaCapture` | `capture_la` | `words`, `channels`, `sample_rate_hz`, `trigger`, `duration`, `channel(la)`, `level_at()`, `edges()`, `edge_times()`, `first_edge()`, `delay()`, `frequency()`, `duty_cycle()`, `pulse_widths()`, `decode()` |
|
|
1414
|
+
| `CorrelatedCapture` | `capture_correlated` | `adc` (a `Capture`), `la` (a `LaCapture`), sharing one hardware trigger |
|
|
1415
|
+
| `Trigger` | `capture_adc`, `capture_la`, `capture_correlated` | `la`, `edge` |
|
|
1416
|
+
| `DacHandle` (class) | `generate` (a context manager) | `stop()` |
|
|
1417
|
+
| `ReplayHandle` (class) | `replay`, `replay_waveform` (a context manager) | a `DacHandle` that also knows what it replays: `stop()` |
|
|
1418
|
+
| `Fault` | `replay(fault=...)` | `type`, `start`, `width`, `level`, `to_dict()` |
|
|
1419
|
+
| `Segment` | `WaveformLibrary.save_segments` | `shape` (`ramp`, `hold`, `step`), `duration`, `v_start`, `v_end`, `to_dict()` |
|
|
1420
|
+
| `ControlLoopHandle` (class) | `control_loop` (a context manager) | `probe()`, `set_input(...)`, `stop()` |
|
|
1421
|
+
| `IVPoint` | `loop_probe`, `ControlLoopHandle.probe` | `i`, `v`, `input_code`, `source`, `tripped`, `loop_input` |
|
|
1422
|
+
| `LoopInputMap` | `control_loop(input_map=...)` | `mv_per_unit`, `range_min`, `range_max`, `mv_at_zero`, `trip`, `to_request()` |
|
|
1423
|
+
| `LoopState` | `loop_input` | `source`, `input_code`, `step`, `output_code` |
|
|
1424
|
+
| `FpgaImage` (enum) | `fpga_image` | `LOOP`, `DEEP_REPLAY` |
|
|
1425
|
+
| `FpgaImageInfo` | `fpga_image` | `image`, `version`, `features` |
|
|
1426
|
+
| `CanBus` (class) | `open_can` | `write()`, `read()`, `read_until()`, `expect()`, `collect()`, `assert_periodic()`, `add_responder()`, `clear_responders()`, `simulate_ecu()`, `set_term()`, `status()`, `close()` |
|
|
1427
|
+
| `CanFrame` | `CanBus.read`, `CanBus.expect` | `id`, `data`, `ext`, `rtr`, `ts`, `dlc` |
|
|
1428
|
+
| `CanReadResult` | `can_read` | `frames`, `overflow` |
|
|
1429
|
+
| `MotorEmulator` (class) | `open_motor_emulator` | `configure()`, `boards()`, `probe()`, `read()`, `read_signed(reg)`, `write()`, `status()`, `trips()`, `clear_trips()`, `arm()`, `set_control()`, `set_pwm()`, `set_duties(a, b, c)`, `set_shape(shape)`, `set_protection()`, `set_battery()`, `battery_state()`, `set_pv_calibration(offset, gain)`, `sample()`, `time()`, `start_log()`, `stop_log()`, `read_log()`, `eeprom_read(addr, length)`, `eeprom_write(addr, data)`, `close()` |
|
|
1430
|
+
| `BatteryModel` | `MotorEmulator.set_battery` | `capacity_ah`, `ocv`, `r0_ohm`, `r1_ohm`, `tau_s`, `soc`, `v_min`, `v_max`, `invert_current` |
|
|
1431
|
+
| `EmulatorCalibration` | `open_motor_emulator(calibration=...)` | `codes_per_amp`, `codes_per_volt`, `offsets`, `sample_rate_hz`, `amps()`, `volts()`, `current_code()`, `bus_code()`, `resistance_code()` |
|
|
1432
|
+
| `Wiring` | `wiring`, `save_wiring` | see [Wiring profile](#wiring-profile); also `defaults()`, `load(path)`, `coerce(value)` (a `Wiring`, dict or file path), `from_dict(data, *, strict=True)` (`strict=False` keeps unknown keys in `extra`), `to_dict()`, `with_changes(**changes)`, `assignments()`, `pins()`, `la(name)`, `signal(name)`, `describe()`, `warnings()`, `la_voltage`, `i2c_address` |
|
|
1433
|
+
| `Signal` | `Wiring.signals` | `name`, `la`, `direction`, `active_low`, `description`, `to_dict()` |
|
|
1434
|
+
| `Waveform` | `WaveformLibrary` | `id`, `name`, `kind`, `sample_count`, `sample_rate_hz`, `bits`, `full_scale_v`, `recording_size_bytes`, `created_at`, `dac_path`, `segments`, `unit`, `is_recording` |
|
|
1435
|
+
| `WaveformLibrary` (class) | `waveforms` | `list()`, `get()`, `find()`, `save_recording()`, `save_waveform()`, `save_segments()`, `rename()`, `delete()`, `preview()`, `download_recording()`, `samples_b64(waveform_id)` |
|
|
1436
|
+
| `BuildReporter`, `NoopBuildReporter` (classes) | `build_report`, `make_build_reporter` | `active`, `build_id`, `record_wiring()`, `upload_artifact()`, `upload_artifacts()`, `upload_logs()`, `set_result()`, `finalize()` |
|
|
1437
|
+
| `DeviceLease` (class) | a cloud `BenchPod` holds one | `acquire(*, wait_timeout=600.0, poll_interval=5.0)`, `held`, `lease_id`, `release()` |
|
|
1438
|
+
| `ConnSpec` | `parse_connection`, `resolve_connection` | `kind`, `addr`, `device`, `device_name`, `is_wifi()`, `is_serial()`, `is_cloud()` |
|
|
1439
|
+
| `LowLevel` (class) | `lowlevel` | see [Escape hatches](#escape-hatches) |
|
|
1440
|
+
| `Pin` / `Efuse` (enums) | channel and rail arguments | `PIN1` … `PIN14`, `INTERNAL`, `EXTERNAL` |
|
|
1441
|
+
|
|
1442
|
+
### `Capabilities`
|
|
1443
|
+
|
|
1444
|
+
`bp.capabilities` (and the `benchpod_capabilities` fixture) parses the pod's `status` reply
|
|
1445
|
+
(`Capabilities.from_status`) or the server's `cap.*` map for a cloud device
|
|
1446
|
+
(`Capabilities.from_parameters`); `merge(other)` overlays one on another. A flag that was not
|
|
1447
|
+
reported keeps its default (`False`, `0` or `""`), so a feature check is just
|
|
1448
|
+
`if bp.capabilities.spi_master:`. `@pytest.mark.benchpod_capability("name")` takes the same names.
|
|
1449
|
+
|
|
1450
|
+
| Group | Fields |
|
|
1451
|
+
|---|---|
|
|
1452
|
+
| Identity | `board`, `firmware_version`, `board_rev` (`"v2"`, `"v3"`, `"unknown"`, or `""`), `la_vccio_mv` (the LA bank voltage the pod reports) |
|
|
1453
|
+
| ADC | `adc_bits`, `adc_fullscale_mv`, `adc_channels`, `adc_offset_counts`, `adc_affine` (the front-end fit), `adc_max_count`, `counts_to_volts(count)` |
|
|
1454
|
+
| DAC and replay | `dac`, `dac_dc`, `dac_replay`, `dac_deep_replay`, `dac_control_loop`, `dac_loop_sources` (gateware v29+), `dac_loop_input_map` (v30+), `dac_cotrig` (v27+), `dac_bits`, `dac_replay_bits`, `dac_replay_max_samples`, `dac_fullscale_mv`, `dac_channels`, `dac_limits` (output limits on the DAC paths) |
|
|
1455
|
+
| Features | `scope`, `analyzer`, `serial`, `tunnel`, `command`, `ota`, `la_pins`, `gpio_read` (direct pin reads, v35+), `capture_trigger` (v35+), `power_profile`, `capture_b64` (faster ADC read-back, used automatically), `nrst_pin` (the dedicated reset pin, rev3), `usb_cc` (rev3), `spi_master` (v45+), `spi_stream`, `calibrate`, `current_out`, `can`, `pod_current` (`PowerStatus.pod`), `analog` (`False` on the digital-only board, `None` when not announced) |
|
|
1456
|
+
| Firmware updates and policies | `flash_kb` (MCU flash in KiB, 0 when not reported), `blob_slots`, `ota_sig`, `sig_policy` (`"audit"`, `"required"`, ...), `sig_keys`, `sig_policy_cmd`, `lan_policy` (`"open"`, `"locked"`, `"off"`), `lan_policy_cmd`, `tunnel_max_tier`, `lease_state` (the pod refuses LAN writes while a cloud job holds it), `cloud_ca`, `cloud_proxy`, `ws_auth_v2` (server only) |
|
|
1457
|
+
| Boot health | `safe_mode`, `safe_reason`, `last_crash`, `reset_cause`, `boot_warning()` (a one-line warning after safe mode or a crash, else `None`) |
|
|
1458
|
+
| Source | `raw` (the map it was parsed from) |
|
|
1459
|
+
|
|
1460
|
+
### `ServerApi`
|
|
1461
|
+
|
|
1462
|
+
A thin client for the embeddedci server's HTTP API (`{api_base}/api/...`), for scripts that need
|
|
1463
|
+
the server rather than a pod. `bp.server_api` returns one bound to the connection's credentials;
|
|
1464
|
+
build one yourself with `ServerApi(*, api_base=None, api_key=None, token_provider=None,
|
|
1465
|
+
timeout=60.0, lease_id=None)`. Errors raise `ServerApiError` (`status`), and an HTTP 403 raises
|
|
1466
|
+
`ServerPermissionDeniedError`.
|
|
1467
|
+
|
|
1468
|
+
| Method | Purpose |
|
|
1469
|
+
|---|---|
|
|
1470
|
+
| `list_devices()` | the pods the credentials can see |
|
|
1471
|
+
| `resolve_device_id(name)` | a device name to its server id (raises when unknown) |
|
|
1472
|
+
| `device_parameters(name_or_id)` | the server's `cap.*` map for a device (`Capabilities.from_parameters` parses it) |
|
|
1473
|
+
| `wiring_profile(device_id)` | the stored wiring profile: `{"profile", "stored", "defaults", "warnings"}` |
|
|
1474
|
+
| `put_wiring(device_id, wiring)` | store a wiring profile (the server validates it; errors name the fields) |
|
|
1475
|
+
| `scope_capture_start(device_id, *, samples=256, sample_rate_mhz=1.0)` | start a server-side ADC capture; returns its id |
|
|
1476
|
+
| `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 |
|
|
1477
|
+
| `capture_snapshot(capture_id)` | fetch a server-side capture |
|
|
1478
|
+
| `replay_start(payload)` | arm a server-side DAC replay (`POST /dac/replay/start`) |
|
|
1479
|
+
| `request(method, path, *, query=None, json_body=None, raw_body=None, content_type=None, parse_json=True)` | any other endpoint; returns `(status, body)` |
|
|
1480
|
+
|
|
1481
|
+
### Functions, constants and modules
|
|
1482
|
+
|
|
1483
|
+
| Name | Purpose |
|
|
1484
|
+
|---|---|
|
|
1485
|
+
| `parse_connection(raw)` | parse a connection string into a `ConnSpec` (raises `ConnectionConfigError`) |
|
|
1486
|
+
| `resolve_connection(connection=None)` | the same, falling back to `BENCHPOD_CONNECTION` |
|
|
1487
|
+
| `make_build_reporter(*, api_base=None, audience=None, target="", name="")` | a `BuildReporter` inside GitHub Actions, else a `NoopBuildReporter` |
|
|
1488
|
+
| `build_panel_curve(voc_code, sharpness=4.0, points=256)`, `build_linear_curve(max_code, rising=True, points=256)`, `build_constant_curve(value_code, points=256)` | control-loop curves (see [In-fabric DAC control loop](#in-fabric-dac-control-loop)) |
|
|
1489
|
+
| `curve_output_at(curve, input_code)`, `input_percent_to_code(percent)` | evaluate a curve the way the gateware does; an input percentage as a code |
|
|
1490
|
+
| `encode_curve_b64url(codes)` | 16-bit DAC codes as little-endian base64url, the wire form of a curve |
|
|
1491
|
+
| `INTERNAL`, `EXTERNAL`, `PIN1` … `PIN14`, `BMP280_ADDR_PRIMARY`, `BMP280_ADDR_SECONDARY` | named constants (see [API conventions](#api-conventions-and-stability)) |
|
|
1492
|
+
| `DacPath`, `DacOutputPath`, `AnalogPath`, `AdcSource`, `CalibrateSource` (`"current_in"`), `LoopSource`, `Waveshape`, `ReplayMapping`, `DecodeProtocol`, `CanMode`, `FaultType`, `GpioMode`, `Edge`, `TriggerEdge` | the `Literal` string types |
|
|
1493
|
+
| `decode`, `i2c`, `can`, `control_loop`, `motor_emulator`, `dsp` | submodules: off-device decoders, I2C helpers, CAN types, curve helpers, the emulator's registers, and `dsp`, a pure-Python mirror of the server's record-to-replay processing |
|
|
1494
|
+
|
|
1316
1495
|
## Examples
|
|
1317
1496
|
|
|
1318
1497
|
* [`examples/test_bmp280.py`](https://github.com/embeddedci-com/embeddedci-python/blob/main/packages/embeddedci/examples/test_bmp280.py)
|
|
@@ -100,6 +100,7 @@ from .errors import (
|
|
|
100
100
|
TriggerTimeout,
|
|
101
101
|
UartLinkError,
|
|
102
102
|
UartTimeout,
|
|
103
|
+
UnsupportedFeatureError,
|
|
103
104
|
)
|
|
104
105
|
from .flash import FlashResult
|
|
105
106
|
from .gpio import GpioPin, LaPinState
|
|
@@ -279,6 +280,7 @@ __all__ = [
|
|
|
279
280
|
"PinConflictError",
|
|
280
281
|
"PullConflictError",
|
|
281
282
|
"TriggerTimeout",
|
|
283
|
+
"UnsupportedFeatureError",
|
|
282
284
|
# device lease
|
|
283
285
|
"DeviceLease",
|
|
284
286
|
# CI build reporting
|
|
@@ -82,7 +82,7 @@ from .constants import (
|
|
|
82
82
|
coerce_pin,
|
|
83
83
|
)
|
|
84
84
|
from .connection import resolve_connection
|
|
85
|
-
from .errors import BenchPodError, FirmwareError, classify_firmware_error
|
|
85
|
+
from .errors import BenchPodError, FirmwareError, UnsupportedFeatureError, classify_firmware_error
|
|
86
86
|
from .flash import FlashResult
|
|
87
87
|
from .gpio import GpioPin, LaPinState
|
|
88
88
|
from .lease import DEFAULT_LEASE_TTL, DEFAULT_LEASE_WAIT, DeviceLease
|
|
@@ -355,11 +355,62 @@ class BenchPod:
|
|
|
355
355
|
self._caps = None
|
|
356
356
|
return self.capabilities
|
|
357
357
|
|
|
358
|
+
def supports(self, feature: str) -> bool:
|
|
359
|
+
"""Whether this pod has ``feature``, a :class:`Capabilities` flag (``"spi_master"``,
|
|
360
|
+
``"capture_trigger"``, ``"can"``, ...). Reads the capabilities once; ``False`` for a
|
|
361
|
+
flag this SDK does not know. The ``can_*`` properties name the common ones."""
|
|
362
|
+
return bool(getattr(self.capabilities, feature, False))
|
|
363
|
+
|
|
364
|
+
@property
|
|
365
|
+
def can_gpio(self) -> bool:
|
|
366
|
+
"""LA pin modes and GPIO (:meth:`gpio`, :meth:`la_pins`, :meth:`set_gpio`): capability ``la_pins``."""
|
|
367
|
+
return self.supports("la_pins")
|
|
368
|
+
|
|
369
|
+
@property
|
|
370
|
+
def can_trigger(self) -> bool:
|
|
371
|
+
"""Triggered captures (``trigger=``): capability ``capture_trigger``."""
|
|
372
|
+
return self.supports("capture_trigger")
|
|
373
|
+
|
|
374
|
+
@property
|
|
375
|
+
def can_spi(self) -> bool:
|
|
376
|
+
"""The SPI master and SPI NOR flash programming (:meth:`open_spi`, :meth:`spi_flash`):
|
|
377
|
+
capability ``spi_master``."""
|
|
378
|
+
return self.supports("spi_master")
|
|
379
|
+
|
|
380
|
+
@property
|
|
381
|
+
def can_profile_power(self) -> bool:
|
|
382
|
+
"""Gap-free power profiles (:meth:`measure_power`): capability ``power_profile``."""
|
|
383
|
+
return self.supports("power_profile")
|
|
384
|
+
|
|
385
|
+
@property
|
|
386
|
+
def can_calibrate(self) -> bool:
|
|
387
|
+
"""On-pod ADC calibration (:meth:`calibrate`): capability ``calibrate``."""
|
|
388
|
+
return self.supports("calibrate")
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def can_current_out(self) -> bool:
|
|
392
|
+
"""The 4-20 mA output (:meth:`current_out`): capability ``current_out``."""
|
|
393
|
+
return self.supports("current_out")
|
|
394
|
+
|
|
395
|
+
@property
|
|
396
|
+
def can_reset_target(self) -> bool:
|
|
397
|
+
"""The dedicated target-reset pin (:meth:`reset_target`, ``flash(nreset=True)``):
|
|
398
|
+
capability ``nrst_pin``."""
|
|
399
|
+
return self.supports("nrst_pin")
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def can_analog(self) -> bool:
|
|
403
|
+
"""The analog front end (ADC, DAC, 4-20 mA). ``False`` only on the digital-only board;
|
|
404
|
+
firmware that does not say is taken as analog (every older board has it)."""
|
|
405
|
+
return self.capabilities.analog is not False
|
|
406
|
+
|
|
358
407
|
def _require_capability(self, flag: str, what: str) -> None:
|
|
359
|
-
if not
|
|
408
|
+
if not self.supports(flag):
|
|
360
409
|
fw = self.capabilities.firmware_version or "unknown"
|
|
361
|
-
raise
|
|
362
|
-
|
|
410
|
+
raise UnsupportedFeatureError(
|
|
411
|
+
f"{what} need newer pod firmware (the {flag!r} capability is missing; "
|
|
412
|
+
f"this pod runs firmware {fw})",
|
|
413
|
+
feature=flag, firmware_version=self.capabilities.firmware_version)
|
|
363
414
|
|
|
364
415
|
# -- wiring profile -------------------------------------------------------
|
|
365
416
|
|