embeddedci 2.13.2__tar.gz → 2.13.4__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 (130) hide show
  1. {embeddedci-2.13.2 → embeddedci-2.13.4}/CHANGELOG.md +24 -0
  2. {embeddedci-2.13.2 → embeddedci-2.13.4}/PKG-INFO +88 -13
  3. {embeddedci-2.13.2 → embeddedci-2.13.4}/README.md +87 -12
  4. {embeddedci-2.13.2 → embeddedci-2.13.4}/pyproject.toml +1 -1
  5. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/__init__.py +16 -0
  6. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/capabilities.py +27 -1
  7. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/capture.py +3 -2
  8. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/client.py +176 -16
  9. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/constants.py +14 -0
  10. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/results.py +17 -0
  11. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/state.py +132 -6
  12. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/transport/cloud.py +2 -1
  13. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/wiring.py +1 -1
  14. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/api_surface.json +76 -5
  15. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_pod.py +29 -0
  16. embeddedci-2.13.4/tests/test_adc_range.py +218 -0
  17. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_client_v2.py +62 -0
  18. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_current_out_waveforms.py +3 -3
  19. {embeddedci-2.13.2 → embeddedci-2.13.4}/.gitignore +0 -0
  20. {embeddedci-2.13.2 → embeddedci-2.13.4}/LICENSE +0 -0
  21. {embeddedci-2.13.2 → embeddedci-2.13.4}/docs/event-uart-design.md +0 -0
  22. {embeddedci-2.13.2 → embeddedci-2.13.4}/examples/README.md +0 -0
  23. {embeddedci-2.13.2 → embeddedci-2.13.4}/examples/test_bmp280.py +0 -0
  24. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/__init__.py +0 -0
  25. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/can.py +0 -0
  26. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/ci.py +0 -0
  27. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/cli_config.py +0 -0
  28. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/cli_login.py +0 -0
  29. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/cloud_auth.py +0 -0
  30. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/connection.py +0 -0
  31. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/control_loop.py +0 -0
  32. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/decode.py +0 -0
  33. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/discovery.py +0 -0
  34. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/dsp.py +0 -0
  35. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/errors.py +0 -0
  36. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/flash.py +0 -0
  37. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/gpio.py +0 -0
  38. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/gps.py +0 -0
  39. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/i2c.py +0 -0
  40. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/lease.py +0 -0
  41. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/lowlevel.py +0 -0
  42. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/motor_emulator.py +0 -0
  43. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/power.py +0 -0
  44. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/protocol.py +0 -0
  45. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/pytest_plugin.py +0 -0
  46. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/replay.py +0 -0
  47. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/sensor.py +0 -0
  48. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/server_api.py +0 -0
  49. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/spi.py +0 -0
  50. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/transport/__init__.py +0 -0
  51. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/transport/base.py +0 -0
  52. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/transport/serial.py +0 -0
  53. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/transport/tcp.py +0 -0
  54. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/transport/tls.py +0 -0
  55. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/transport/usblink.py +0 -0
  56. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/uart.py +0 -0
  57. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/benchpod/waveforms.py +0 -0
  58. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/py.typed +0 -0
  59. {embeddedci-2.13.2 → embeddedci-2.13.4}/src/embeddedci/upload_build.py +0 -0
  60. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/conftest.py +0 -0
  61. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/README.md +0 -0
  62. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/conftest.py +0 -0
  63. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/e2e_helpers.py +0 -0
  64. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_cloud.py +0 -0
  65. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_digital.py +0 -0
  66. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_dut.py +0 -0
  67. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_gpio.py +0 -0
  68. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_la13_14.py +0 -0
  69. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_rev3.py +0 -0
  70. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_spi.py +0 -0
  71. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/e2e/test_e2e_usb.py +0 -0
  72. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/examples/test_bmp280_hil.py +0 -0
  73. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/examples/test_can_ecu_demo.py +0 -0
  74. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/examples/test_can_loopback_hil.py +0 -0
  75. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/examples/test_smoke.py +0 -0
  76. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_adc_fft_hw.py +0 -0
  77. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_api_surface.py +0 -0
  78. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_can.py +0 -0
  79. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_capabilities.py +0 -0
  80. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_capabilities_scope.py +0 -0
  81. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_capture.py +0 -0
  82. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_capture_replay_hw.py +0 -0
  83. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_ci.py +0 -0
  84. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_cloud.py +0 -0
  85. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_connection.py +0 -0
  86. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_control_loop.py +0 -0
  87. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_control_loop_hw.py +0 -0
  88. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_dac_limits_fixture.py +0 -0
  89. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_dac_path_default.py +0 -0
  90. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_dac_path_ranges.py +0 -0
  91. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_decode.py +0 -0
  92. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_decode_parity.py +0 -0
  93. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_docs_coverage.py +0 -0
  94. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_dsp.py +0 -0
  95. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_flash_bridge.py +0 -0
  96. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_flash_build.py +0 -0
  97. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_generate16.py +0 -0
  98. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_gpio.py +0 -0
  99. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_i2c_decode.py +0 -0
  100. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_i2c_parity.py +0 -0
  101. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_image_switching.py +0 -0
  102. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_integration.py +0 -0
  103. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_la_voltage_fixture.py +0 -0
  104. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_lan_tls.py +0 -0
  105. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_lease.py +0 -0
  106. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_motor_emulator.py +0 -0
  107. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_one_source.py +0 -0
  108. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_pins_bias.py +0 -0
  109. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_protocol.py +0 -0
  110. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_refusal_errors.py +0 -0
  111. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_replay.py +0 -0
  112. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_sensor.py +0 -0
  113. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_serial_autodetect.py +0 -0
  114. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_serial_text_console.py +0 -0
  115. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_spi.py +0 -0
  116. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_tcp_transport.py +0 -0
  117. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_timed_power.py +0 -0
  118. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_timing.py +0 -0
  119. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_transport_failures.py +0 -0
  120. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_trigger_power.py +0 -0
  121. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_uart_capture.py +0 -0
  122. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_uart_session.py +0 -0
  123. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_unsupported_feature.py +0 -0
  124. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_upload_build.py +0 -0
  125. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_usb_net.py +0 -0
  126. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_waveforms.py +0 -0
  127. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/test_wiring.py +0 -0
  128. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/testdata/i2c_decode_vectors.json +0 -0
  129. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/testdata/spi_decode_vectors.json +0 -0
  130. {embeddedci-2.13.2 → embeddedci-2.13.4}/tests/testdata/uart_decode_vectors.json +0 -0
