embeddedci 2.13.0__tar.gz → 2.13.2__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.
Files changed (129) hide show
  1. {embeddedci-2.13.0 → embeddedci-2.13.2}/CHANGELOG.md +28 -0
  2. {embeddedci-2.13.0 → embeddedci-2.13.2}/PKG-INFO +59 -13
  3. {embeddedci-2.13.0 → embeddedci-2.13.2}/README.md +58 -12
  4. {embeddedci-2.13.0 → embeddedci-2.13.2}/pyproject.toml +1 -1
  5. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/__init__.py +7 -1
  6. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/capabilities.py +5 -1
  7. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/client.py +136 -13
  8. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/constants.py +3 -0
  9. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/dsp.py +1 -1
  10. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/errors.py +19 -0
  11. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/replay.py +1 -1
  12. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/results.py +34 -2
  13. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/transport/tcp.py +77 -7
  14. embeddedci-2.13.2/src/embeddedci/benchpod/transport/tls.py +294 -0
  15. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/waveforms.py +1 -1
  16. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/api_surface.json +33 -5
  17. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/e2e_helpers.py +8 -0
  18. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_pod.py +7 -5
  19. embeddedci-2.13.2/tests/test_capabilities_scope.py +21 -0
  20. embeddedci-2.13.2/tests/test_dac_path_default.py +83 -0
  21. embeddedci-2.13.2/tests/test_lan_tls.py +151 -0
  22. {embeddedci-2.13.0 → embeddedci-2.13.2}/.gitignore +0 -0
  23. {embeddedci-2.13.0 → embeddedci-2.13.2}/LICENSE +0 -0
  24. {embeddedci-2.13.0 → embeddedci-2.13.2}/docs/event-uart-design.md +0 -0
  25. {embeddedci-2.13.0 → embeddedci-2.13.2}/examples/README.md +0 -0
  26. {embeddedci-2.13.0 → embeddedci-2.13.2}/examples/test_bmp280.py +0 -0
  27. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/__init__.py +0 -0
  28. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/can.py +0 -0
  29. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/capture.py +0 -0
  30. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/ci.py +0 -0
  31. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/cli_config.py +0 -0
  32. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/cli_login.py +0 -0
  33. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/cloud_auth.py +0 -0
  34. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/connection.py +0 -0
  35. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/control_loop.py +0 -0
  36. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/decode.py +0 -0
  37. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/discovery.py +0 -0
  38. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/flash.py +0 -0
  39. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/gpio.py +0 -0
  40. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/gps.py +0 -0
  41. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/i2c.py +0 -0
  42. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/lease.py +0 -0
  43. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/lowlevel.py +0 -0
  44. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/motor_emulator.py +0 -0
  45. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/power.py +0 -0
  46. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/protocol.py +0 -0
  47. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/pytest_plugin.py +0 -0
  48. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/sensor.py +0 -0
  49. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/server_api.py +0 -0
  50. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/spi.py +0 -0
  51. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/state.py +0 -0
  52. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/transport/__init__.py +0 -0
  53. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/transport/base.py +0 -0
  54. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/transport/cloud.py +0 -0
  55. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/transport/serial.py +0 -0
  56. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/transport/usblink.py +0 -0
  57. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/uart.py +0 -0
  58. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/benchpod/wiring.py +0 -0
  59. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/py.typed +0 -0
  60. {embeddedci-2.13.0 → embeddedci-2.13.2}/src/embeddedci/upload_build.py +0 -0
  61. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/conftest.py +0 -0
  62. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/README.md +0 -0
  63. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/conftest.py +0 -0
  64. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_cloud.py +0 -0
  65. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_digital.py +0 -0
  66. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_dut.py +0 -0
  67. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_gpio.py +0 -0
  68. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_la13_14.py +0 -0
  69. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_rev3.py +0 -0
  70. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_spi.py +0 -0
  71. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/e2e/test_e2e_usb.py +0 -0
  72. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/examples/test_bmp280_hil.py +0 -0
  73. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/examples/test_can_ecu_demo.py +0 -0
  74. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/examples/test_can_loopback_hil.py +0 -0
  75. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/examples/test_smoke.py +0 -0
  76. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_adc_fft_hw.py +0 -0
  77. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_api_surface.py +0 -0
  78. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_can.py +0 -0
  79. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_capabilities.py +0 -0
  80. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_capture.py +0 -0
  81. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_capture_replay_hw.py +0 -0
  82. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_ci.py +0 -0
  83. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_client_v2.py +0 -0
  84. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_cloud.py +0 -0
  85. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_connection.py +0 -0
  86. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_control_loop.py +0 -0
  87. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_control_loop_hw.py +0 -0
  88. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_current_out_waveforms.py +0 -0
  89. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_dac_limits_fixture.py +0 -0
  90. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_dac_path_ranges.py +0 -0
  91. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_decode.py +0 -0
  92. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_decode_parity.py +0 -0
  93. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_docs_coverage.py +0 -0
  94. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_dsp.py +0 -0
  95. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_flash_bridge.py +0 -0
  96. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_flash_build.py +0 -0
  97. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_generate16.py +0 -0
  98. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_gpio.py +0 -0
  99. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_i2c_decode.py +0 -0
  100. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_i2c_parity.py +0 -0
  101. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_image_switching.py +0 -0
  102. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_integration.py +0 -0
  103. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_la_voltage_fixture.py +0 -0
  104. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_lease.py +0 -0
  105. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_motor_emulator.py +0 -0
  106. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_one_source.py +0 -0
  107. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_pins_bias.py +0 -0
  108. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_protocol.py +0 -0
  109. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_refusal_errors.py +0 -0
  110. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_replay.py +0 -0
  111. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_sensor.py +0 -0
  112. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_serial_autodetect.py +0 -0
  113. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_serial_text_console.py +0 -0
  114. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_spi.py +0 -0
  115. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_tcp_transport.py +0 -0
  116. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_timed_power.py +0 -0
  117. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_timing.py +0 -0
  118. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_transport_failures.py +0 -0
  119. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_trigger_power.py +0 -0
  120. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_uart_capture.py +0 -0
  121. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_uart_session.py +0 -0
  122. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_unsupported_feature.py +0 -0
  123. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_upload_build.py +0 -0
  124. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_usb_net.py +0 -0
  125. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_waveforms.py +0 -0
  126. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/test_wiring.py +0 -0
  127. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/testdata/i2c_decode_vectors.json +0 -0
  128. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/testdata/spi_decode_vectors.json +0 -0
  129. {embeddedci-2.13.0 → embeddedci-2.13.2}/tests/testdata/uart_decode_vectors.json +0 -0
