embeddedci 2.13.2__tar.gz → 2.13.3__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.2 → embeddedci-2.13.3}/CHANGELOG.md +10 -0
  2. {embeddedci-2.13.2 → embeddedci-2.13.3}/PKG-INFO +45 -7
  3. {embeddedci-2.13.2 → embeddedci-2.13.3}/README.md +44 -6
  4. {embeddedci-2.13.2 → embeddedci-2.13.3}/pyproject.toml +1 -1
  5. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/__init__.py +8 -0
  6. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/capabilities.py +6 -1
  7. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/client.py +65 -3
  8. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/constants.py +10 -0
  9. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/state.py +51 -4
  10. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/transport/cloud.py +2 -1
  11. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/api_surface.json +25 -1
  12. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_client_v2.py +62 -0
  13. {embeddedci-2.13.2 → embeddedci-2.13.3}/.gitignore +0 -0
  14. {embeddedci-2.13.2 → embeddedci-2.13.3}/LICENSE +0 -0
  15. {embeddedci-2.13.2 → embeddedci-2.13.3}/docs/event-uart-design.md +0 -0
  16. {embeddedci-2.13.2 → embeddedci-2.13.3}/examples/README.md +0 -0
  17. {embeddedci-2.13.2 → embeddedci-2.13.3}/examples/test_bmp280.py +0 -0
  18. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/__init__.py +0 -0
  19. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/can.py +0 -0
  20. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/capture.py +0 -0
  21. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/ci.py +0 -0
  22. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/cli_config.py +0 -0
  23. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/cli_login.py +0 -0
  24. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/cloud_auth.py +0 -0
  25. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/connection.py +0 -0
  26. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/control_loop.py +0 -0
  27. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/decode.py +0 -0
  28. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/discovery.py +0 -0
  29. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/dsp.py +0 -0
  30. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/errors.py +0 -0
  31. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/flash.py +0 -0
  32. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/gpio.py +0 -0
  33. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/gps.py +0 -0
  34. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/i2c.py +0 -0
  35. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/lease.py +0 -0
  36. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/lowlevel.py +0 -0
  37. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/motor_emulator.py +0 -0
  38. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/power.py +0 -0
  39. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/protocol.py +0 -0
  40. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/pytest_plugin.py +0 -0
  41. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/replay.py +0 -0
  42. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/results.py +0 -0
  43. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/sensor.py +0 -0
  44. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/server_api.py +0 -0
  45. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/spi.py +0 -0
  46. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/transport/__init__.py +0 -0
  47. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/transport/base.py +0 -0
  48. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/transport/serial.py +0 -0
  49. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/transport/tcp.py +0 -0
  50. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/transport/tls.py +0 -0
  51. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/transport/usblink.py +0 -0
  52. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/uart.py +0 -0
  53. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/waveforms.py +0 -0
  54. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/benchpod/wiring.py +0 -0
  55. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/py.typed +0 -0
  56. {embeddedci-2.13.2 → embeddedci-2.13.3}/src/embeddedci/upload_build.py +0 -0
  57. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/conftest.py +0 -0
  58. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/README.md +0 -0
  59. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/conftest.py +0 -0
  60. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/e2e_helpers.py +0 -0
  61. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_cloud.py +0 -0
  62. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_digital.py +0 -0
  63. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_dut.py +0 -0
  64. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_gpio.py +0 -0
  65. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_la13_14.py +0 -0
  66. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_pod.py +0 -0
  67. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_rev3.py +0 -0
  68. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_spi.py +0 -0
  69. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/e2e/test_e2e_usb.py +0 -0
  70. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/examples/test_bmp280_hil.py +0 -0
  71. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/examples/test_can_ecu_demo.py +0 -0
  72. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/examples/test_can_loopback_hil.py +0 -0
  73. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/examples/test_smoke.py +0 -0
  74. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_adc_fft_hw.py +0 -0
  75. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_api_surface.py +0 -0
  76. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_can.py +0 -0
  77. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_capabilities.py +0 -0
  78. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_capabilities_scope.py +0 -0
  79. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_capture.py +0 -0
  80. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_capture_replay_hw.py +0 -0
  81. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_ci.py +0 -0
  82. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_cloud.py +0 -0
  83. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_connection.py +0 -0
  84. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_control_loop.py +0 -0
  85. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_control_loop_hw.py +0 -0
  86. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_current_out_waveforms.py +0 -0
  87. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_dac_limits_fixture.py +0 -0
  88. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_dac_path_default.py +0 -0
  89. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_dac_path_ranges.py +0 -0
  90. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_decode.py +0 -0
  91. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_decode_parity.py +0 -0
  92. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_docs_coverage.py +0 -0
  93. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_dsp.py +0 -0
  94. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_flash_bridge.py +0 -0
  95. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_flash_build.py +0 -0
  96. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_generate16.py +0 -0
  97. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_gpio.py +0 -0
  98. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_i2c_decode.py +0 -0
  99. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_i2c_parity.py +0 -0
  100. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_image_switching.py +0 -0
  101. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_integration.py +0 -0
  102. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_la_voltage_fixture.py +0 -0
  103. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_lan_tls.py +0 -0
  104. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_lease.py +0 -0
  105. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_motor_emulator.py +0 -0
  106. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_one_source.py +0 -0
  107. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_pins_bias.py +0 -0
  108. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_protocol.py +0 -0
  109. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_refusal_errors.py +0 -0
  110. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_replay.py +0 -0
  111. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_sensor.py +0 -0
  112. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_serial_autodetect.py +0 -0
  113. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_serial_text_console.py +0 -0
  114. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_spi.py +0 -0
  115. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_tcp_transport.py +0 -0
  116. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_timed_power.py +0 -0
  117. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_timing.py +0 -0
  118. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_transport_failures.py +0 -0
  119. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_trigger_power.py +0 -0
  120. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_uart_capture.py +0 -0
  121. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_uart_session.py +0 -0
  122. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_unsupported_feature.py +0 -0
  123. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_upload_build.py +0 -0
  124. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_usb_net.py +0 -0
  125. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_waveforms.py +0 -0
  126. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/test_wiring.py +0 -0
  127. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/testdata/i2c_decode_vectors.json +0 -0
  128. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/testdata/spi_decode_vectors.json +0 -0
  129. {embeddedci-2.13.2 → embeddedci-2.13.3}/tests/testdata/uart_decode_vectors.json +0 -0