@@ -1,5 +1,29 @@
1
1
  # Changelog — `embeddedci`
2
2
 
3
+ ## 2.13.4
4
+
5
+ - The ADC input range (pod firmware 3.9.5+, analog module v3r4, `Capabilities.adc_low_range`):
6
+ `BenchPod.adc_range(range=None)` reads or switches it (`"normal"` ±32.8 V, `"low"` ±5.46 V at
7
+ about 6x the resolution), `adc_range_test()` self-tests it through the cal1 loopback, and
8
+ `analog_path`, `adc_read`, `capture_adc` take `range=`. `calibrate_adc_gain("cal1", range="low")`
9
+ measures the low range's gain (`Calibration.adc_low_gain_ppm`). Captures scale with the fit of
10
+ the range in use: the SDK re-reads it whenever a reply says the pod changed range.
11
+ `AdcReading` gains `range`, `fullscale`, `overrange`; `Capture` gains `range`, `overrange`,
12
+ `clipped_samples`; `AnalogPathState.range`; `Capabilities.adc_range`, `adc_range_fullscale`.
13
+ New types `AdcRange`, `AdcRangeState`, `AdcRangeTest`, `AdcRangeTestPoint`.
14
+ - `capture_adc(source="current_in")` reads the pod's `current_in` fit after routing, so it is the
15
+ fit of the range in use.
16
+
17
+ ## 2.13.3
18
+
19
+ - The DMM-guided calibration (pod firmware 3.9.4+, `Capabilities.cal_dmm`):
20
+ `BenchPod.calibrate_dac(path, points)` stores this pod's own line for a DAC output from two
21
+ `(code16, volts)` DMM readings, `calibrate_adc_gain(source)` measures an ADC gain (`cal1`,
22
+ `cal2`, `ext`) through a loopback, and `clear_calibration(part)` clears one part (`offset`,
23
+ `dac`, `adc`). `Calibration` gains `dac` (`DacLine`), `adc_gain_ppm`, `dmm` and `measured`;
24
+ `BenchPod.DMM_SET_POINTS` has the set points. New types `DacCalPath`, `AdcGainSource`,
25
+ `CalibrationPart`. `Calibration.zero_count` is the raw J8-open zero the pod derives its offset from.
26
+
3
27
  ## 2.13.2
4
28
 
5
29
  - Docs: `calibrate()` corrects every ADC source from firmware 3.9.3 (they share the front end) and
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: embeddedci
3
- Version: 2.13.2
3
+ Version: 2.13.4
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
@@ -1021,6 +1021,40 @@ microvolt resolution (about 76 µV a step on `"5v"`) and `code16` is the 16-bit
1021
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,
1022
1022
  calibrated reading; the pod refuses it while the input is still moving (e.g. a DAC left running).
1023
1023
 
1024
+ ### The ADC input range (analog module v3r4)
1025
+
1026
+ Analog module v3r4 (firmware 3.9.5+, `Capabilities.adc_low_range`) adds a ÷2 low range to the
1027
+ ADC input next to the normal ÷12 one:
1028
+
1029
+ | Range | Full scale | Resolution | Input impedance |
1030
+ |---|---|---|---|
1031
+ | `"normal"` (default) | ±32.8 V | ~1 mV per count | ~1 MΩ |
1032
+ | `"low"` | ±5.46 V | ~167 µV per count | 1.82 MΩ: use 1x leads, not 10x probes |
1033
+
1034
+ ```python
1035
+ st = bp.adc_range("low") # AdcRangeState(range, supported, fullscale, ...)
1036
+ r = bp.adc_read("ext") # read in the low range: r.range == "low"
1037
+ r = bp.adc_read("current_in", range="low") # or switch it with the reading
1038
+ cap = bp.capture_adc(4096, source="ext", range="low")
1039
+ print(cap.range, cap.overrange) # a clipped sample: capture again in the normal range
1040
+ bp.adc_range("normal")
1041
+ print(bp.adc_range_test().passed) # self-test through the cal1 loopback (5 V output)
1042
+ ```
1043
+
1044
+ The range applies to `ext`, `cal1` and `current_in` (4-20 mA fits inside it); `cal2` has none.
1045
+ It stays until it is changed, `analog_path("off")` or `analog_path("cal2")`, or the pod restarts.
1046
+ `analog_path`, `adc_read` and `capture_adc` take `range=` too. Captures are raw counts: the SDK
1047
+ keeps the fit it scales them with on the range in use (`Capabilities.adc_range`), and re-reads it
1048
+ whenever a reply says the pod moved to the other range. `AdcReading.overrange` and
1049
+ `Capture.overrange` (with `clipped_samples`) flag samples on the ADC's rails. A board without the
1050
+ low range raises `BenchPodError` for `"low"`.
1051
+
1052
+ `adc_range_test()` drives the 5 V output into `cal1` at 1 V and 3.5 V, reads each point in both
1053
+ ranges, and fails (`passed` is `False`) when they disagree by more than 5 % + 30 mV. The low range's
1054
+ gain is calibrated through `cal1` with `calibrate_adc_gain("cal1", range="low")` (after the offset and
1055
+ the 5 V DMM line, see below); it serves every source in the low range and shows as
1056
+ `Calibration.adc_low_gain_ppm`.
1057
+
1024
1058
  ### Calibrating the ADC offset
1025
1059
 
1026
1060
  `"current_in"` reads the 4-20 mA terminal (J8) across a 249 Ω resistor, so 4 mA is 0.996 V and 20 mA is
@@ -1046,8 +1080,45 @@ and keeps what it had. `a` and `b` are the fit the pod now uses for `current_in`
1046
1080
  Every ADC source shares the same front end after its input switch, so from firmware 3.9.3 the pod
1047
1081
  takes the offset out of all of them: `adc_read` on `ext`, `cal1` and `cal2`, and `capture_adc`,