@@ -1,5 +1,33 @@
1
1
  # Changelog — `embeddedci`
2
2
 
3
+ ## 2.13.2
4
+
5
+ - Docs: `calibrate()` corrects every ADC source from firmware 3.9.3 (they share the front end) and
6
+ the pod accepts an offset up to 150 mV. On the LAN, `capture_adc` volts use the fit the pod now
7
+ reports in `status`, so they include the pod's calibrated offset.
8
+ - The LAN API over TLS (pod firmware 3.9.3+): a network address on the default port connects to
9
+ 8443 with TLS when the pod has it and falls back to plaintext 8080 only when 8443 refuses and
10
+ nothing is pinned. The pod's certificate is pinned on first use in `known_pods.json`, shared with
11
+ `benchpod-cli`; a different one raises `TlsPinMismatchError`. `BENCHPOD_TLS` (auto/on/off) and
12
+ `BENCHPOD_TLS_CA` (verify against a CA instead). New: `BenchPod.lan_tls()`,
13
+ `set_lan_tls_https_only()`, `regenerate_lan_tls()`, `set_lan_tls_cert()`, `LanTls`,
14
+ `trust_lan_tls()`, `forget_lan_tls()`, `TcpTransport.last_tls`, `TcpTransport.command_sequence()`.
15
+ The transport keeps the last TLS session per pod, so later connections resume in ~35 ms instead
16
+ of the pod's ~0.46 s full handshake.
17
+ The 8443 probe waits 1.5 s, past the host's 1 s SYN retransmit (a pod can leave the first SYN
18
+ unanswered), and a probe that times out is remembered like a refusal, so a pod without LAN TLS
19
+ costs one probe per process instead of 0.5 s on every command.
20
+
21
+ ## 2.13.1
22
+
23
+ - `Capabilities.scope` is False on the digital-only board. Current firmware lists `signal` on every
24
+ board; it only stands for the scope on firmware without the `analog` flag.
25
+ - `generate`, `replay`, `Waveforms.preview` and `replay_waveform` (for a waveform stored without
26
+ a path) default to `dac_path="3v3"` instead of `"5v"`, so a call that leaves the output out can
27
+ no longer put 5 V on a 3.3 V pin. Pass `dac_path="5v"` to keep the old output. With
28
+ `route=False` (a loopback set up with `analog_path`), `generate` and `replay` scale for the
29
+ output this connection last routed (`cal1` = 5 V, `cal2` = 12 V), else `5v` as before.
30
+
3
31
  ## 2.13.0
4
32
 
5
33
  - `generate()` on firmware 3.9.1+ (`Capabilities.generate16`) sends the volts (or microamps on
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: embeddedci
3
- Version: 2.13.0
3
+ Version: 2.13.2
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
@@ -292,6 +292,36 @@ power moved to the network too; see the compatibility table above.)
292
292
  | `replay()` / client-side `replay_waveform()` (streams the waveform to the pod) | yes | yes | no | yes |
293
293
  | Device lease | — | — | — | yes |
294
294
 