@@ -1,5 +1,15 @@
1
1
  # Changelog — `embeddedci`
2
2
 
3
+ ## 2.13.3
4
+
5
+ - The DMM-guided calibration (pod firmware 3.9.4+, `Capabilities.cal_dmm`):
6
+ `BenchPod.calibrate_dac(path, points)` stores this pod's own line for a DAC output from two
7
+ `(code16, volts)` DMM readings, `calibrate_adc_gain(source)` measures an ADC gain (`cal1`,
8
+ `cal2`, `ext`) through a loopback, and `clear_calibration(part)` clears one part (`offset`,
9
+ `dac`, `adc`). `Calibration` gains `dac` (`DacLine`), `adc_gain_ppm`, `dmm` and `measured`;
10
+ `BenchPod.DMM_SET_POINTS` has the set points. New types `DacCalPath`, `AdcGainSource`,
11
+ `CalibrationPart`. `Calibration.zero_count` is the raw J8-open zero the pod derives its offset from.
12
+
3
13
  ## 2.13.2
4
14
 
5
15
  - 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.3
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
@@ -1046,8 +1046,45 @@ and keeps what it had. `a` and `b` are the fit the pod now uses for `current_in`
1046
1046
  Every ADC source shares the same front end after its input switch, so from firmware 3.9.3 the pod
1047
1047
  takes the offset out of all of them: `adc_read` on `ext`, `cal1` and `cal2`, and `capture_adc`,
1048
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`).
1049
+ capabilities). Older firmware corrects `current_in` only. Needs firmware 3.4.0 (capability
1050
+ `calibrate`, `Capabilities.calibrate`). The web app has it in the pod's Settings tab, under
1051
+ Calibration.
1052
+
1053
+ #### Calibrating with a DMM
1054
+
1055
+ With a bench DMM the pod can also take out what the offset leaves (firmware 3.9.4+, capability
1056
+ `cal_dmm`, `Capabilities.cal_dmm`): its own DAC line per output, then the ADC gain per source.
1057
+ Every step is optional and stored on its own; an output or source without one keeps the built-in
1058
+ fit. Disconnect the DUT from the DAC outputs first: this drives up to ±5 V on them.
1059
+
1060
+ ```python
1061
+ points = []
1062
+ for volts in bp.DMM_SET_POINTS["5v"]: # 1.0 and 4.0 V; 3v3: 0.5 / 3.0, 12v: -5 / +5
1063
+ out = bp.dac_output("5v", volts=volts)
1064
+ points.append((out.code16, read_dmm())) # the DMM on the 5 V output SMA, DC volts
1065
+ cal = bp.calibrate_dac("5v", points) # this pod's line for the 5 V output
1066
+ bp.dac_output("off")
1067
+ print(cal.dac["5v"]) # DacLine(a, b, offset=-0.0031, gain_ppm=-996)
1068
+
1069
+ bp.calibrate_adc_gain("cal1") # 5 V loopback, against the line just stored
1070
+ bp.calibrate_adc_gain("ext") # needs the 12v line and an SMA cable ±12 V -> ADC in
1071
+ bp.clear_calibration("dac") # one part: "offset", "dac" or "adc"; no part = all
1072
+ ```
1073
+
1074
+ `calibrate_dac(path, points)` takes two `(code16, volts)` pairs: the 16-bit code `dac_output`
1075
+ drove and the DMM reading. From then on `dac_output`, the calibrated `generate` and the DAC output
1076
+ limits convert with this pod's line (`DacLine`: `a`, `b` per 16-bit code, `offset`, `gain_ppm`
1077
+ against the built-in line). `calibrate_adc_gain(source)` measures `cal1` through the 5 V output,
1078
+ `cal2` through the ±12 V output and `ext` (the front SMA, which `capture_adc` and the control loop
1079
+ use) through an SMA cable from the ±12 V output to the ADC input; `current_in` follows `cal1`. It
1080
+ needs the ADC offset and the driving output's DMM line, and `Calibration.measured` says what it
1081
+ saw. The pod refuses (`BenchPodError`) and keeps what it had when a reading cannot be right: two
1082
+ codes too close, a reading far from what the output should give (the DMM on another SMA), a gain
1083
+ more than 2 % off. It also refuses while DAC output limits are set. `Calibration.dac`,
1084
+ `Calibration.adc_gain_ppm` and `Calibration.dmm` show what is stored. The pod stores raw
1085
+ measurements (the J8-open zero as `Calibration.zero_count`, the DMM points) and derives every fit
1086
+ from them, so a later firmware with re-measured built-in fits keeps them right. Going back to
1087
+ firmware 3.4.0 to 3.9.3 keeps the offset; the DMM parts are not used there.
1051
1088
 
1052
1089
  ### The 4-20 mA output
1053
1090
 
@@ -1631,7 +1668,7 @@ lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is
1631
1668
  | 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
1669
  | 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
1670
  | 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()` |
1671
+ | 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(part=None)`, `calibrate_dac(path, points)`, `calibrate_adc_gain(source)`, `DMM_SET_POINTS` |
1635
1672
  | Captures | `capture_adc(...)`, `capture_la(...)`, `capture_correlated(...)`, `decode(source, protocol="i2c", ...)` |
1636
1673
  | DAC | `generate(...)`, `replay(...)`, `replay_waveform(...)`, `dac_stop()`, `control_loop(...)`, `loop_input(...)`, `loop_probe()` |
1637
1674
  | 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()` |
@@ -1676,7 +1713,8 @@ few built from other shapes); `to_dict()` goes the other way where a type is sen
1676
1713
  | `AdcReading` | `adc_read` | `source`, `voltage`, `count`, `span`, `offset`, `current` |
1677
1714
  | `DacOutput` | `dac_output` | `path`, `voltage`, `code` |