1048
1082
  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`).
1083
+ capabilities). Older firmware corrects `current_in` only. Needs firmware 3.4.0 (capability
1084
+ `calibrate`, `Capabilities.calibrate`). The web app has it in the pod's Settings tab, under
1085
+ Calibration.
1086
+
1087
+ #### Calibrating with a DMM
1088
+
1089
+ With a bench DMM the pod can also take out what the offset leaves (firmware 3.9.4+, capability
1090
+ `cal_dmm`, `Capabilities.cal_dmm`): its own DAC line per output, then the ADC gain per source.
1091
+ Every step is optional and stored on its own; an output or source without one keeps the built-in
1092
+ fit. Disconnect the DUT from the DAC outputs first: this drives up to ±5 V on them.
1093
+
1094
+ ```python
1095
+ points = []
1096
+ for volts in bp.DMM_SET_POINTS["5v"]: # 1.0 and 4.0 V; 3v3: 0.5 / 3.0, 12v: -5 / +5
1097
+ out = bp.dac_output("5v", volts=volts)
1098
+ points.append((out.code16, read_dmm())) # the DMM on the 5 V output SMA, DC volts
1099
+ cal = bp.calibrate_dac("5v", points) # this pod's line for the 5 V output
1100
+ bp.dac_output("off")
1101
+ print(cal.dac["5v"]) # DacLine(a, b, offset=-0.0031, gain_ppm=-996)
1102
+
1103
+ bp.calibrate_adc_gain("cal1") # 5 V loopback, against the line just stored
1104
+ bp.calibrate_adc_gain("ext") # needs the 12v line and an SMA cable ±12 V -> ADC in
1105
+ bp.clear_calibration("dac") # one part: "offset", "dac" or "adc"; no part = all
1106
+ ```
1107
+
1108
+ `calibrate_dac(path, points)` takes two `(code16, volts)` pairs: the 16-bit code `dac_output`
1109
+ drove and the DMM reading. From then on `dac_output`, the calibrated `generate` and the DAC output
1110
+ limits convert with this pod's line (`DacLine`: `a`, `b` per 16-bit code, `offset`, `gain_ppm`
1111
+ against the built-in line). `calibrate_adc_gain(source)` measures `cal1` through the 5 V output,
1112
+ `cal2` through the ±12 V output and `ext` (the front SMA, which `capture_adc` and the control loop
1113
+ use) through an SMA cable from the ±12 V output to the ADC input; `current_in` follows `cal1`. It
1114
+ needs the ADC offset and the driving output's DMM line, and `Calibration.measured` says what it
1115
+ saw. The pod refuses (`BenchPodError`) and keeps what it had when a reading cannot be right: two
1116
+ codes too close, a reading far from what the output should give (the DMM on another SMA), a gain
1117
+ more than 2 % off. It also refuses while DAC output limits are set. `Calibration.dac`,
1118
+ `Calibration.adc_gain_ppm` and `Calibration.dmm` show what is stored. The pod stores raw
1119
+ measurements (the J8-open zero as `Calibration.zero_count`, the DMM points) and derives every fit
1120
+ from them, so a later firmware with re-measured built-in fits keeps them right. Going back to
1121
+ firmware 3.4.0 to 3.9.3 keeps the offset; the DMM parts are not used there.
1051
1122
 
1052
1123
  ### The 4-20 mA output
1053
1124
 
@@ -1140,9 +1211,9 @@ both = bp.capture_correlated(adc_samples=4096, adc_sample_rate_hz=400_000,
1140
1211
  print(both.adc.duration, both.la.duration) # one hardware trigger: timebases align
1141
1212
  ```
1142
1213
 
1143
- * `capture_adc(samples=4096, *, sample_rate_hz=None, source=None, trigger=None, trigger_timeout=10.0)` — omitted rate = the device
1214
+ * `capture_adc(samples=4096, *, sample_rate_hz=None, source=None, trigger=None, trigger_timeout=10.0, range=None)` — omitted rate = the device
1144
1215
  maximum; the **achieved** rate is on the result. `source` routes the ADC first (omitted, the
1145
- current routing is left alone). `volts` use the front-SMA calibration; for the other sources
1216
+ current routing is left alone); `range` sets the [ADC input range](#the-adc-input-range-analog-module-v3r4). `volts` use the front-SMA calibration; for the other sources
1146
1217
  compare `counts` or use `adc_read`.
1147
1218
  * `capture_la(samples=4096, *, sample_rate_hz=None, stop_dac_after=None, trigger=None, trigger_timeout=10.0)` — 12-bit words; bit *n* is
1148
1219
  channel LA*n+1*. The **achieved** rate is on the result and can be below the request: the pod
@@ -1631,7 +1702,7 @@ lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is
1631
1702
  | 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)` |
1632
1703
  | I2C sensor | `enable_i2c_sensor(sensor=Sensor.BMP280, *, sda=None, scl=None, address=None, **values)`, `set_i2c_sensor(**values)`, `disable_i2c_sensor()`, `i2c_sensor_status()`, `i2c_sensor_types()`, `i2c_sensor_regs()`, `i2c_sensor_capture(...)` |
1633
1704
  | GPS receiver | `enable_gps(tx, *, baud=9600, rate_hz=1, sentences=None, utc=None, **fix)`, `set_gps(*, utc=None, **fix)`, `disable_gps()`, `gps_status()` |
1634
- | 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()` |
1705
+ | Analog | `analog_path(path, *, range=None)`, `adc_read(source="ext", *, range=None)`, `adc_range(range=None)`, `adc_range_test()`, `dac_output(path, *, volts=None)`, `current_out(current)`, `current_out_range()`, `calibrate(source="current_in")`, `calibration()`, `clear_calibration(part=None)`, `calibrate_dac(path, points)`, `calibrate_adc_gain(source, *, range="normal")`, `DMM_SET_POINTS` |
1635
1706
  | Captures | `capture_adc(...)`, `capture_la(...)`, `capture_correlated(...)`, `decode(source, protocol="i2c", ...)` |
1636
1707
  | DAC | `generate(...)`, `replay(...)`, `replay_waveform(...)`, `dac_stop()`, `control_loop(...)`, `loop_input(...)`, `loop_probe()` |