295
+ ### TLS on the LAN
296
+
297
+ Pods with firmware 3.9.3 or later also serve the API on port 8443 with TLS. For a network address
298
+ on the default port (`192.168.1.5`, `:8080`, a pod name) the SDK connects to 8443 first and falls
299
+ back to plaintext 8080 only when the pod has no TLS port and nothing is pinned for it, so older
300
+ pods keep working. `:8443` means TLS only; any other port and the USB network adapter (`usb`, a
301
+ physical link) stay plaintext. `BENCHPOD_TLS=on|off` overrides (`auto` is the default).
302
+ `bp.transport.last_tls` says which one the last connection used.
303
+
304
+ The pod's certificate is its own, so the SDK pins its SHA-256 the first time (trust on first use)
305
+ in `known_pods.json` next to `benchpod-cli`'s config; the CLI reads and writes the same file. A
306
+ different certificate later raises `TlsPinMismatchError` (`addr`, `pinned`, `got`). After a
307
+ deliberate change elsewhere, `trust_lan_tls(host, sha256=None)` pins the certificate the pod
308
+ presents now (`sha256` only accepts that one) and `forget_lan_tls(host)` drops the pin. With
309
+ `BENCHPOD_TLS_CA=ca.pem` the certificate and host name are verified against that CA instead (for a
310
+ certificate you installed, signed by your own CA).
311
+
312
+ ```python
313
+ bp = BenchPod("192.168.1.220")
314
+ info = bp.lan_tls() # LanTls: port, https_only, sessions, subject, san, sha256, ...
315
+ bp.set_lan_tls_https_only(True) # plaintext 8080 off on Ethernet and Wi-Fi (USB keeps it)
316
+ bp.set_lan_tls_cert(open("pod.pem").read(), open("pod.key").read())
317
+ bp.regenerate_lan_tls()
318
+ ```
319
+
320
+ `set_lan_tls_https_only(False)`, `regenerate_lan_tls()` and `set_lan_tls_cert()` need a TLS, USB or
321
+ cloud connection; over plaintext Ethernet or Wi-Fi the pod refuses (`PodLockedError`).
322
+ `set_lan_tls_cert` needs a network connection (it uploads the bundle); after it and after
323
+ `regenerate_lan_tls()` the SDK pins the new certificate for the address it used.
324
+
295
325
  ### Environment variables
296
326
 
297
327
  | Variable | Used when | Purpose |