1678
1715
  | `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` |
1716
+ | `Calibration` | `calibrate`, `calibration`, `clear_calibration`, `calibrate_dac`, `calibrate_adc_gain` | `source`, `calibrated`, `offset`, `a`, `b`, `count`, `span`, `samples`, `dac`, `adc_gain_ppm`, `dmm`, `measured`, `zero_count` |
1717
+ | `DacLine` | `Calibration.dac` | `a`, `b`, `offset`, `gain_ppm` |
1680
1718
  | `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()` |
1681
1719
  | `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
1720
  | `CorrelatedCapture` | `capture_correlated` | `adc` (a `Capture`), `la` (a `LaCapture`), sharing one hardware trigger |
@@ -1720,7 +1758,7 @@ reported keeps its default (`False`, `0` or `""`), so a feature check is just
1720
1758
  | Identity | `board`, `firmware_version`, `board_rev` (`"v2"`, `"v3"`, `"unknown"`, or `""`), `la_vccio_mv` (the LA bank voltage the pod reports) |
1721
1759
  | ADC | `adc_bits`, `adc_fullscale_mv`, `adc_channels`, `adc_offset_counts`, `adc_affine` (the front-end fit), `adc_max_count`, `counts_to_volts(count)` |
1722
1760
  | 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) |
1761
+ | 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
1762
  | 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
1763
  | 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
1764
  | Source | `raw` (the map it was parsed from) |
@@ -1761,7 +1799,7 @@ timeout=60.0, lease_id=None)`. Errors raise `ServerApiError` (`status`), and an
1761
1799
  | `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
1800
  | `encode_curve_b64url(codes)` | 16-bit DAC codes as little-endian base64url, the wire form of a curve |
1763
1801
  | `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 |
1802
+ | `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
1803
  | `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
1804
 
1767
1805
  ## Examples
@@ -1008,8 +1008,45 @@ and keeps what it had. `a` and `b` are the fit the pod now uses for `current_in`
1008
1008
  Every ADC source shares the same front end after its input switch, so from firmware 3.9.3 the pod
1009
1009
  takes the offset out of all of them: `adc_read` on `ext`, `cal1` and `cal2`, and `capture_adc`,
1010
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`).
1011
+ capabilities). Older firmware corrects `current_in` only. Needs firmware 3.4.0 (capability
1012
+ `calibrate`, `Capabilities.calibrate`). The web app has it in the pod's Settings tab, under
1013
+ Calibration.
1014
+
1015
+ #### Calibrating with a DMM
1016
+
1017
+ With a bench DMM the pod can also take out what the offset leaves (firmware 3.9.4+, capability
1018
+ `cal_dmm`, `Capabilities.cal_dmm`): its own DAC line per output, then the ADC gain per source.
1019
+ Every step is optional and stored on its own; an output or source without one keeps the built-in
1020
+ fit. Disconnect the DUT from the DAC outputs first: this drives up to ±5 V on them.
1021
+
1022
+ ```python
1023
+ points = []
1024
+ for volts in bp.DMM_SET_POINTS["5v"]: # 1.0 and 4.0 V; 3v3: 0.5 / 3.0, 12v: -5 / +5
1025
+ out = bp.dac_output("5v", volts=volts)
1026
+ points.append((out.code16, read_dmm())) # the DMM on the 5 V output SMA, DC volts
1027
+ cal = bp.calibrate_dac("5v", points) # this pod's line for the 5 V output
1028
+ bp.dac_output("off")
1029
+ print(cal.dac["5v"]) # DacLine(a, b, offset=-0.0031, gain_ppm=-996)
1030
+
1031
+ bp.calibrate_adc_gain("cal1") # 5 V loopback, against the line just stored
1032
+ bp.calibrate_adc_gain("ext") # needs the 12v line and an SMA cable ±12 V -> ADC in
1033
+ bp.clear_calibration("dac") # one part: "offset", "dac" or "adc"; no part = all
1034
+ ```
1035
+
1036
+ `calibrate_dac(path, points)` takes two `(code16, volts)` pairs: the 16-bit code `dac_output`
1037
+ drove and the DMM reading. From then on `dac_output`, the calibrated `generate` and the DAC output
1038
+ limits convert with this pod's line (`DacLine`: `a`, `b` per 16-bit code, `offset`, `gain_ppm`
1039
+ against the built-in line). `calibrate_adc_gain(source)` measures `cal1` through the 5 V output,
1040
+ `cal2` through the ±12 V output and `ext` (the front SMA, which `capture_adc` and the control loop
1041
+ use) through an SMA cable from the ±12 V output to the ADC input; `current_in` follows `cal1`. It
1042
+ needs the ADC offset and the driving output's DMM line, and `Calibration.measured` says what it
1043
+ saw. The pod refuses (`BenchPodError`) and keeps what it had when a reading cannot be right: two
1044
+ codes too close, a reading far from what the output should give (the DMM on another SMA), a gain
1045
+ more than 2 % off. It also refuses while DAC output limits are set. `Calibration.dac`,
1046
+ `Calibration.adc_gain_ppm` and `Calibration.dmm` show what is stored. The pod stores raw
1047
+ measurements (the J8-open zero as `Calibration.zero_count`, the DMM points) and derives every fit
1048
+ from them, so a later firmware with re-measured built-in fits keeps them right. Going back to
1049
+ firmware 3.4.0 to 3.9.3 keeps the offset; the DMM parts are not used there.
1013
1050
 
1014
1051
  ### The 4-20 mA output
1015
1052
 
@@ -1593,7 +1630,7 @@ lease_wait=600.0, lease_ttl=120, wiring=None)`. A context manager; `close()` is
1593
1630
  | 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
1631
  | 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
1632
  | 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()` |
1633
+ | 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(part=None)`, `calibrate_dac(path, points)`, `calibrate_adc_gain(source)`, `DMM_SET_POINTS` |
1597
1634
  | Captures | `capture_adc(...)`, `capture_la(...)`, `capture_correlated(...)`, `decode(source, protocol="i2c", ...)` |