1637
1708
  | 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()` |
@@ -1672,12 +1743,16 @@ few built from other shapes); `to_dict()` goes the other way where a type is sen
1672
1743
  | `I2CByte` | `I2CMessage.data` | `value`, `ack` |
1673
1744
  | `UartFrame` | `decode("uart")` | `index`, `start_us`, `end_us`, `value`, `hex`, `text`, `ok`, `error` |
1674
1745
  | `SpiFrame` | `decode("spi")` | `index`, `start_us`, `end_us`, `mosi`, `miso`, `mosi_hex`, `miso_hex` |
1675
- | `AnalogPathState` | `analog_path` | `path`, `dac_mux_register`, `cal_relay_register` |
1676
- | `AdcReading` | `adc_read` | `source`, `voltage`, `count`, `span`, `offset`, `current` |
1746
+ | `AnalogPathState` | `analog_path` | `path`, `dac_mux_register`, `cal_relay_register`, `range` |
1747
+ | `AdcReading` | `adc_read` | `source`, `voltage`, `count`, `span`, `offset`, `current`, `range`, `fullscale`, `overrange` |
1748
+ | `AdcRangeState` | `adc_range` | `range`, `supported`, `fullscale`, `normal_fullscale`, `low_fullscale`, `a`, `b` (volts, the `ext` fit of the range) |
1749
+ | `AdcRangeTest` | `adc_range_test` | `passed`, `points` (each an `AdcRangeTestPoint`: `set_voltage`, `normal`, `low`, `ok`) |
1750
+ | `AdcRange` (literal) | `adc_range`, `range=` | `"normal"`, `"low"` |
1677
1751
  | `DacOutput` | `dac_output` | `path`, `voltage`, `code` |
1678
1752
  | `CurrentOutput` | `current_out`, `current_out_range` | `current`, `code`, `min_current`, `max_current` |
1679
- | `Calibration` | `calibrate`, `calibration`, `clear_calibration` | `source`, `calibrated`, `offset`, `a`, `b`, `count`, `span`, `samples` |
1680
- | `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()` |
1753
+ | `Calibration` | `calibrate`, `calibration`, `clear_calibration`, `calibrate_dac`, `calibrate_adc_gain` | `source`, `calibrated`, `offset`, `a`, `b`, `count`, `span`, `samples`, `dac`, `adc_gain_ppm`, `adc_low_gain_ppm`, `dmm`, `measured`, `zero_count` |
1754
+ | `DacLine` | `Calibration.dac` | `a`, `b`, `offset`, `gain_ppm` |
1755
+ | `Capture` | `capture_adc` | `counts`, `volts`, `currents`, `sample_rate_hz`, `source`, `trigger`, `range`, `overrange`, `clipped_samples`, `duration`, `times()`, `mean()`, `min()`, `max()`, `rms()`, `rms_ac()`, `peak_to_peak()`, `first_crossing()`, `crossing_times()`, `fft()`, `dominant_frequency()` |
1681
1756
  | `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()` |
1682
1757
  | `CorrelatedCapture` | `capture_correlated` | `adc` (a `Capture`), `la` (a `LaCapture`), sharing one hardware trigger |
1683
1758
  | `Trigger` | `capture_adc`, `capture_la`, `capture_correlated` | `la`, `edge` |
@@ -1718,9 +1793,9 @@ reported keeps its default (`False`, `0` or `""`), so a feature check is just
1718
1793
  | Group | Fields |
1719
1794
  |---|---|
1720
1795
  | Identity | `board`, `firmware_version`, `board_rev` (`"v2"`, `"v3"`, `"unknown"`, or `""`), `la_vccio_mv` (the LA bank voltage the pod reports) |
1721
- | ADC | `adc_bits`, `adc_fullscale_mv`, `adc_channels`, `adc_offset_counts`, `adc_affine` (the front-end fit), `adc_max_count`, `counts_to_volts(count)` |
1796
+ | ADC | `adc_bits`, `adc_fullscale_mv`, `adc_channels`, `adc_offset_counts`, `adc_affine` (the front-end fit, of the range in use), `adc_low_range` (analog module v3r4+: the low range), `adc_range` (`"normal"`, `"low"`, or `""` before firmware 3.9.5), `adc_range_fullscale` (volts, ±), `adc_max_count`, `counts_to_volts(count)` |
1722
1797
  | 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+), `generate16` (firmware 3.9.1+: `generate` in calibrated 16-bit volts), `dac_bits`, `dac_replay_bits`, `dac_replay_max_samples`, `dac_fullscale_mv`, `dac_channels`, `dac_limits` (output limits on the DAC paths) |
1723
- | 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`), `sensor_types` (the BME280/SHT4x/MPU-6050 models), `gps` (the GPS receiver, gateware v48+), `analog` (`False` on the digital-only board, `None` when not announced) |
1798
+ | 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`, `cal_dmm` (firmware 3.9.4+: `calibrate_dac`, `calibrate_adc_gain`), `current_out`, `can`, `pod_current` (`PowerStatus.pod`), `sensor_types` (the BME280/SHT4x/MPU-6050 models), `gps` (the GPS receiver, gateware v48+), `analog` (`False` on the digital-only board, `None` when not announced) |
1724
1799
  | 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) |
1725
1800
  | Boot health | `safe_mode`, `safe_reason`, `last_crash`, `reset_cause`, `boot_warning()` (a one-line warning after safe mode or a crash, else `None`) |
1726
1801
  | Source | `raw` (the map it was parsed from) |
@@ -1761,7 +1836,7 @@ timeout=60.0, lease_id=None)`. Errors raise `ServerApiError` (`status`), and an
1761
1836
  | `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 |
1762
1837
  | `encode_curve_b64url(codes)` | 16-bit DAC codes as little-endian base64url, the wire form of a curve |
1763
1838
  | `INTERNAL`, `EXTERNAL`, `PIN1` … `PIN14`, `BMP280_ADDR_PRIMARY`, `BMP280_ADDR_SECONDARY` | named constants (see [API conventions](#api-conventions-and-stability)) |
1764
- | `DacPath`, `DacOutputPath`, `AnalogPath`, `AdcSource`, `CalibrateSource` (`"current_in"`), `LoopSource`, `Waveshape`, `ReplayMapping`, `DecodeProtocol`, `CanMode`, `FaultType`, `GpioMode`, `Edge`, `TriggerEdge` | the `Literal` string types |
1839
+ | `DacPath`, `DacOutputPath`, `AnalogPath`, `AdcSource`, `CalibrateSource` (`"current_in"`), `DacCalPath` (`"3v3"`, `"5v"`, `"12v"`), `AdcGainSource` (`"cal1"`, `"cal2"`, `"ext"`), `CalibrationPart` (`"offset"`, `"dac"`, `"adc"`), `LoopSource`, `Waveshape`, `ReplayMapping`, `DecodeProtocol`, `CanMode`, `FaultType`, `GpioMode`, `Edge`, `TriggerEdge` | the `Literal` string types |
1765
1840
  | `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 |