@@ -304,6 +334,8 @@ power moved to the network too; see the compatibility table above.)
304
334
  | `BENCHPOD_WIRING` | no `wiring` argument / `--benchpod-wiring` / `benchpod_wiring` fixture | wiring profile file (`.json`/`.toml`), see [Wiring profile](#wiring-profile) |
305
335
  | `BENCHPOD_WIRING_SOURCE` | no `wiring_source` argument | `local` never asks embeddedci.com for the pod's stored wiring profile; `auto` (default) does, see [Wiring profile](#wiring-profile) |
306
336
  | `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) |
337
+ | `BENCHPOD_TLS` | network connections | `auto` (default), `on` or `off`: TLS to the pod's LAN API on 8443, see [TLS on the LAN](#tls-on-the-lan) |
338
+ | `BENCHPOD_TLS_CA` | network connections over TLS | PEM file of CA certificates: verify the pod's certificate and host name instead of pinning it |
307
339
  | `BENCHPOD_STALL_TIMEOUT` | flashing | seconds with no SWD traffic before a flash attempt is aborted and retried (default 60) |
308
340
 
309
341
  ## API conventions and stability
@@ -989,11 +1021,12 @@ microvolt resolution (about 76 µV a step on `"5v"`) and `code16` is the 16-bit
989
1021
  stays the nearest 8-bit level. On older firmware `code16` is `None` and the steps are 8-bit. `adc_read` routes the source (`"ext"`, `"cal1"`, `"cal2"`, `"current_in"`) and returns one averaged,
990
1022
  calibrated reading; the pod refuses it while the input is still moving (e.g. a DAC left running).
991
1023
 
992
- ### Calibrating the 4-20 mA input
1024
+ ### Calibrating the ADC offset
993
1025
 
994
1026
  `"current_in"` reads the 4-20 mA terminal (J8) across a 249 Ω resistor, so 4 mA is 0.996 V and 20 mA is
995
- 4.98 V. Each pod is off by a few mV there. The pod can measure that offset itself and keep it in
996
- its flash:
1027
+ 4.98 V. Each pod's analog input has its own offset, from part tolerances: anywhere from a few mV
1028
+ to about 50 mV. With J8 open the terminal is at 0 V, so the pod can measure that offset itself and
1029
+ keep it in its flash:
997
1030
 
998
1031
  ```python
999
1032
  cal = bp.calibrate() # disconnect J8 first. Calibration(source, calibrated, offset, a, b, ...)
@@ -1006,10 +1039,15 @@ bp.clear_calibration() # back to the built-in fit
1006
1039
  ```
1007
1040
 
1008
1041
  Calibrate once per pod: it survives a reboot and a firmware update. The pod refuses
1009
- (`BenchPodError`) when something is driving J8 (more than 50 mV) and keeps what it had. `a` and `b`
1010
- are the fit the pod now uses for `current_in` (`volts = a + b * count`). Only the offset of `current_in` is
1011
- calibrated; the other sources use the built-in fits. Needs firmware 3.4.0 (capability
1012
- `calibrate`, `Capabilities.calibrate`).
1042
+ (`BenchPodError`) when something is driving J8 (more than 150 mV; 50 mV before firmware 3.9.3)
1043
+ and keeps what it had. `a` and `b` are the fit the pod now uses for `current_in`
1044
+ (`volts = a + b * count`).
1045
+
1046
+ Every ADC source shares the same front end after its input switch, so from firmware 3.9.3 the pod
1047
+ takes the offset out of all of them: `adc_read` on `ext`, `cal1` and `cal2`, and `capture_adc`,
1048
+ whose `volts` use the fit the pod reports (on the LAN in `status`, through embeddedci.com in its
1049
+ capabilities). Older firmware corrects `current_in` only. Only the offset is calibrated; the gain
1050
+ uses the built-in fits. Needs firmware 3.4.0 (capability `calibrate`, `Capabilities.calibrate`).
1013
1051
 
1014
1052
  ### The 4-20 mA output
1015
1053
 
@@ -1176,7 +1214,12 @@ v35+ (`Capabilities.capture_trigger`).
1176
1214
 
1177
1215
  Every DAC output keeps running until stopped. The returned `DacHandle` / `ReplayHandle` is a
1178
1216
  context manager that stops it on exit; `bp.dac_stop()` stops any output (generator, replay or
1179
- control loop). DAC paths are `"3v3"`, `"5v"` and `"12v"`.
1217
+ control loop). DAC paths are `"3v3"`, `"5v"` and `"12v"`. `generate`, `replay` and
1218
+ `replay_waveform` (for a waveform stored without a path) default to `"3v3"`, the lowest voltage
1219
+ output: pass the output your DUT pin tolerates, since `"5v"` and `"12v"` can drive more than a
1220
+ 3.3 V pin takes. With `route=False` nothing is switched and the path only scales the volts, so the
1221
+ default is the output this connection last routed (`analog_path("cal1")` is the 5 V DAC,
1222
+ `"cal2"` the 12 V one), else `"5v"`.
1180
1223
 
1181
1224
  ```python
1182
1225
  from embeddedci.benchpod import Fault
@@ -1203,11 +1246,11 @@ with bp.replay(ramp, dac_path="3v3", sample_rate_hz=10_000,
1203
1246
  print(r.samples, r.sample_rate_hz, r.deep)
1204
1247
  ```
1205
1248
 
1206
- * `generate(waveform, *, freq_hz, amplitude, offset=None, dac_path="5v", duration=None,
1249
+ * `generate(waveform, *, freq_hz, amplitude, offset=None, dac_path=None, duration=None,
1207
1250
  sample_rate_hz=None, on_capture=False)` — `sine`, `square` or `sawtooth`. `amplitude` is the peak
1208
- and `offset` the centre (default: mid-range of the path). The firmware builds the waveform from
1251
+ and `offset` the center (default: mid-range of the path). The firmware builds the waveform from
1209
1252
  8-bit levels, so volts are quantised to full-scale / 255.
1210
- * `replay(source, *, dac_path="5v", mapping="faithful", sample_rate_hz=None, deep=None, fault=None,
1253
+ * `replay(source, *, dac_path=None, mapping="faithful", sample_rate_hz=None, deep=None, fault=None,
1211
1254
  are_codes=False, on_capture=False)` — `source` is a `Capture` (its volts, at its own sample rate
1212
1255
  by default), a sequence of volts, or raw DAC codes with `are_codes=True`. `mapping="faithful"`
1213
1256
  reproduces the voltage (clipping outside the path's range), `"fit"` auto-scales. Above 2048 samples
@@ -1534,6 +1577,7 @@ Invalid arguments raise `ValueError` before anything is sent. Everything else ra
1534
1577
  | `TransportError` | the pod or tunnel could not be reached / talked to | |
1535
1578
  | `TransportTimeout` (a `TransportError` and a `TimeoutError`) | the pod or tunnel did not answer in time | |
1536
1579
  | `ConnectionClosedError` (a `TransportError`) | the connection ended or reset before the reply | |
1580
+ | `TlsPinMismatchError` (a `TransportError`) | the pod's TLS certificate is not the one pinned for it (see [TLS on the LAN](#tls-on-the-lan)) | `addr`, `pinned`, `got` |
1537
1581
  | `FirmwareError` | the pod replied with an error (e.g. "la voltage not set"); a fix the firmware spells as a raw protocol command is rewritten into the SDK call in the message, and `firmware_message` keeps the pod's own text | `firmware_message`, `cmd` |
1538
1582
  | `FlashError` | OpenOCD failed, is missing, or lacks the TCP backend | |
1539
1583
  | `TargetUnreachableError` (a `FlashError`) | the probe worked but no target answered on SWD | |
@@ -1578,7 +1622,7 @@ lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is
1578
1622
 
1579
1623
  | Area | Methods and properties |
1580
1624
  |---|---|
1581
- | 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()`, `usb_network(on=None)` (the USB network setting; `False` turns the adapter off over the USB link) |
1625
+ | 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()`, `usb_network(on=None)` (the USB network setting; `False` turns the adapter off over the USB link), `lan_tls()`, `set_lan_tls_https_only(on)`, `regenerate_lan_tls()`, `set_lan_tls_cert(cert_pem, key_pem)` (the LAN API over TLS, see [TLS on the LAN](#tls-on-the-lan)) |
1582
1626
  | LA voltage | `set_la_voltage(volts)`, `get_la_voltage()` |
1583
1627
  | 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()` |
1584
1628
  | Flashing | `flash(...)`, `spi_flash(image, addr=0, *, erase=True, verify=True, hold_reset=False, sck=, mosi=, miso=, cs=)`, `open_spi(...)`, `open_motor_emulator(...)` |
@@ -1610,6 +1654,7 @@ few built from other shapes); `to_dict()` goes the other way where a type is sen
1610
1654
  | `PowerProfile` | `measure_power`, `PowerProfileSession.result` | see [Power profiles](#power-profiles) |
1611
1655
  | `PowerProfileSession` | `power_profile` (a context manager) | `start()`, `stop()`, `status()`, `result` |
1612
1656
  | `ResetState` | `reset_target`, `set_reset`, `reset_state` | `asserted`, `supported` |
1657
+ | `LanTls` | `lan_tls`, `set_lan_tls_https_only`, `regenerate_lan_tls`, `set_lan_tls_cert` | `port`, `https_only`, `sessions`, `origin` (`generated`/`user`), `key`, `subject`, `san`, `not_after`, `sha256` (what clients pin), `error` (no certificate) |
1613
1658
  | `UsbCcStatus` | `usb_cc` | `orientation`, `advertised`, `advertised_current` (A), `cc1_voltage`, `cc2_voltage` (V) |
1614
1659
  | `PullState` | `pull_state`, `set_pull` | `la`, `enabled`, `direction`, `ohms`, `available` |
1615
1660
  | `LaPinState` | `la_pins`, `configure_gpio` | `la`, `function`, `gpio`, `level`, `pull`, `pull_ohms`, `pull_on`, `in_use` |
@@ -1710,6 +1755,7 @@ timeout=60.0, lease_id=None)`. Errors raise `ServerApiError` (`status`), and an
1710
1755
  |---|---|
1711
1756
  | `parse_connection(raw)` | parse a connection string into a `ConnSpec` (raises `ConnectionConfigError`) |
1712
1757
  | `resolve_connection(connection=None)` | the same, falling back to `BENCHPOD_CONNECTION` |
1758
+ | `trust_lan_tls(host, port=8080, *, sha256=None)`, `forget_lan_tls(host, port=8080)` | pin the TLS certificate the pod presents now (returns its SHA-256), or drop the pin (see [TLS on the LAN](#tls-on-the-lan)) |
1713
1759
  | `make_build_reporter(*, api_base=None, audience=None, target="", name="")` | a `BuildReporter` inside GitHub Actions, else a `NoopBuildReporter` |
1714
1760
  | `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)) |