1598
1635
  | DAC | `generate(...)`, `replay(...)`, `replay_waveform(...)`, `dac_stop()`, `control_loop(...)`, `loop_input(...)`, `loop_probe()` |
1599
1636
  | 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()` |
@@ -1638,7 +1675,8 @@ few built from other shapes); `to_dict()` goes the other way where a type is sen
1638
1675
  | `AdcReading` | `adc_read` | `source`, `voltage`, `count`, `span`, `offset`, `current` |
1639
1676
  | `DacOutput` | `dac_output` | `path`, `voltage`, `code` |
1640
1677
  | `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` |
1678
+ | `Calibration` | `calibrate`, `calibration`, `clear_calibration`, `calibrate_dac`, `calibrate_adc_gain` | `source`, `calibrated`, `offset`, `a`, `b`, `count`, `span`, `samples`, `dac`, `adc_gain_ppm`, `dmm`, `measured`, `zero_count` |
1679
+ | `DacLine` | `Calibration.dac` | `a`, `b`, `offset`, `gain_ppm` |
1642
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()` |
1643
1681
  | `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
1682
  | `CorrelatedCapture` | `capture_correlated` | `adc` (a `Capture`), `la` (a `LaCapture`), sharing one hardware trigger |
@@ -1682,7 +1720,7 @@ reported keeps its default (`False`, `0` or `""`), so a feature check is just
1682
1720
  | Identity | `board`, `firmware_version`, `board_rev` (`"v2"`, `"v3"`, `"unknown"`, or `""`), `la_vccio_mv` (the LA bank voltage the pod reports) |
1683
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)` |
1684
1722
  | 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) |
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`, `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
1724
  | 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
1725
  | 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
1726
  | Source | `raw` (the map it was parsed from) |
@@ -1723,7 +1761,7 @@ timeout=60.0, lease_id=None)`. Errors raise `ServerApiError` (`status`), and an
1723
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 |
1724
1762
  | `encode_curve_b64url(codes)` | 16-bit DAC codes as little-endian base64url, the wire form of a curve |
1725
1763
  | `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 |
1764
+ | `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
1765
  | `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
1766
 