1766
1841
 
1767
1842
  ## Examples
@@ -983,6 +983,40 @@ microvolt resolution (about 76 µV a step on `"5v"`) and `code16` is the 16-bit
983
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,
984
984
  calibrated reading; the pod refuses it while the input is still moving (e.g. a DAC left running).
985
985
 
986
+ ### The ADC input range (analog module v3r4)
987
+
988
+ Analog module v3r4 (firmware 3.9.5+, `Capabilities.adc_low_range`) adds a ÷2 low range to the
989
+ ADC input next to the normal ÷12 one:
990
+
991
+ | Range | Full scale | Resolution | Input impedance |
992
+ |---|---|---|---|
993
+ | `"normal"` (default) | ±32.8 V | ~1 mV per count | ~1 MΩ |
994
+ | `"low"` | ±5.46 V | ~167 µV per count | 1.82 MΩ: use 1x leads, not 10x probes |
995
+
996
+ ```python
997
+ st = bp.adc_range("low") # AdcRangeState(range, supported, fullscale, ...)
998
+ r = bp.adc_read("ext") # read in the low range: r.range == "low"
999
+ r = bp.adc_read("current_in", range="low") # or switch it with the reading
1000
+ cap = bp.capture_adc(4096, source="ext", range="low")
1001
+ print(cap.range, cap.overrange) # a clipped sample: capture again in the normal range
1002
+ bp.adc_range("normal")
1003
+ print(bp.adc_range_test().passed) # self-test through the cal1 loopback (5 V output)
1004
+ ```
1005
+
1006
+ The range applies to `ext`, `cal1` and `current_in` (4-20 mA fits inside it); `cal2` has none.
1007
+ It stays until it is changed, `analog_path("off")` or `analog_path("cal2")`, or the pod restarts.
1008
+ `analog_path`, `adc_read` and `capture_adc` take `range=` too. Captures are raw counts: the SDK
1009
+ keeps the fit it scales them with on the range in use (`Capabilities.adc_range`), and re-reads it
1010
+ whenever a reply says the pod moved to the other range. `AdcReading.overrange` and
1011
+ `Capture.overrange` (with `clipped_samples`) flag samples on the ADC's rails. A board without the
1012
+ low range raises `BenchPodError` for `"low"`.
1013
+
1014
+ `adc_range_test()` drives the 5 V output into `cal1` at 1 V and 3.5 V, reads each point in both
1015
+ ranges, and fails (`passed` is `False`) when they disagree by more than 5 % + 30 mV. The low range's
1016
+ gain is calibrated through `cal1` with `calibrate_adc_gain("cal1", range="low")` (after the offset and
1017
+ the 5 V DMM line, see below); it serves every source in the low range and shows as
1018
+ `Calibration.adc_low_gain_ppm`.
1019
+
986
1020
  ### Calibrating the ADC offset
987
1021
 
988
1022
  `"current_in"` reads the 4-20 mA terminal (J8) across a 249 Ω resistor, so 4 mA is 0.996 V and 20 mA is
@@ -1008,8 +1042,45 @@ and keeps what it had. `a` and `b` are the fit the pod now uses for `current_in`
1008
1042
  Every ADC source shares the same front end after its input switch, so from firmware 3.9.3 the pod
1009
1043
  takes the offset out of all of them: `adc_read` on `ext`, `cal1` and `cal2`, and `capture_adc`,
1010
1044
  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`).
1045
+ capabilities). Older firmware corrects `current_in` only. Needs firmware 3.4.0 (capability
1046
+ `calibrate`, `Capabilities.calibrate`). The web app has it in the pod's Settings tab, under
1047
+ Calibration.
1048
+
1049
+ #### Calibrating with a DMM
1050
+
1051
+ With a bench DMM the pod can also take out what the offset leaves (firmware 3.9.4+, capability
1052
+ `cal_dmm`, `Capabilities.cal_dmm`): its own DAC line per output, then the ADC gain per source.
1053
+ Every step is optional and stored on its own; an output or source without one keeps the built-in
1054
+ fit. Disconnect the DUT from the DAC outputs first: this drives up to ±5 V on them.
1055
+
1056
+ ```python
1057
+ points = []
1058
+ for volts in bp.DMM_SET_POINTS["5v"]: # 1.0 and 4.0 V; 3v3: 0.5 / 3.0, 12v: -5 / +5
1059
+ out = bp.dac_output("5v", volts=volts)
1060
+ points.append((out.code16, read_dmm())) # the DMM on the 5 V output SMA, DC volts
1061
+ cal = bp.calibrate_dac("5v", points) # this pod's line for the 5 V output
1062
+ bp.dac_output("off")
1063
+ print(cal.dac["5v"]) # DacLine(a, b, offset=-0.0031, gain_ppm=-996)
1064
+
1065
+ bp.calibrate_adc_gain("cal1") # 5 V loopback, against the line just stored
1066
+ bp.calibrate_adc_gain("ext") # needs the 12v line and an SMA cable ±12 V -> ADC in
1067
+ bp.clear_calibration("dac") # one part: "offset", "dac" or "adc"; no part = all
1068
+ ```
1069
+
1070
+ `calibrate_dac(path, points)` takes two `(code16, volts)` pairs: the 16-bit code `dac_output`
1071
+ drove and the DMM reading. From then on `dac_output`, the calibrated `generate` and the DAC output
1072
+ limits convert with this pod's line (`DacLine`: `a`, `b` per 16-bit code, `offset`, `gain_ppm`
1073
+ against the built-in line). `calibrate_adc_gain(source)` measures `cal1` through the 5 V output,
1074
+ `cal2` through the ±12 V output and `ext` (the front SMA, which `capture_adc` and the control loop
1075
+ use) through an SMA cable from the ±12 V output to the ADC input; `current_in` follows `cal1`. It
1076
+ needs the ADC offset and the driving output's DMM line, and `Calibration.measured` says what it
1077
+ saw. The pod refuses (`BenchPodError`) and keeps what it had when a reading cannot be right: two
1078
+ codes too close, a reading far from what the output should give (the DMM on another SMA), a gain
1079
+ more than 2 % off. It also refuses while DAC output limits are set. `Calibration.dac`,
1080
+ `Calibration.adc_gain_ppm` and `Calibration.dmm` show what is stored. The pod stores raw
1081
+ measurements (the J8-open zero as `Calibration.zero_count`, the DMM points) and derives every fit
1082
+ from them, so a later firmware with re-measured built-in fits keeps them right. Going back to
1083
+ firmware 3.4.0 to 3.9.3 keeps the offset; the DMM parts are not used there.
1013
1084
 
1014
1085
  ### The 4-20 mA output
1015
1086
 
@@ -1102,9 +1173,9 @@ both = bp.capture_correlated(adc_samples=4096, adc_sample_rate_hz=400_000,
1102
1173
  print(both.adc.duration, both.la.duration) # one hardware trigger: timebases align
1103
1174
  ```