1715
1761
  | `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 |
@@ -254,6 +254,36 @@ power moved to the network too; see the compatibility table above.)
254
254
  | `replay()` / client-side `replay_waveform()` (streams the waveform to the pod) | yes | yes | no | yes |
255
255
  | Device lease | — | — | — | yes |
256
256
 
257
+ ### TLS on the LAN
258
+
259
+ Pods with firmware 3.9.3 or later also serve the API on port 8443 with TLS. For a network address
260
+ on the default port (`192.168.1.5`, `:8080`, a pod name) the SDK connects to 8443 first and falls
261
+ back to plaintext 8080 only when the pod has no TLS port and nothing is pinned for it, so older
262
+ pods keep working. `:8443` means TLS only; any other port and the USB network adapter (`usb`, a
263
+ physical link) stay plaintext. `BENCHPOD_TLS=on|off` overrides (`auto` is the default).
264
+ `bp.transport.last_tls` says which one the last connection used.
265
+
266
+ The pod's certificate is its own, so the SDK pins its SHA-256 the first time (trust on first use)
267
+ in `known_pods.json` next to `benchpod-cli`'s config; the CLI reads and writes the same file. A
268
+ different certificate later raises `TlsPinMismatchError` (`addr`, `pinned`, `got`). After a
269
+ deliberate change elsewhere, `trust_lan_tls(host, sha256=None)` pins the certificate the pod
270
+ presents now (`sha256` only accepts that one) and `forget_lan_tls(host)` drops the pin. With
271
+ `BENCHPOD_TLS_CA=ca.pem` the certificate and host name are verified against that CA instead (for a
272
+ certificate you installed, signed by your own CA).
273
+
274
+ ```python
275
+ bp = BenchPod("192.168.1.220")
276
+ info = bp.lan_tls() # LanTls: port, https_only, sessions, subject, san, sha256, ...
277
+ bp.set_lan_tls_https_only(True) # plaintext 8080 off on Ethernet and Wi-Fi (USB keeps it)
278
+ bp.set_lan_tls_cert(open("pod.pem").read(), open("pod.key").read())
279
+ bp.regenerate_lan_tls()
280
+ ```
281
+
282
+ `set_lan_tls_https_only(False)`, `regenerate_lan_tls()` and `set_lan_tls_cert()` need a TLS, USB or
283
+ cloud connection; over plaintext Ethernet or Wi-Fi the pod refuses (`PodLockedError`).
284
+ `set_lan_tls_cert` needs a network connection (it uploads the bundle); after it and after
285
+ `regenerate_lan_tls()` the SDK pins the new certificate for the address it used.
286
+
257
287
  ### Environment variables
258
288
 
259
289
  | Variable | Used when | Purpose |
@@ -266,6 +296,8 @@ power moved to the network too; see the compatibility table above.)
266
296
  | `BENCHPOD_WIRING` | no `wiring` argument / `--benchpod-wiring` / `benchpod_wiring` fixture | wiring profile file (`.json`/`.toml`), see [Wiring profile](#wiring-profile) |
267
297
  | `BENCHPOD_WIRING_SOURCE` | no `wiring_source` argument | `local` never asks embeddedci.com for the pod's stored wiring profile; `auto` (default) does, see [Wiring profile](#wiring-profile) |
268
298
  | `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) |