1729
1767
  ## 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.3"
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"
@@ -50,6 +50,9 @@ from .constants import (
50
50
  PIN14,
51
51
  AdcSource,
52
52
  CalibrateSource,
53
+ CalibrationPart,
54
+ DacCalPath,
55
+ AdcGainSource,
53
56
  AnalogPath,
54
57
  CanMode,
55
58
  DacOutputPath,
@@ -117,6 +120,7 @@ from .state import (
117
120
  AdcReading,
118
121
  AnalogPathState,
119
122
  Calibration,
123
+ DacLine,
120
124
  CurrentOutput,
121
125
  DacOutput,
122
126
  EfuseState,
@@ -161,6 +165,7 @@ __all__ = [
161
165
  "DacOutput",
162
166
  "AdcReading",
163
167
  "Calibration",
168
+ "DacLine",
164
169
  "CurrentOutput",
165
170
  "FpgaImageInfo",
166
171
  "LoopState",
@@ -254,6 +259,9 @@ __all__ = [
254
259
  "AnalogPath",
255
260
  "AdcSource",
256
261
  "CalibrateSource",
262
+ "CalibrationPart",
263
+ "DacCalPath",
264
+ "AdcGainSource",
257
265
  "Waveshape",
258
266
  "ReplayMapping",
259
267
  "LoopSource",
@@ -176,6 +176,10 @@ class Capabilities:
176
176
  spi_stream: bool = False
177
177
  #: The pod can calibrate its own ADC and keeps the result: :meth:`BenchPod.calibrate`.
178
178
  calibrate: bool = False
179
+ #: ``calibrate`` also takes the DMM-guided forms (firmware 3.9.4+): this pod's own DAC line
180
+ #: per output and ADC gain per source, :meth:`BenchPod.calibrate_dac`,
181
+ #: :meth:`BenchPod.calibrate_adc_gain`.
182
+ cal_dmm: bool = False
179
183
  #: The pod can hold a current on its 4-20 mA output (J9): :meth:`BenchPod.current_out`.
180
184
  current_out: bool = False
181
185
  #: Classic CAN on the pod's CAN+/CAN- terminals: :meth:`BenchPod.open_can`. Firmware older
@@ -319,6 +323,7 @@ class Capabilities:
319
323
  ("spi_master", "spi_master"),
320
324
  ("spi_stream", "spi_stream"),
321
325
  ("calibrate", "calibrate"),
326
+ ("cal_dmm", "cal_dmm"),
322
327
  ("current_out", "current_out"),
323
328
  ("can", "can"),
324
329
  ("pod_current", "pod_current"),
@@ -379,7 +384,7 @@ class Capabilities:
379
384
  ("capture_b64", "cap.capture_b64"),
380
385
  ("nrst_pin", "cap.nrst_pin"), ("usb_cc", "cap.usb_cc"),
381
386
  ("spi_master", "cap.spi_master"), ("spi_stream", "cap.spi_stream"),
382
- ("calibrate", "cap.calibrate"), ("current_out", "cap.current_out"),
387
+ ("calibrate", "cap.calibrate"), ("cal_dmm", "cap.cal_dmm"), ("current_out", "cap.current_out"),
383
388
  ("can", "cap.can"), ("pod_current", "cap.pod_current"),
384
389
  ("sensor_types", "cap.sensor_types"), ("gps", "cap.gps"),
385
390
  ("dac_limits", "cap.dac_limits"),
@@ -48,9 +48,12 @@ from . import uart as _uart
48
48
  from .capabilities import ADC_WRAP_SPAN, ADC_WRAP_THRESHOLD, Capabilities
49
49
  from .constants import (
50
50
  ADC_SOURCE_PATHS,
51
+ ADC_GAIN_SOURCES,
51
52
  ADC_SOURCES,
52
53
  ANALOG_PATHS,
53
54
  CALIBRATE_SOURCES,
55
+ CALIBRATION_PARTS,
56
+ DAC_CAL_PATHS,
54
57
  CAN_MODES,
55
58
  DAC_OUTPUT_PATHS,
56
59
  DAC_PATHS,
@@ -65,9 +68,12 @@ from .constants import (
65
68
  PULLUP_CHANNELS,
66
69
  REPLAY_MAPPINGS,
67
70
  WAVESHAPES,
71
+ AdcGainSource,
68
72
  AdcSource,
69
73
  AnalogPath,
70
74
  CalibrateSource,
75
+ CalibrationPart,
76
+ DacCalPath,
71
77
  CanMode,
72
78
  DacOutputPath,
73
79
  DacPath,
@@ -1255,10 +1261,66 @@ class BenchPod:
1255
1261
  self._require_capability("calibrate", "calibration")
1256
1262
  return Calibration.from_reply(self.command({"cmd": "calibrate"}))
1257
1263
 
1258
- def clear_calibration(self) -> Calibration:
1259
- """Remove the pod's stored calibration. It goes back to the built-in fit."""
1264
+ def clear_calibration(self, part: Optional[CalibrationPart] = None) -> Calibration:
1265
+ """Remove the pod's stored calibration. It goes back to the built-in fits.
1266
+
1267
+ ``part`` removes one part only (firmware 3.9.4+): ``"offset"`` (the ADC offset),
1268
+ ``"dac"`` (the DMM lines of every DAC output) or ``"adc"`` (the ADC gains).
1269
+ """
1260
1270
  self._require_capability("calibrate", "calibration")
1261
- return Calibration.from_reply(self.command({"cmd": "calibrate", "clear": True}))
1271
+ if part is None:
1272
+ return Calibration.from_reply(self.command({"cmd": "calibrate", "clear": True}))
1273
+ check_choice(part, CALIBRATION_PARTS, "part")
1274
+ self._require_capability("cal_dmm", "partial calibration clears")
1275
+ return Calibration.from_reply(self.command({"cmd": "calibrate", "clear": part}))
1276
+
1277
+ #: The set points a DMM calibration drives on each output, in volts: well inside the range,
1278
+ #: away from the output floor near 0 V. The pod's ADC gain step uses the same ones.
1279
+ DMM_SET_POINTS: Dict[str, Tuple[float, float]] = {"3v3": (0.5, 3.0), "5v": (1.0, 4.0), "12v": (-5.0, 5.0)}
1280
+
1281
+ def calibrate_dac(self, path: DacCalPath,
1282
+ points: Sequence[Tuple[int, float]]) -> Calibration:
1283
+ """Store this pod's own line for a DAC output from two DMM readings (firmware 3.9.4+).
1284
+
1285
+ ``points`` is two ``(code16, volts)`` pairs: the 16-bit code the pod drove (the
1286
+ ``code16`` of :meth:`dac_output`) and what a DMM read on that output's SMA. Disconnect
1287
+ the DUT from the output first. A typical run::
1288
+
1289
+ for v in pod.DMM_SET_POINTS["5v"]:
1290
+ out = pod.dac_output("5v", volts=v)
1291
+ points.append((out.code16, dmm.read_volts()))
1292
+ pod.calibrate_dac("5v", points)
1293
+ pod.dac_output("off")
1294
+
1295
+ From then on :meth:`dac_output`, the calibrated :meth:`generate` and the DAC output
1296
+ limits convert with this line. Raises :class:`BenchPodError` when the pod refuses: the
1297
+ codes are too close, a reading is far from what the output should give (the DMM is on
1298
+ another SMA), or the gain is more than 2 % off. The stored line is then unchanged.
1299
+ """
1300
+ check_choice(path, DAC_CAL_PATHS, "path")
1301
+ pts = list(points)
1302
+ if len(pts) != 2:
1303
+ raise ValueError("calibrate_dac needs exactly two (code16, volts) points")
1304
+ (c1, v1), (c2, v2) = pts
1305
+ self._require_capability("cal_dmm", "DMM calibration")
1306
+ return Calibration.from_reply(self.command({
1307
+ "cmd": "calibrate", "dac": path, "code1": int(c1), "uv1": int(round(float(v1) * 1e6)),
1308
+ "code2": int(c2), "uv2": int(round(float(v2) * 1e6))}))
1309
+
1310
+ def calibrate_adc_gain(self, source: AdcGainSource) -> Calibration:
1311
+ """Measure and store an ADC source's gain through a loopback (firmware 3.9.4+).
1312
+
1313
+ The pod drives two set points on a DAC output that has its DMM line
1314
+ (:meth:`calibrate_dac`) and compares what the ADC reads with what the output puts out:
1315
+ ``cal1`` through the 5 V output, ``cal2`` through the ±12 V output, and ``ext`` (the front
1316
+ SMA, which captures and the control loop use) through an SMA cable from the ±12 V output
1317
+ to the ADC input. ``current_in`` follows ``cal1``. Needs the ADC offset first
1318
+ (:meth:`calibrate`). ``Calibration.measured`` says what was measured. Takes about 0.7 s
1319
+ and switches the outputs off afterwards.
1320
+ """
1321
+ check_choice(source, ADC_GAIN_SOURCES, "source")
1322
+ self._require_capability("cal_dmm", "DMM calibration")
1323
+ return Calibration.from_reply(self.command({"cmd": "calibrate", "adc": source}))
1262
1324
 
1263
1325
  # -- CAN (FDCAN1 / TCAN1044 transceiver) -----------------------------------
1264
1326
  # Single-node board: use a loopback ``mode`` to self-test on one pod. :meth:`open_can` and
@@ -112,6 +112,13 @@ AnalogPath = Literal["off", "dac_3v3", "dac_5v", "dac_12v", "adc_ext", "cal1", "
112
112
  AdcSource = Literal["ext", "cal1", "cal2", "current_in"]
113
113
  #: ADC sources a pod can calibrate on its own (:meth:`BenchPod.calibrate`): the 4-20 mA input.
114
114
  CalibrateSource = Literal["current_in"]
115
+ #: DAC outputs a DMM line can be stored for (:meth:`BenchPod.calibrate_dac`).
116
+ DacCalPath = Literal["3v3", "5v", "12v"]
117
+ #: ADC sources whose gain the pod measures through a loopback (:meth:`BenchPod.calibrate_adc_gain`);
118
+ #: ``current_in`` follows ``cal1``.
119
+ AdcGainSource = Literal["cal1", "cal2", "ext"]
120
+ #: One part of the pod's calibration (:meth:`BenchPod.clear_calibration`).
121
+ CalibrationPart = Literal["offset", "dac", "adc"]
115
122
  #: Parametric DAC waveforms the firmware generator produces.
116
123
  Waveshape = Literal["sine", "square", "sawtooth"]
117
124
  #: How replayed volts map onto DAC codes: reproduce them (``faithful``) or auto-scale (``fit``).
@@ -139,6 +146,9 @@ DAC_OUTPUT_PATHS: Tuple[str, ...] = get_args(DacOutputPath)
139
146
  ANALOG_PATHS: Tuple[str, ...] = get_args(AnalogPath)
140
147
  ADC_SOURCES: Tuple[str, ...] = get_args(AdcSource)
141
148
  CALIBRATE_SOURCES: Tuple[str, ...] = get_args(CalibrateSource)
149
+ DAC_CAL_PATHS: Tuple[str, ...] = get_args(DacCalPath)
150
+ ADC_GAIN_SOURCES: Tuple[str, ...] = get_args(AdcGainSource)
151
+ CALIBRATION_PARTS: Tuple[str, ...] = get_args(CalibrationPart)
142
152
  WAVESHAPES: Tuple[str, ...] = get_args(Waveshape)
143
153
  REPLAY_MAPPINGS: Tuple[str, ...] = get_args(ReplayMapping)
144
154
  LOOP_SOURCES: Tuple[str, ...] = get_args(LoopSource)
@@ -314,13 +314,35 @@ class AdcReading:
314
314
  current=None if ua is None else ua / 1e6, raw=d)
315
315
 
316
316
 
317
+ @dataclass(frozen=True)
318
+ class DacLine:
319
+ """This pod's own line for a DAC output, from two DMM readings (``BenchPod.calibrate_dac``)."""
320
+
321
+ #: ``volts = a + b * code16``: volts at code 0 and volts per 16-bit code.
322
+ a: float
323
+ b: float
324
+ #: Against the built-in line: the offset in volts (at 0 V on ``12v``, at code 0 on the
325
+ #: others) and the gain difference in ppm.
326
+ offset: float
327
+ gain_ppm: int
328
+
329
+ @classmethod
330
+ def from_reply(cls, d: Any) -> Optional["DacLine"]:
331
+ if not isinstance(d, dict):
332
+ return None
333
+ return cls(a=(_opt_int(d, "a_uv") or 0) / 1e6, b=(_opt_int(d, "b_pv") or 0) / 1e12,
334
+ offset=(_opt_int(d, "offset_uv") or 0) / 1e6, gain_ppm=_opt_int(d, "gain_ppm") or 0)
335
+
336
+
317
337
  @dataclass(frozen=True)
318
338
  class Calibration:
319
- """The pod's own ADC calibration for one source (``BenchPod.calibrate`` / ``calibration``).
339
+ """The pod's own calibration (``BenchPod.calibrate`` / ``calibration``).
320
340
 
321
341
  Every ADC source is scaled with a fit, ``volts = a + b * count``, that is built into the
322
- firmware and the same on every pod. A pod that calibrated itself keeps its own value on top
323
- of that, in flash. Today that is the offset of ``current_in``, the 4-20 mA input (J8).
342
+ firmware and the same on every pod. A pod that calibrated itself keeps its own values on top
343
+ of that, in flash: the ADC offset, measured on ``current_in`` (the 4-20 mA input, J8), and from
344
+ firmware 3.9.4 the DMM-guided parts, a DAC line per output (``dac``) and an ADC gain per
345
+ source (``adc_gain_ppm``).
324
346
  """
325
347
 
326
348
  source: str
@@ -338,16 +360,41 @@ class Calibration:
338
360
  count: Optional[int] = None
339
361
  span: Optional[int] = None
340
362
  samples: Optional[int] = None
363
+ #: This pod's DMM line per DAC output (``3v3``, ``5v``, ``12v``); ``None`` = the built-in
364
+ #: line. Empty on firmware before 3.9.3.
365
+ dac: Dict[str, Optional[DacLine]] = field(default_factory=dict)
366
+ #: The ADC gain correction per source (``cal1``, ``cal2``, ``ext``) in ppm; ``None`` = the
367
+ #: built-in gain. Empty on firmware before 3.9.3.
368
+ adc_gain_ppm: Dict[str, Optional[int]] = field(default_factory=dict)
369
+ #: ``True`` when any DMM part is stored.
370
+ dmm: bool = False
371
+ #: The raw zero the offset is derived from: the mean count ``current_in`` read with J8 open,
372
+ #: unwrapped. ``None`` when not calibrated or on firmware before 3.9.4.
373
+ zero_count: Optional[float] = None
374
+ #: After :meth:`BenchPod.calibrate_adc_gain`: what the DAC put out at the two set points
375
+ #: (``v1``, ``v2``), what the ADC read before the gain (``r1``, ``r2``) and the mean offset left
376
+ #: after it (``residual``), all in volts. ``None`` otherwise.
377
+ measured: Optional[Dict[str, float]] = None
341
378
  raw: Dict[str, Any] = _raw()
342
379
 
343
380
  @classmethod
344
381
  def from_reply(cls, reply: Any) -> "Calibration":
345
382
  d = _d(reply)
383
+ dac = d.get("dac") if isinstance(d.get("dac"), dict) else {}
384
+ gains = d.get("adc_gain_ppm") if isinstance(d.get("adc_gain_ppm"), dict) else {}
385
+ meas = d.get("measured") if isinstance(d.get("measured"), dict) else None
346
386
  return cls(source=str(d.get("source", "")), calibrated=d.get("calibrated") is True,
347
387
  offset=(_opt_int(d, "offset_uv") or 0) / 1e6,
348
388
  a=(_opt_int(d, "a_uv") or 0) / 1e6, b=(_opt_int(d, "b_nv") or 0) / 1e9,
349
389
  count=_opt_int(d, "count"), span=_opt_int(d, "span"),
350
- samples=_opt_int(d, "samples"), raw=d)
390
+ samples=_opt_int(d, "samples"),
391
+ dac={str(k): DacLine.from_reply(v) for k, v in dac.items()},
392
+ adc_gain_ppm={str(k): _opt_int(gains, k) for k in gains},
393
+ dmm=d.get("dmm") is True,
394
+ zero_count=None if _opt_int(d, "zero_count_mc") is None else _opt_int(d, "zero_count_mc") / 1000.0,
395
+ measured=None if meas is None else {
396
+ k[:-3]: (_opt_int(meas, k) or 0) / 1e6 for k in meas if k.endswith("_uv")},
397
+ raw=d)
351
398
 
352
399
 
353
400
  @dataclass(frozen=True)
@@ -157,7 +157,8 @@ def _read_form(req: dict) -> bool:
157
157
  if cmd == "dac_limits":
158
158
  return "path" not in req and req.get("enabled") is not False
159
159
  if cmd == "calibrate":
160
- return "source" not in req and req.get("clear") is not True
160
+ return (not any(k in req for k in ("source", "dac", "adc"))
161
+ and req.get("clear") in (None, False))
161
162
  if cmd in ("sig_policy", "lan_policy"):
162
163
  return "set" not in req
163
164
  if cmd in ("cloud_ca", "cloud_proxy"):
@@ -1,4 +1,5 @@
1
1
  {
2
+ "AdcGainSource": "Literal['cal1', 'cal2', 'ext']",
2
3
  "AdcReading": {
3
4
  "__fields__": [
4
5
  "source",
@@ -42,6 +43,8 @@
42
43
  "adc_read": "(self, source: 'AdcSource' = 'ext') -> 'AdcReading'",
43
44
  "analog_path": "(self, path: 'AnalogPath') -> 'AnalogPathState'",
44
45
  "calibrate": "(self, source: 'CalibrateSource' = 'current_in') -> 'Calibration'",
46
+ "calibrate_adc_gain": "(self, source: 'AdcGainSource') -> 'Calibration'",
47
+ "calibrate_dac": "(self, path: 'DacCalPath', points: 'Sequence[Tuple[int, float]]') -> 'Calibration'",
45
48
  "calibration": "(self) -> 'Calibration'",
46
49
  "can_analog": "property",
47
50
  "can_calibrate": "property",
@@ -64,7 +67,7 @@
64
67
  "capture_correlated": "(self, *, adc_samples: 'int' = 4096, adc_sample_rate_hz: 'Optional[float]' = None, la_samples: 'int' = 4096, la_sample_rate_hz: 'Optional[float]' = None, stop_dac_after: 'Optional[float]' = None, trigger: 'Optional[Trigger]' = None, trigger_timeout: 'float' = 10.0, source: 'Optional[AdcSource]' = None) -> 'CorrelatedCapture'",
65
68
  "capture_la": "(self, samples: 'int' = 4096, *, sample_rate_hz: 'Optional[float]' = None, stop_dac_after: 'Optional[float]' = None, trigger: 'Optional[Trigger]' = None, trigger_timeout: 'float' = 10.0) -> 'LaCapture'",
66
69
  "capture_uart": "(self, *, rx: 'Union[Pin, int, str, None]' = None, tx: 'Union[Pin, int, str, None]' = None, baud: 'Optional[int]' = None, duration: 'float', until: 'Optional[_uart.Until]' = None) -> '_uart.UartCapture'",
67
- "clear_calibration": "(self) -> 'Calibration'",
70
+ "clear_calibration": "(self, part: 'Optional[CalibrationPart]' = None) -> 'Calibration'",
68
71
  "close": "(self) -> 'None'",
69
72
  "command": "(self, req: 'Dict[str, Any]') -> 'Any'",
70
73
  "configure_gpio": "(self, las: 'Sequence[Union[Pin, int, str]]', mode: 'GpioMode' = 'output', *, level: 'Optional[int]' = None) -> 'List[LaPinState]'",
@@ -168,10 +171,16 @@
168
171
  "count",
169
172
  "span",
170
173
  "samples",
174
+ "dac",
175
+ "adc_gain_ppm",
176
+ "dmm",
177
+ "zero_count",
178
+ "measured",
171
179
  "raw"
172
180
  ],
173
181
  "from_reply": "(cls, reply: 'Any') -> \"'Calibration'\""
174
182
  },
183
+ "CalibrationPart": "Literal['offset', 'dac', 'adc']",
175
184
  "CanBus": {
176
185
  "__init__": "(self, bp: \"'BenchPod'\", *, bitrate: 'int', mode: 'str', term: 'bool' = False, fd: 'bool' = False) -> 'None'",
177
186
  "add_responder": "(self, match_id: 'int', reply_id: 'int', reply_data: 'Union[bytes, Sequence[int], None]' = None, *, match_ext: 'bool' = False, reply_ext: 'bool' = False) -> 'dict'",
@@ -249,6 +258,7 @@
249
258
  "spi_master",
250
259
  "spi_stream",
251
260
  "calibrate",
261
+ "cal_dmm",
252
262
  "current_out",
253
263
  "can",
254
264
  "pod_current",
@@ -342,10 +352,20 @@
342
352
  ],
343
353
  "from_reply": "(cls, reply: 'Any') -> \"'CurrentOutput'\""
344
354
  },
355
+ "DacCalPath": "Literal['3v3', '5v', '12v']",
345
356
  "DacHandle": {
346
357
  "__init__": "(self, *, stop: 'Callable[[], Any]', dac_path: 'str' = '', cotrig: 'bool' = False, data: 'Optional[dict]' = None) -> 'None'",
347
358
  "stop": "(self) -> 'None'"
348
359
  },
360
+ "DacLine": {
361
+ "__fields__": [
362
+ "a",
363
+ "b",
364
+ "offset",
365
+ "gain_ppm"
366
+ ],
367
+ "from_reply": "(cls, d: 'Any') -> \"Optional['DacLine']\""
368
+ },
349
369
  "DacOutput": {
350
370
  "__fields__": [
351
371
  "path",
@@ -974,6 +994,7 @@
974
994
  "with_changes": "(self, **changes: 'Any') -> \"'Wiring'\""
975
995
  },
976
996
  "__all__": [
997
+ "AdcGainSource",
977
998
  "AdcReading",
978
999
  "AdcSource",
979
1000
  "AnalogPath",
@@ -986,6 +1007,7 @@
986
1007
  "BuildReporter",
987
1008
  "CalibrateSource",
988
1009
  "Calibration",
1010
+ "CalibrationPart",
989
1011
  "CanBus",
990
1012
  "CanFrame",
991
1013
  "CanMode",
@@ -1000,7 +1022,9 @@
1000
1022
  "ControlLoopHandle",
1001
1023
  "CorrelatedCapture",
1002
1024
  "CurrentOutput",
1025
+ "DacCalPath",
1003
1026
  "DacHandle",
1027
+ "DacLine",
1004
1028
  "DacOutput",
1005
1029
  "DacOutputPath",
1006
1030
  "DacPath",
@@ -13,6 +13,7 @@ from embeddedci.benchpod.state import (
13
13
  AdcReading,
14
14
  AnalogPathState,
15
15
  Calibration,
16
+ DacLine,
16
17
  CurrentOutput,
17
18
  DacOutput,
18
19
  LaVoltage,
@@ -373,6 +374,67 @@ def test_calibrate_needs_the_capability():
373
374
  assert BenchPod(transport=CalPod(), lease=False).capabilities.calibrate
374
375
 
375
376
 
377
+ class DmmCalPod(FakeTransport):
378
+ """A pod that takes the DMM-guided calibration (firmware 3.9.4+)."""
379
+
380
+ def status(self) -> Any:
381
+ return {**super().status(), "version": "3.9.4", "caps": ["calibrate", "cal_dmm"]}
382
+
383
+
384
+ _DMM = {**_CAL, "dmm": True, "zero_count_mc": 65585123,
385
+ "dac": {"3v3": None, "5v": {"a_uv": -609, "b_pv": 76249841, "offset_uv": -3127, "gain_ppm": -996},
386
+ "12v": None},
387
+ "adc_gain_ppm": {"cal1": 1850, "cal2": None, "ext": None}}
388
+
389
+
390
+ def test_calibrate_dac_sends_both_points_in_microvolts():
391
+ t = DmmCalPod({"calibrate": lambda req: _DMM})
392
+ bp = BenchPod(transport=t, lease=False)
393
+ assert bp.capabilities.cal_dmm
394
+ c = bp.calibrate_dac("5v", [(13069, 0.9959), (52374, 3.9929)])
395
+ assert t.commands[-1] == {"cmd": "calibrate", "dac": "5v", "code1": 13069, "uv1": 995900,
396
+ "code2": 52374, "uv2": 3992900}
397
+ line = c.dac["5v"]
398
+ assert isinstance(line, DacLine) and c.dac["3v3"] is None and c.dmm
399
+ assert line.a == pytest.approx(-609e-6) and line.b == pytest.approx(76.249841e-6)
400
+ assert line.offset == pytest.approx(-3.127e-3) and line.gain_ppm == -996
401
+ assert c.adc_gain_ppm == {"cal1": 1850, "cal2": None, "ext": None}
402
+ assert c.zero_count == pytest.approx(65585.123)
403
+ with pytest.raises(ValueError):
404
+ bp.calibrate_dac("5v", [(13069, 0.9959)])
405
+ with pytest.raises(ValueError):
406
+ bp.calibrate_dac("off", [(1, 0.0), (2, 1.0)]) # type: ignore[arg-type]
407
+ assert set(bp.DMM_SET_POINTS) == {"3v3", "5v", "12v"}
408
+
409
+
410
+ def test_calibrate_adc_gain_and_partial_clear():
411
+ meas = {"v1_uv": 995900, "v2_uv": 3992900, "r1_uv": 994100, "r2_uv": 3985500, "residual_uv": 1200}
412
+ t = DmmCalPod({"calibrate": lambda req: {**_DMM, "measured": meas} if "adc" in req else _DMM})
413
+ bp = BenchPod(transport=t, lease=False)
414
+ c = bp.calibrate_adc_gain("cal1")
415
+ assert t.commands[-1] == {"cmd": "calibrate", "adc": "cal1"}
416
+ assert c.measured == pytest.approx({"v1": 0.9959, "v2": 3.9929, "r1": 0.9941, "r2": 3.9855, "residual": 0.0012})
417
+ with pytest.raises(ValueError):
418
+ bp.calibrate_adc_gain("current_in") # type: ignore[arg-type]
419
+ bp.clear_calibration("dac")
420
+ assert t.commands[-1] == {"cmd": "calibrate", "clear": "dac"}
421
+ with pytest.raises(ValueError):
422
+ bp.clear_calibration("everything") # type: ignore[arg-type]
423
+
424
+
425
+ def test_dmm_calibration_needs_cal_dmm():
426
+ t = CalPod({"calibrate": lambda req: _CAL}) # 3.4.0 to 3.9.2: offset only
427
+ bp = BenchPod(transport=t, lease=False)
428
+ for call in (lambda: bp.calibrate_dac("5v", [(1, 1.0), (40000, 3.0)]), lambda: bp.calibrate_adc_gain("cal1"),
429
+ lambda: bp.clear_calibration("adc")):
430
+ with pytest.raises(BenchPodError, match="cal_dmm"):
431
+ call()
432
+ assert t.commands == []
433
+ old = bp.calibration() # an old reply has no DMM parts
434
+ assert old.dac == {} and old.adc_gain_ppm == {} and not old.dmm and old.measured is None
435
+ assert old.zero_count is None
436
+
437
+
376
438
  def test_capture_adc_routes_the_source_first():
377
439
  bp, t = _bp({"analog_path": {"path": "cal1"}})
378
440
  cap = bp.capture_adc(2, source="cal1", sample_rate_hz=100_000)
File without changes
File without changes