1104
1175
 
1105
- * `capture_adc(samples=4096, *, sample_rate_hz=None, source=None, trigger=None, trigger_timeout=10.0)` — omitted rate = the device
1176
+ * `capture_adc(samples=4096, *, sample_rate_hz=None, source=None, trigger=None, trigger_timeout=10.0, range=None)` — omitted rate = the device
1106
1177
  maximum; the **achieved** rate is on the result. `source` routes the ADC first (omitted, the
1107
- current routing is left alone). `volts` use the front-SMA calibration; for the other sources
1178
+ current routing is left alone); `range` sets the [ADC input range](#the-adc-input-range-analog-module-v3r4). `volts` use the front-SMA calibration; for the other sources
1108
1179
  compare `counts` or use `adc_read`.
1109
1180
  * `capture_la(samples=4096, *, sample_rate_hz=None, stop_dac_after=None, trigger=None, trigger_timeout=10.0)` — 12-bit words; bit *n* is
1110
1181
  channel LA*n+1*. The **achieved** rate is on the result and can be below the request: the pod
@@ -1593,7 +1664,7 @@ lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is
1593
1664
  | 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)` |
1594
1665
  | I2C sensor | `enable_i2c_sensor(sensor=Sensor.BMP280, *, sda=None, scl=None, address=None, **values)`, `set_i2c_sensor(**values)`, `disable_i2c_sensor()`, `i2c_sensor_status()`, `i2c_sensor_types()`, `i2c_sensor_regs()`, `i2c_sensor_capture(...)` |
1595
1666
  | GPS receiver | `enable_gps(tx, *, baud=9600, rate_hz=1, sentences=None, utc=None, **fix)`, `set_gps(*, utc=None, **fix)`, `disable_gps()`, `gps_status()` |
1596
- | 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()` |
1667
+ | Analog | `analog_path(path, *, range=None)`, `adc_read(source="ext", *, range=None)`, `adc_range(range=None)`, `adc_range_test()`, `dac_output(path, *, volts=None)`, `current_out(current)`, `current_out_range()`, `calibrate(source="current_in")`, `calibration()`, `clear_calibration(part=None)`, `calibrate_dac(path, points)`, `calibrate_adc_gain(source, *, range="normal")`, `DMM_SET_POINTS` |
1597
1668
  | Captures | `capture_adc(...)`, `capture_la(...)`, `capture_correlated(...)`, `decode(source, protocol="i2c", ...)` |
1598
1669
  | DAC | `generate(...)`, `replay(...)`, `replay_waveform(...)`, `dac_stop()`, `control_loop(...)`, `loop_input(...)`, `loop_probe()` |