299
+ | `BENCHPOD_TLS` | network connections | `auto` (default), `on` or `off`: TLS to the pod's LAN API on 8443, see [TLS on the LAN](#tls-on-the-lan) |
300
+ | `BENCHPOD_TLS_CA` | network connections over TLS | PEM file of CA certificates: verify the pod's certificate and host name instead of pinning it |
269
301
  | `BENCHPOD_STALL_TIMEOUT` | flashing | seconds with no SWD traffic before a flash attempt is aborted and retried (default 60) |
270
302
 
271
303
  ## API conventions and stability
@@ -951,11 +983,12 @@ microvolt resolution (about 76 µV a step on `"5v"`) and `code16` is the 16-bit
951
983
  stays the nearest 8-bit level. On older firmware `code16` is `None` and the steps are 8-bit. `adc_read` routes the source (`"ext"`, `"cal1"`, `"cal2"`, `"current_in"`) and returns one averaged,
952
984
  calibrated reading; the pod refuses it while the input is still moving (e.g. a DAC left running).
953
985
 
954
- ### Calibrating the 4-20 mA input
986
+ ### Calibrating the ADC offset
955
987
 
956
988
  `"current_in"` reads the 4-20 mA terminal (J8) across a 249 Ω resistor, so 4 mA is 0.996 V and 20 mA is
957
- 4.98 V. Each pod is off by a few mV there. The pod can measure that offset itself and keep it in
958
- its flash:
989
+ 4.98 V. Each pod's analog input has its own offset, from part tolerances: anywhere from a few mV
990
+ to about 50 mV. With J8 open the terminal is at 0 V, so the pod can measure that offset itself and
991
+ keep it in its flash:
959
992
 
960
993
  ```python
961
994
  cal = bp.calibrate() # disconnect J8 first. Calibration(source, calibrated, offset, a, b, ...)
@@ -968,10 +1001,15 @@ bp.clear_calibration() # back to the built-in fit
968
1001
  ```
969
1002
 
970
1003
  Calibrate once per pod: it survives a reboot and a firmware update. The pod refuses
971
- (`BenchPodError`) when something is driving J8 (more than 50 mV) and keeps what it had. `a` and `b`
972
- are the fit the pod now uses for `current_in` (`volts = a + b * count`). Only the offset of `current_in` is
973
- calibrated; the other sources use the built-in fits. Needs firmware 3.4.0 (capability
974
- `calibrate`, `Capabilities.calibrate`).
1004
+ (`BenchPodError`) when something is driving J8 (more than 150 mV; 50 mV before firmware 3.9.3)
1005
+ and keeps what it had. `a` and `b` are the fit the pod now uses for `current_in`
1006
+ (`volts = a + b * count`).
1007
+
1008
+ Every ADC source shares the same front end after its input switch, so from firmware 3.9.3 the pod
1009
+ takes the offset out of all of them: `adc_read` on `ext`, `cal1` and `cal2`, and `capture_adc`,
1010
+ whose `volts` use the fit the pod reports (on the LAN in `status`, through embeddedci.com in its
1011
+ capabilities). Older firmware corrects `current_in` only. Only the offset is calibrated; the gain
1012
+ uses the built-in fits. Needs firmware 3.4.0 (capability `calibrate`, `Capabilities.calibrate`).
975
1013
 
976
1014
  ### The 4-20 mA output
977
1015
 
@@ -1138,7 +1176,12 @@ v35+ (`Capabilities.capture_trigger`).
1138
1176
 
1139
1177
  Every DAC output keeps running until stopped. The returned `DacHandle` / `ReplayHandle` is a
1140
1178
  context manager that stops it on exit; `bp.dac_stop()` stops any output (generator, replay or
1141
- control loop). DAC paths are `"3v3"`, `"5v"` and `"12v"`.
1179
+ control loop). DAC paths are `"3v3"`, `"5v"` and `"12v"`. `generate`, `replay` and
1180
+ `replay_waveform` (for a waveform stored without a path) default to `"3v3"`, the lowest voltage
1181
+ output: pass the output your DUT pin tolerates, since `"5v"` and `"12v"` can drive more than a
1182
+ 3.3 V pin takes. With `route=False` nothing is switched and the path only scales the volts, so the
1183
+ default is the output this connection last routed (`analog_path("cal1")` is the 5 V DAC,
1184
+ `"cal2"` the 12 V one), else `"5v"`.
1142
1185
 
1143
1186
  ```python
1144
1187
  from embeddedci.benchpod import Fault
@@ -1165,11 +1208,11 @@ with bp.replay(ramp, dac_path="3v3", sample_rate_hz=10_000,
1165
1208
  print(r.samples, r.sample_rate_hz, r.deep)
1166
1209
  ```
1167
1210
 