1599
1670
  | 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()` |
@@ -1634,12 +1705,16 @@ few built from other shapes); `to_dict()` goes the other way where a type is sen
1634
1705
  | `I2CByte` | `I2CMessage.data` | `value`, `ack` |
1635
1706
  | `UartFrame` | `decode("uart")` | `index`, `start_us`, `end_us`, `value`, `hex`, `text`, `ok`, `error` |
1636
1707
  | `SpiFrame` | `decode("spi")` | `index`, `start_us`, `end_us`, `mosi`, `miso`, `mosi_hex`, `miso_hex` |
1637
- | `AnalogPathState` | `analog_path` | `path`, `dac_mux_register`, `cal_relay_register` |
1638
- | `AdcReading` | `adc_read` | `source`, `voltage`, `count`, `span`, `offset`, `current` |
1708
+ | `AnalogPathState` | `analog_path` | `path`, `dac_mux_register`, `cal_relay_register`, `range` |
1709
+ | `AdcReading` | `adc_read` | `source`, `voltage`, `count`, `span`, `offset`, `current`, `range`, `fullscale`, `overrange` |
1710
+ | `AdcRangeState` | `adc_range` | `range`, `supported`, `fullscale`, `normal_fullscale`, `low_fullscale`, `a`, `b` (volts, the `ext` fit of the range) |
1711
+ | `AdcRangeTest` | `adc_range_test` | `passed`, `points` (each an `AdcRangeTestPoint`: `set_voltage`, `normal`, `low`, `ok`) |
1712
+ | `AdcRange` (literal) | `adc_range`, `range=` | `"normal"`, `"low"` |
1639
1713
  | `DacOutput` | `dac_output` | `path`, `voltage`, `code` |
1640
1714
  | `CurrentOutput` | `current_out`, `current_out_range` | `current`, `code`, `min_current`, `max_current` |
1641
- | `Calibration` | `calibrate`, `calibration`, `clear_calibration` | `source`, `calibrated`, `offset`, `a`, `b`, `count`, `span`, `samples` |
1642
- | `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()` |
1715
+ | `Calibration` | `calibrate`, `calibration`, `clear_calibration`, `calibrate_dac`, `calibrate_adc_gain` | `source`, `calibrated`, `offset`, `a`, `b`, `count`, `span`, `samples`, `dac`, `adc_gain_ppm`, `adc_low_gain_ppm`, `dmm`, `measured`, `zero_count` |
1716
+ | `DacLine` | `Calibration.dac` | `a`, `b`, `offset`, `gain_ppm` |
1717
+ | `Capture` | `capture_adc` | `counts`, `volts`, `currents`, `sample_rate_hz`, `source`, `trigger`, `range`, `overrange`, `clipped_samples`, `duration`, `times()`, `mean()`, `min()`, `max()`, `rms()`, `rms_ac()`, `peak_to_peak()`, `first_crossing()`, `crossing_times()`, `fft()`, `dominant_frequency()` |
1643
1718
  | `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()` |
1644
1719
  | `CorrelatedCapture` | `capture_correlated` | `adc` (a `Capture`), `la` (a `LaCapture`), sharing one hardware trigger |
1645
1720
  | `Trigger` | `capture_adc`, `capture_la`, `capture_correlated` | `la`, `edge` |
@@ -1680,9 +1755,9 @@ reported keeps its default (`False`, `0` or `""`), so a feature check is just
1680
1755
  | Group | Fields |
1681
1756
  |---|---|
1682
1757
  | Identity | `board`, `firmware_version`, `board_rev` (`"v2"`, `"v3"`, `"unknown"`, or `""`), `la_vccio_mv` (the LA bank voltage the pod reports) |
1683
- | ADC | `adc_bits`, `adc_fullscale_mv`, `adc_channels`, `adc_offset_counts`, `adc_affine` (the front-end fit), `adc_max_count`, `counts_to_volts(count)` |
1758
+ | ADC | `adc_bits`, `adc_fullscale_mv`, `adc_channels`, `adc_offset_counts`, `adc_affine` (the front-end fit, of the range in use), `adc_low_range` (analog module v3r4+: the low range), `adc_range` (`"normal"`, `"low"`, or `""` before firmware 3.9.5), `adc_range_fullscale` (volts, ±), `adc_max_count`, `counts_to_volts(count)` |
1684
1759
  | 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+), `generate16` (firmware 3.9.1+: `generate` in calibrated 16-bit volts), `dac_bits`, `dac_replay_bits`, `dac_replay_max_samples`, `dac_fullscale_mv`, `dac_channels`, `dac_limits` (output limits on the DAC paths) |
1685
- | 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`), `sensor_types` (the BME280/SHT4x/MPU-6050 models), `gps` (the GPS receiver, gateware v48+), `analog` (`False` on the digital-only board, `None` when not announced) |
1760
+ | 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`, `cal_dmm` (firmware 3.9.4+: `calibrate_dac`, `calibrate_adc_gain`), `current_out`, `can`, `pod_current` (`PowerStatus.pod`), `sensor_types` (the BME280/SHT4x/MPU-6050 models), `gps` (the GPS receiver, gateware v48+), `analog` (`False` on the digital-only board, `None` when not announced) |
1686
1761
  | 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) |
1687
1762
  | Boot health | `safe_mode`, `safe_reason`, `last_crash`, `reset_cause`, `boot_warning()` (a one-line warning after safe mode or a crash, else `None`) |
1688
1763
  | Source | `raw` (the map it was parsed from) |
@@ -1723,7 +1798,7 @@ timeout=60.0, lease_id=None)`. Errors raise `ServerApiError` (`status`), and an
1723
1798
  | `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 |
1724
1799
  | `encode_curve_b64url(codes)` | 16-bit DAC codes as little-endian base64url, the wire form of a curve |
1725
1800
  | `INTERNAL`, `EXTERNAL`, `PIN1` … `PIN14`, `BMP280_ADDR_PRIMARY`, `BMP280_ADDR_SECONDARY` | named constants (see [API conventions](#api-conventions-and-stability)) |
1726
- | `DacPath`, `DacOutputPath`, `AnalogPath`, `AdcSource`, `CalibrateSource` (`"current_in"`), `LoopSource`, `Waveshape`, `ReplayMapping`, `DecodeProtocol`, `CanMode`, `FaultType`, `GpioMode`, `Edge`, `TriggerEdge` | the `Literal` string types |
1801
+ | `DacPath`, `DacOutputPath`, `AnalogPath`, `AdcSource`, `CalibrateSource` (`"current_in"`), `DacCalPath` (`"3v3"`, `"5v"`, `"12v"`), `AdcGainSource` (`"cal1"`, `"cal2"`, `"ext"`), `CalibrationPart` (`"offset"`, `"dac"`, `"adc"`), `LoopSource`, `Waveshape`, `ReplayMapping`, `DecodeProtocol`, `CanMode`, `FaultType`, `GpioMode`, `Edge`, `TriggerEdge` | the `Literal` string types |
1727
1802
  | `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 |
1728
1803
 