1168
- * `generate(waveform, *, freq_hz, amplitude, offset=None, dac_path="5v", duration=None,
1211
+ * `generate(waveform, *, freq_hz, amplitude, offset=None, dac_path=None, duration=None,
1169
1212
  sample_rate_hz=None, on_capture=False)` — `sine`, `square` or `sawtooth`. `amplitude` is the peak
1170
- and `offset` the centre (default: mid-range of the path). The firmware builds the waveform from
1213
+ and `offset` the center (default: mid-range of the path). The firmware builds the waveform from
1171
1214
  8-bit levels, so volts are quantised to full-scale / 255.
1172
- * `replay(source, *, dac_path="5v", mapping="faithful", sample_rate_hz=None, deep=None, fault=None,
1215
+ * `replay(source, *, dac_path=None, mapping="faithful", sample_rate_hz=None, deep=None, fault=None,
1173
1216
  are_codes=False, on_capture=False)` — `source` is a `Capture` (its volts, at its own sample rate
1174
1217
  by default), a sequence of volts, or raw DAC codes with `are_codes=True`. `mapping="faithful"`
1175
1218
  reproduces the voltage (clipping outside the path's range), `"fit"` auto-scales. Above 2048 samples
@@ -1496,6 +1539,7 @@ Invalid arguments raise `ValueError` before anything is sent. Everything else ra
1496
1539
  | `TransportError` | the pod or tunnel could not be reached / talked to | |
1497
1540
  | `TransportTimeout` (a `TransportError` and a `TimeoutError`) | the pod or tunnel did not answer in time | |
1498
1541
  | `ConnectionClosedError` (a `TransportError`) | the connection ended or reset before the reply | |
1542
+ | `TlsPinMismatchError` (a `TransportError`) | the pod's TLS certificate is not the one pinned for it (see [TLS on the LAN](#tls-on-the-lan)) | `addr`, `pinned`, `got` |
1499
1543
  | `FirmwareError` | the pod replied with an error (e.g. "la voltage not set"); a fix the firmware spells as a raw protocol command is rewritten into the SDK call in the message, and `firmware_message` keeps the pod's own text | `firmware_message`, `cmd` |
1500
1544
  | `FlashError` | OpenOCD failed, is missing, or lacks the TCP backend | |
1501
1545
  | `TargetUnreachableError` (a `FlashError`) | the probe worked but no target answered on SWD | |
@@ -1540,7 +1584,7 @@ lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is
1540
1584
 
1541
1585
  | Area | Methods and properties |
1542
1586
  |---|---|
1543
- | 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()`, `usb_network(on=None)` (the USB network setting; `False` turns the adapter off over the USB link) |
1587
+ | 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()`, `usb_network(on=None)` (the USB network setting; `False` turns the adapter off over the USB link), `lan_tls()`, `set_lan_tls_https_only(on)`, `regenerate_lan_tls()`, `set_lan_tls_cert(cert_pem, key_pem)` (the LAN API over TLS, see [TLS on the LAN](#tls-on-the-lan)) |
1544
1588
  | LA voltage | `set_la_voltage(volts)`, `get_la_voltage()` |
1545
1589
  | 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()` |
1546
1590
  | Flashing | `flash(...)`, `spi_flash(image, addr=0, *, erase=True, verify=True, hold_reset=False, sck=, mosi=, miso=, cs=)`, `open_spi(...)`, `open_motor_emulator(...)` |
@@ -1572,6 +1616,7 @@ few built from other shapes); `to_dict()` goes the other way where a type is sen
1572
1616
  | `PowerProfile` | `measure_power`, `PowerProfileSession.result` | see [Power profiles](#power-profiles) |
1573
1617
  | `PowerProfileSession` | `power_profile` (a context manager) | `start()`, `stop()`, `status()`, `result` |
1574
1618
  | `ResetState` | `reset_target`, `set_reset`, `reset_state` | `asserted`, `supported` |
1619
+ | `LanTls` | `lan_tls`, `set_lan_tls_https_only`, `regenerate_lan_tls`, `set_lan_tls_cert` | `port`, `https_only`, `sessions`, `origin` (`generated`/`user`), `key`, `subject`, `san`, `not_after`, `sha256` (what clients pin), `error` (no certificate) |
1575
1620
  | `UsbCcStatus` | `usb_cc` | `orientation`, `advertised`, `advertised_current` (A), `cc1_voltage`, `cc2_voltage` (V) |
1576
1621
  | `PullState` | `pull_state`, `set_pull` | `la`, `enabled`, `direction`, `ohms`, `available` |
1577
1622
  | `LaPinState` | `la_pins`, `configure_gpio` | `la`, `function`, `gpio`, `level`, `pull`, `pull_ohms`, `pull_on`, `in_use` |
@@ -1672,6 +1717,7 @@ timeout=60.0, lease_id=None)`. Errors raise `ServerApiError` (`status`), and an
1672
1717
  |---|---|
1673
1718
  | `parse_connection(raw)` | parse a connection string into a `ConnSpec` (raises `ConnectionConfigError`) |
1674
1719
  | `resolve_connection(connection=None)` | the same, falling back to `BENCHPOD_CONNECTION` |
1720
+ | `trust_lan_tls(host, port=8080, *, sha256=None)`, `forget_lan_tls(host, port=8080)` | pin the TLS certificate the pod presents now (returns its SHA-256), or drop the pin (see [TLS on the LAN](#tls-on-the-lan)) |
1675
1721
  | `make_build_reporter(*, api_base=None, audience=None, target="", name="")` | a `BuildReporter` inside GitHub Actions, else a `NoopBuildReporter` |
1676
1722
  | `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)) |
1677
1723
  | `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 |
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "embeddedci"
7
- version = "2.13.0"
7
+ version = "2.13.2"
8
8
  description = "BenchPod SDK and pytest plugin for EmbeddedCI hardware-in-the-loop testing"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -95,6 +95,7 @@ from .errors import (
95
95
  PodLockedError,
96
96
  PullConflictError,
97
97
  TargetUnreachableError,
98
+ TlsPinMismatchError,
98
99
  TransportError,
99
100
  TransportTimeout,
100
101
  TriggerTimeout,
@@ -109,7 +110,8 @@ from .lease import DeviceLease
109
110
  from .lowlevel import LowLevel
110
111
  from .power import PowerProfile, PowerProfileSession
111
112
  from .replay import DacHandle, Fault, ReplayHandle, Segment
112
- from .results import Capture, CorrelatedCapture, LaCapture, Trigger
113
+ from .results import Capture, CorrelatedCapture, LaCapture, LanTls, Trigger
114
+ from .transport.tls import delete_pin as forget_lan_tls, trust_lan_tls
113
115
  from .server_api import ServerApi, ServerApiError, ServerPermissionDeniedError
114
116
  from .state import (
115
117
  AdcReading,
@@ -190,6 +192,10 @@ __all__ = [
190
192
  "dsp",
191
193
  "Capabilities",
192
194
  "Capture",
195
+ "LanTls",
196
+ "TlsPinMismatchError",
197
+ "trust_lan_tls",
198
+ "forget_lan_tls",
193
199
  "LaCapture",
194
200
  "CorrelatedCapture",
195
201
  "Trigger",
@@ -285,7 +285,11 @@ class Capabilities:
285
285
  caps = status.get("caps")
286
286
  if isinstance(caps, (list, tuple)):
287
287
  names = {str(x).lower() for x in caps}
288
- c.scope = c.scope or ("signal" in names or "scope" in names)
288
+ # "signal" stood for the scope on firmware that predates the analog flag. Current
289
+ # firmware lists it on every board, the digital-only one included, and names "scope"
290
+ # itself, so it only counts when the status has no analog flag.
291
+ legacy = _as_bool(status, "analog") is None
292
+ c.scope = c.scope or "scope" in names or (legacy and "signal" in names)
289
293
  c.analyzer = c.analyzer or ("la" in names or "analyzer" in names)
290
294
  c.serial = c.serial or ("uart" in names or "serial" in names)
291
295
  # The DAC/gateware features. Firmware advertises these in status.caps[] built from