1729
1804
  ## Examples
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "embeddedci"
7
- version = "2.13.2"
7
+ version = "2.13.4"
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"
@@ -48,8 +48,12 @@ from .constants import (
48
48
  PIN12,
49
49
  PIN13,
50
50
  PIN14,
51
+ AdcRange,
51
52
  AdcSource,
52
53
  CalibrateSource,
54
+ CalibrationPart,
55
+ DacCalPath,
56
+ AdcGainSource,
53
57
  AnalogPath,
54
58
  CanMode,
55
59
  DacOutputPath,
@@ -114,9 +118,13 @@ from .results import Capture, CorrelatedCapture, LaCapture, LanTls, Trigger
114
118
  from .transport.tls import delete_pin as forget_lan_tls, trust_lan_tls
115
119
  from .server_api import ServerApi, ServerApiError, ServerPermissionDeniedError
116
120
  from .state import (
121
+ AdcRangeState,
122
+ AdcRangeTest,
123
+ AdcRangeTestPoint,
117
124
  AdcReading,
118
125
  AnalogPathState,
119
126
  Calibration,
127
+ DacLine,
120
128
  CurrentOutput,
121
129
  DacOutput,
122
130
  EfuseState,
@@ -159,8 +167,12 @@ __all__ = [
159
167
  "PullState",
160
168
  "AnalogPathState",
161
169
  "DacOutput",
170
+ "AdcRangeState",
171
+ "AdcRangeTest",
172
+ "AdcRangeTestPoint",
162
173
  "AdcReading",
163
174
  "Calibration",
175
+ "DacLine",
164
176
  "CurrentOutput",
165
177
  "FpgaImageInfo",
166
178
  "LoopState",
@@ -252,8 +264,12 @@ __all__ = [
252
264
  "DacPath",
253
265
  "DacOutputPath",
254
266
  "AnalogPath",
267
+ "AdcRange",
255
268
  "AdcSource",
256
269
  "CalibrateSource",
270
+ "CalibrationPart",
271
+ "DacCalPath",
272
+ "AdcGainSource",
257
273
  "Waveshape",
258
274
  "ReplayMapping",
259
275
  "LoopSource",
@@ -115,6 +115,13 @@ class Capabilities:
115
115
  adc_offset_counts: float = 0.0
116
116
  #: affine front-end fit; None => use the naive linear model.
117
117
  adc_affine: Optional[AffineCal] = None
118
+ #: The board has the ADC low range (analog module v3r4+): :meth:`BenchPod.adc_range`.
119
+ adc_low_range: bool = False
120
+ #: The ADC input range :attr:`adc_affine` is for: ``"normal"`` or ``"low"``; ``""`` when the
121
+ #: firmware does not say (before 3.9.5: always normal).
122
+ adc_range: str = ""
123
+ #: That range's input full scale in volts (±), or 0.0 when not reported.
124
+ adc_range_fullscale: float = 0.0
118
125
 
119
126
  # DAC / replay
120
127
  dac: bool = False
@@ -176,6 +183,10 @@ class Capabilities:
176
183
  spi_stream: bool = False
177
184
  #: The pod can calibrate its own ADC and keeps the result: :meth:`BenchPod.calibrate`.
178
185
  calibrate: bool = False
186
+ #: ``calibrate`` also takes the DMM-guided forms (firmware 3.9.4+): this pod's own DAC line
187
+ #: per output and ADC gain per source, :meth:`BenchPod.calibrate_dac`,
188
+ #: :meth:`BenchPod.calibrate_adc_gain`.
189
+ cal_dmm: bool = False
179
190
  #: The pod can hold a current on its 4-20 mA output (J9): :meth:`BenchPod.current_out`.
180
191
  current_out: bool = False
181
192
  #: Classic CAN on the pod's CAN+/CAN- terminals: :meth:`BenchPod.open_can`. Firmware older
@@ -319,9 +330,11 @@ class Capabilities:
319
330
  ("spi_master", "spi_master"),
320
331
  ("spi_stream", "spi_stream"),
321
332
  ("calibrate", "calibrate"),
333
+ ("cal_dmm", "cal_dmm"),
322
334
  ("current_out", "current_out"),
323
335
  ("can", "can"),
324
336
  ("pod_current", "pod_current"),
337
+ ("adc_low_range", "adc_low_range"),
325
338
  ("dac_limits", "dac_limits"),
326
339
  ("sensor_types", "sensor_types"),
327
340
  ("gps", "gps"),
@@ -336,8 +349,19 @@ class Capabilities:
336
349
  c.last_crash = _crash_text(status.get("last_crash", status.get("crash")))
337
350
  c.reset_cause = str(status.get("reset", status.get("reset_cause", "")) or "")
338
351
  c._apply_affine(status)
352
+ c._apply_range(status)
339
353
  return c
340
354
 
355
+ def _apply_range(self, src: Mapping[str, Any], prefix: str = "") -> None:
356
+ """The ADC input range the fit is for (firmware 3.9.5+)."""
357
+ rng = src.get(prefix + "adc_range")
358
+ if isinstance(rng, str) and rng:
359
+ self.adc_range = rng
360
+ if (v := _as_float(src, prefix + "adc_range_fs_mv")) is not None and v > 0:
361
+ self.adc_range_fullscale = v / 1000.0
362
+ if _as_bool(src, prefix + "adc_low_range") is True:
363
+ self.adc_low_range = True
364
+
341
365
  @classmethod
342
366
  def from_parameters(cls, params: Mapping[str, Any]) -> "Capabilities":
343
367
  """Parse a server ``cap.*`` device-parameter map (complete capability set)."""
@@ -379,8 +403,9 @@ class Capabilities:
379
403
  ("capture_b64", "cap.capture_b64"),
380
404
  ("nrst_pin", "cap.nrst_pin"), ("usb_cc", "cap.usb_cc"),
381
405
  ("spi_master", "cap.spi_master"), ("spi_stream", "cap.spi_stream"),
382
- ("calibrate", "cap.calibrate"), ("current_out", "cap.current_out"),
406
+ ("calibrate", "cap.calibrate"), ("cal_dmm", "cap.cal_dmm"), ("current_out", "cap.current_out"),
383
407
  ("can", "cap.can"), ("pod_current", "cap.pod_current"),
408
+ ("adc_low_range", "cap.adc_low_range"),
384
409
  ("sensor_types", "cap.sensor_types"), ("gps", "cap.gps"),
385
410
  ("dac_limits", "cap.dac_limits"),
386
411
  ("ws_auth_v2", "cap.ws_auth_v2"),
@@ -408,6 +433,7 @@ class Capabilities:
408
433
  c.adc_affine = AffineCal(a=cal_a, b=cal_b, unwrap=bool(_as_bool(params, "cap.adc_cal_unwrap")))
409
434
  else:
410
435
  c._apply_affine(params, board=c.board)
436
+ c._apply_range(params, "cap.")
411
437
  return c
412
438
 
413
439
  def _apply_affine(self, src: Mapping[str, Any], board: Optional[str] = None) -> None:
@@ -147,7 +147,7 @@ def capture_adc(transport: Any, caps: Capabilities, *, samples: int = 4096,
147
147
  counts.extend(_dense(chunk))
148
148
  volts = [caps.counts_to_volts(c) for c in counts]
149
149
  return Capture(counts=counts, volts=volts, sample_rate_hz=_rate_hz(rate, sample_rate_hz),
150
- source=source)
150
+ source=source, range=caps.adc_range)
151
151
 
152
152
 
153
153
  def capture_la(transport: Any, *, samples: int = 4096, sample_rate_hz: Optional[float] = None,
@@ -279,7 +279,8 @@ def capture_correlated(transport: Any, caps: Capabilities, *, adc_samples: int =
279
279
  la_words = la_dense[:la_samples] if la_samples else la_dense
280
280
 
281
281
  adc = Capture(counts=adc_counts, volts=[caps.counts_to_volts(c) for c in adc_counts],
282
- sample_rate_hz=_rate_hz(adc_rate, adc_sample_rate_hz), trigger=trigger)
282
+ sample_rate_hz=_rate_hz(adc_rate, adc_sample_rate_hz), trigger=trigger,
283
+ range=caps.adc_range)
283
284
  la = LaCapture(words=la_words, sample_rate_hz=_rate_hz(la_rate, la_sample_rate_hz),
284
285
  trigger=trigger)
285
286
  return CorrelatedCapture(adc=adc, la=la)