embeddedci-openhtf 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- embeddedci_openhtf-0.1.0/.gitignore +13 -0
- embeddedci_openhtf-0.1.0/PKG-INFO +170 -0
- embeddedci_openhtf-0.1.0/README.md +144 -0
- embeddedci_openhtf-0.1.0/examples/analog_loopback.py +63 -0
- embeddedci_openhtf-0.1.0/examples/flash_and_boot.py +64 -0
- embeddedci_openhtf-0.1.0/examples/serial_smoke.py +70 -0
- embeddedci_openhtf-0.1.0/examples/station.py +68 -0
- embeddedci_openhtf-0.1.0/pyproject.toml +48 -0
- embeddedci_openhtf-0.1.0/src/embeddedci_openhtf/__init__.py +70 -0
- embeddedci_openhtf-0.1.0/src/embeddedci_openhtf/analog.py +184 -0
- embeddedci_openhtf-0.1.0/src/embeddedci_openhtf/measurements.py +109 -0
- embeddedci_openhtf-0.1.0/src/embeddedci_openhtf/phases.py +121 -0
- embeddedci_openhtf-0.1.0/src/embeddedci_openhtf/plug.py +218 -0
- embeddedci_openhtf-0.1.0/tests/_fake.py +88 -0
- embeddedci_openhtf-0.1.0/tests/conftest.py +9 -0
- embeddedci_openhtf-0.1.0/tests/test_analog.py +88 -0
- embeddedci_openhtf-0.1.0/tests/test_measurements.py +69 -0
- embeddedci_openhtf-0.1.0/tests/test_persistent.py +60 -0
- embeddedci_openhtf-0.1.0/tests/test_phases.py +72 -0
- embeddedci_openhtf-0.1.0/tests/test_plug.py +41 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: embeddedci-openhtf
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: OpenHTF plug for driving an EmbeddedCI BenchPod directly over TCP or serial (no cloud)
|
|
5
|
+
Project-URL: Homepage, https://embeddedci.com
|
|
6
|
+
Project-URL: Repository, https://github.com/embeddedci-com/embeddedci-python
|
|
7
|
+
Project-URL: Issues, https://github.com/embeddedci-com/embeddedci-python/issues
|
|
8
|
+
Author: EmbeddedCI
|
|
9
|
+
License: Apache-2.0
|
|
10
|
+
Keywords: benchpod,embedded,hardware-in-the-loop,manufacturing-test,openhtf,swd
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Software Development :: Testing
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: embeddedci>=0.2
|
|
22
|
+
Requires-Dist: openhtf>=1.6
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
# embeddedci-openhtf
|
|
28
|
+
|
|
29
|
+
> ⚠️ **Experimental.** This package is new and its API may change between
|
|
30
|
+
> releases. Pin a version and expect breaking changes before 1.0.
|
|
31
|
+
|
|
32
|
+
Drive an **EmbeddedCI BenchPod** from [OpenHTF](https://www.openhtf.com/) —
|
|
33
|
+
Google's open-source hardware test framework — connecting **directly** to the pod
|
|
34
|
+
over a TCP socket or serial port. No EmbeddedCI cloud account, OIDC, or web UI is
|
|
35
|
+
required: this package is for teams who want OpenHTF's test sequencing, limits,
|
|
36
|
+
records, and station GUI while talking straight to a pod on their own bench.
|
|
37
|
+
|
|
38
|
+
It's a thin wrapper over the [`embeddedci`](../embeddedci) BenchPod SDK: a single
|
|
39
|
+
plug plus a few phase helpers. The dependency direction is strictly
|
|
40
|
+
**`embeddedci-openhtf` → `embeddedci`**.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pip install embeddedci-openhtf # pulls in embeddedci + openhtf
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## The plug
|
|
47
|
+
|
|
48
|
+
`BenchPodPlug` opens a `BenchPod` when a test starts and closes it at teardown.
|
|
49
|
+
Bind the connection inline with `benchpod_plug(...)`, or leave it unbound and
|
|
50
|
+
supply it through OpenHTF config / the `BENCHPOD_CONNECTION` env var.
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import openhtf as htf
|
|
54
|
+
from embeddedci_openhtf import benchpod_plug
|
|
55
|
+
|
|
56
|
+
bench = benchpod_plug("192.168.1.50:8080") # TCP — or benchpod_plug("/dev/ttyACM0")
|
|
57
|
+
|
|
58
|
+
@htf.plug(bench=bench)
|
|
59
|
+
def power_up(test, bench):
|
|
60
|
+
bench.power_on() # methods proxy to the BenchPod SDK client
|
|
61
|
+
bench.pod.capture_uart(...) # or reach the full client via .pod
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Connection forms (all **direct**, never cloud):
|
|
65
|
+
|
|
66
|
+
| Form | Example |
|
|
67
|
+
| --- | --- |
|
|
68
|
+
| TCP `host[:port]` | `benchpod_plug("192.168.1.50:8080")` |
|
|
69
|
+
| Serial device path | `benchpod_plug("/dev/ttyACM0")` / `benchpod_plug("COM5")` |
|
|
70
|
+
| `BENCHPOD_CONNECTION` env | `benchpod_plug()` (unbound) |
|
|
71
|
+
| OpenHTF config | `htf.conf.load(benchpod_connection="...")` then `@htf.plug(bench=BenchPodPlug)` |
|
|
72
|
+
|
|
73
|
+
## Phase helpers
|
|
74
|
+
|
|
75
|
+
Ready-made, fully-decorated phases for the common steps:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
from embeddedci_openhtf import benchpod_plug, flash_phase, boot_banner_phase
|
|
79
|
+
|
|
80
|
+
bench = benchpod_plug("192.168.1.50:8080")
|
|
81
|
+
|
|
82
|
+
test = htf.Test(
|
|
83
|
+
flash_phase(bench, file="fw.elf", target="target/stm32f4x.cfg",
|
|
84
|
+
swclk=11, swdio=12, nreset=3), # records flash_ok, attaches openocd.log
|
|
85
|
+
boot_banner_phase(bench, rx=1, tx=2, expect="APP_OK"), # records boot_ok, attaches uart.txt
|
|
86
|
+
)
|
|
87
|
+
test.execute(test_start=lambda: "SN-0001")
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
LA channels are 1-12 (the pod has 12 generic logic-analyzer channels and no
|
|
91
|
+
fixed-role pins — wire any DUT signal to any channel and name it here).
|
|
92
|
+
|
|
93
|
+
For anything custom, write a normal phase and use the recorders in
|
|
94
|
+
`embeddedci_openhtf.measurements` (`record_flash`, `record_uart`,
|
|
95
|
+
`record_samples`) to map SDK results onto measurements and attachments.
|
|
96
|
+
|
|
97
|
+
`flash_phase` needs `openocd` on PATH (the pod is the CMSIS-DAP probe; OpenOCD
|
|
98
|
+
runs the flash algorithm from the `target=` config, so every OpenOCD-supported
|
|
99
|
+
MCU works unchanged).
|
|
100
|
+
|
|
101
|
+
### Analog steps
|
|
102
|
+
|
|
103
|
+
The pod's DAC output and ADC input are exposed as analog phases (and low-level
|
|
104
|
+
helpers). **These are TCP-only** — they need the JSON/sample channel that the
|
|
105
|
+
serial console doesn't provide.
|
|
106
|
+
|
|
107
|
+
```python
|
|
108
|
+
from embeddedci_openhtf import (
|
|
109
|
+
benchpod_plug, signal_generate_phase, adc_capture_phase, loopback_measure_phase,
|
|
110
|
+
)
|
|
111
|
+
bench = benchpod_plug("192.168.1.50:8080")
|
|
112
|
+
|
|
113
|
+
test = htf.Test(
|
|
114
|
+
# drive the DAC and capture the ADC together (loopback), assert the round trip
|
|
115
|
+
loopback_measure_phase(bench, waveform="sine", freq=10_000, amplitude=120,
|
|
116
|
+
pp_range=(180, 255), mean_range=(110, 150)),
|
|
117
|
+
# or: free-run a waveform, then snapshot the ADC separately
|
|
118
|
+
signal_generate_phase(bench, waveform="square", freq=1_000, amplitude=100),
|
|
119
|
+
adc_capture_phase(bench, samples=4096, pp_range=(150, 255)),
|
|
120
|
+
)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Each capture/measure phase records `<prefix>_min` / `_max` / `_mean` / `_pp`
|
|
124
|
+
(peak-to-peak) measurements — pass any as `(low, high)` to make it a limit — and
|
|
125
|
+
attaches the raw samples as `adc.json`. The low-level helpers `signal_generate`,
|
|
126
|
+
`signal_stop`, and `measure` are available for custom phases.
|
|
127
|
+
|
|
128
|
+
## Station mode (persistent connection)
|
|
129
|
+
|
|
130
|
+
By default the plug opens a connection per `Test.execute()` and closes it at
|
|
131
|
+
teardown. On a station cycling many DUTs back-to-back, pass `persistent=True` to
|
|
132
|
+
keep **one** connection open across executions (re-checked with a ping each run,
|
|
133
|
+
reconnected if it dropped). Reuse the *same* plug class for every execution, and
|
|
134
|
+
close it once at the end:
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from embeddedci_openhtf import benchpod_plug, close_persistent_benchpods
|
|
138
|
+
from openhtf.plugs import user_input
|
|
139
|
+
|
|
140
|
+
bench = benchpod_plug("192.168.1.50:8080", persistent=True)
|
|
141
|
+
test = htf.Test(power_phase(bench, on=True), boot_banner_phase(bench, rx=1, tx=2, expect="APP_OK"))
|
|
142
|
+
try:
|
|
143
|
+
while test.execute(test_start=user_input.prompt_for_test_start()):
|
|
144
|
+
pass # next DUT — same pod connection
|
|
145
|
+
finally:
|
|
146
|
+
close_persistent_benchpods() # also runs automatically at process exit
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Examples
|
|
150
|
+
|
|
151
|
+
* [`examples/flash_and_boot.py`](examples/flash_and_boot.py) — flash over SWD then
|
|
152
|
+
assert the boot banner, over a direct TCP connection.
|
|
153
|
+
* [`examples/serial_smoke.py`](examples/serial_smoke.py) — a no-flash power + UART
|
|
154
|
+
smoke test with a parsed measurement, over a direct serial connection.
|
|
155
|
+
* [`examples/analog_loopback.py`](examples/analog_loopback.py) — DAC→ADC loopback
|
|
156
|
+
signal-path self-test, over a direct TCP connection.
|
|
157
|
+
* [`examples/station.py`](examples/station.py) — a station loop testing many DUTs
|
|
158
|
+
over one **persistent** connection.
|
|
159
|
+
|
|
160
|
+
## Development
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
# from the repo root
|
|
164
|
+
pip install -e "packages/embeddedci[dev]"
|
|
165
|
+
pip install -e "packages/embeddedci-openhtf[dev]"
|
|
166
|
+
pytest packages/embeddedci-openhtf
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The test suite runs the real OpenHTF executor against an in-memory fake transport
|
|
170
|
+
(`tests/_fake.py`), so it needs no pod and no OpenOCD.
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# embeddedci-openhtf
|
|
2
|
+
|
|
3
|
+
> ⚠️ **Experimental.** This package is new and its API may change between
|
|
4
|
+
> releases. Pin a version and expect breaking changes before 1.0.
|
|
5
|
+
|
|
6
|
+
Drive an **EmbeddedCI BenchPod** from [OpenHTF](https://www.openhtf.com/) —
|
|
7
|
+
Google's open-source hardware test framework — connecting **directly** to the pod
|
|
8
|
+
over a TCP socket or serial port. No EmbeddedCI cloud account, OIDC, or web UI is
|
|
9
|
+
required: this package is for teams who want OpenHTF's test sequencing, limits,
|
|
10
|
+
records, and station GUI while talking straight to a pod on their own bench.
|
|
11
|
+
|
|
12
|
+
It's a thin wrapper over the [`embeddedci`](../embeddedci) BenchPod SDK: a single
|
|
13
|
+
plug plus a few phase helpers. The dependency direction is strictly
|
|
14
|
+
**`embeddedci-openhtf` → `embeddedci`**.
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install embeddedci-openhtf # pulls in embeddedci + openhtf
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## The plug
|
|
21
|
+
|
|
22
|
+
`BenchPodPlug` opens a `BenchPod` when a test starts and closes it at teardown.
|
|
23
|
+
Bind the connection inline with `benchpod_plug(...)`, or leave it unbound and
|
|
24
|
+
supply it through OpenHTF config / the `BENCHPOD_CONNECTION` env var.
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
import openhtf as htf
|
|
28
|
+
from embeddedci_openhtf import benchpod_plug
|
|
29
|
+
|
|
30
|
+
bench = benchpod_plug("192.168.1.50:8080") # TCP — or benchpod_plug("/dev/ttyACM0")
|
|
31
|
+
|
|
32
|
+
@htf.plug(bench=bench)
|
|
33
|
+
def power_up(test, bench):
|
|
34
|
+
bench.power_on() # methods proxy to the BenchPod SDK client
|
|
35
|
+
bench.pod.capture_uart(...) # or reach the full client via .pod
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Connection forms (all **direct**, never cloud):
|
|
39
|
+
|
|
40
|
+
| Form | Example |
|
|
41
|
+
| --- | --- |
|
|
42
|
+
| TCP `host[:port]` | `benchpod_plug("192.168.1.50:8080")` |
|
|
43
|
+
| Serial device path | `benchpod_plug("/dev/ttyACM0")` / `benchpod_plug("COM5")` |
|
|
44
|
+
| `BENCHPOD_CONNECTION` env | `benchpod_plug()` (unbound) |
|
|
45
|
+
| OpenHTF config | `htf.conf.load(benchpod_connection="...")` then `@htf.plug(bench=BenchPodPlug)` |
|
|
46
|
+
|
|
47
|
+
## Phase helpers
|
|
48
|
+
|
|
49
|
+
Ready-made, fully-decorated phases for the common steps:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from embeddedci_openhtf import benchpod_plug, flash_phase, boot_banner_phase
|
|
53
|
+
|
|
54
|
+
bench = benchpod_plug("192.168.1.50:8080")
|
|
55
|
+
|
|
56
|
+
test = htf.Test(
|
|
57
|
+
flash_phase(bench, file="fw.elf", target="target/stm32f4x.cfg",
|
|
58
|
+
swclk=11, swdio=12, nreset=3), # records flash_ok, attaches openocd.log
|
|
59
|
+
boot_banner_phase(bench, rx=1, tx=2, expect="APP_OK"), # records boot_ok, attaches uart.txt
|
|
60
|
+
)
|
|
61
|
+
test.execute(test_start=lambda: "SN-0001")
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
LA channels are 1-12 (the pod has 12 generic logic-analyzer channels and no
|
|
65
|
+
fixed-role pins — wire any DUT signal to any channel and name it here).
|
|
66
|
+
|
|
67
|
+
For anything custom, write a normal phase and use the recorders in
|
|
68
|
+
`embeddedci_openhtf.measurements` (`record_flash`, `record_uart`,
|
|
69
|
+
`record_samples`) to map SDK results onto measurements and attachments.
|
|
70
|
+
|
|
71
|
+
`flash_phase` needs `openocd` on PATH (the pod is the CMSIS-DAP probe; OpenOCD
|
|
72
|
+
runs the flash algorithm from the `target=` config, so every OpenOCD-supported
|
|
73
|
+
MCU works unchanged).
|
|
74
|
+
|
|
75
|
+
### Analog steps
|
|
76
|
+
|
|
77
|
+
The pod's DAC output and ADC input are exposed as analog phases (and low-level
|
|
78
|
+
helpers). **These are TCP-only** — they need the JSON/sample channel that the
|
|
79
|
+
serial console doesn't provide.
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from embeddedci_openhtf import (
|
|
83
|
+
benchpod_plug, signal_generate_phase, adc_capture_phase, loopback_measure_phase,
|
|
84
|
+
)
|
|
85
|
+
bench = benchpod_plug("192.168.1.50:8080")
|
|
86
|
+
|
|
87
|
+
test = htf.Test(
|
|
88
|
+
# drive the DAC and capture the ADC together (loopback), assert the round trip
|
|
89
|
+
loopback_measure_phase(bench, waveform="sine", freq=10_000, amplitude=120,
|
|
90
|
+
pp_range=(180, 255), mean_range=(110, 150)),
|
|
91
|
+
# or: free-run a waveform, then snapshot the ADC separately
|
|
92
|
+
signal_generate_phase(bench, waveform="square", freq=1_000, amplitude=100),
|
|
93
|
+
adc_capture_phase(bench, samples=4096, pp_range=(150, 255)),
|
|
94
|
+
)
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
Each capture/measure phase records `<prefix>_min` / `_max` / `_mean` / `_pp`
|
|
98
|
+
(peak-to-peak) measurements — pass any as `(low, high)` to make it a limit — and
|
|
99
|
+
attaches the raw samples as `adc.json`. The low-level helpers `signal_generate`,
|
|
100
|
+
`signal_stop`, and `measure` are available for custom phases.
|
|
101
|
+
|
|
102
|
+
## Station mode (persistent connection)
|
|
103
|
+
|
|
104
|
+
By default the plug opens a connection per `Test.execute()` and closes it at
|
|
105
|
+
teardown. On a station cycling many DUTs back-to-back, pass `persistent=True` to
|
|
106
|
+
keep **one** connection open across executions (re-checked with a ping each run,
|
|
107
|
+
reconnected if it dropped). Reuse the *same* plug class for every execution, and
|
|
108
|
+
close it once at the end:
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
from embeddedci_openhtf import benchpod_plug, close_persistent_benchpods
|
|
112
|
+
from openhtf.plugs import user_input
|
|
113
|
+
|
|
114
|
+
bench = benchpod_plug("192.168.1.50:8080", persistent=True)
|
|
115
|
+
test = htf.Test(power_phase(bench, on=True), boot_banner_phase(bench, rx=1, tx=2, expect="APP_OK"))
|
|
116
|
+
try:
|
|
117
|
+
while test.execute(test_start=user_input.prompt_for_test_start()):
|
|
118
|
+
pass # next DUT — same pod connection
|
|
119
|
+
finally:
|
|
120
|
+
close_persistent_benchpods() # also runs automatically at process exit
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Examples
|
|
124
|
+
|
|
125
|
+
* [`examples/flash_and_boot.py`](examples/flash_and_boot.py) — flash over SWD then
|
|
126
|
+
assert the boot banner, over a direct TCP connection.
|
|
127
|
+
* [`examples/serial_smoke.py`](examples/serial_smoke.py) — a no-flash power + UART
|
|
128
|
+
smoke test with a parsed measurement, over a direct serial connection.
|
|
129
|
+
* [`examples/analog_loopback.py`](examples/analog_loopback.py) — DAC→ADC loopback
|
|
130
|
+
signal-path self-test, over a direct TCP connection.
|
|
131
|
+
* [`examples/station.py`](examples/station.py) — a station loop testing many DUTs
|
|
132
|
+
over one **persistent** connection.
|
|
133
|
+
|
|
134
|
+
## Development
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
# from the repo root
|
|
138
|
+
pip install -e "packages/embeddedci[dev]"
|
|
139
|
+
pip install -e "packages/embeddedci-openhtf[dev]"
|
|
140
|
+
pytest packages/embeddedci-openhtf
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
The test suite runs the real OpenHTF executor against an in-memory fake transport
|
|
144
|
+
(`tests/_fake.py`), so it needs no pod and no OpenOCD.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""OpenHTF test: analog stimulus + acquisition over a **direct TCP** connection
|
|
3
|
+
to the BenchPod (no EmbeddedCI cloud).
|
|
4
|
+
|
|
5
|
+
The pod has a DAC output and an ADC input on its FPGA. This test drives a known
|
|
6
|
+
waveform and captures the ADC at the same time (``measure`` = synchronized
|
|
7
|
+
DAC+ADC loopback), then asserts the round-trip amplitude — the canonical analog
|
|
8
|
+
signal-path self-test. Wire the pod's DAC output to its ADC input (directly, or
|
|
9
|
+
through a DUT filter/amplifier whose response you want to check).
|
|
10
|
+
|
|
11
|
+
pip install embeddedci-openhtf
|
|
12
|
+
python analog_loopback.py --pod 192.168.1.50:8080
|
|
13
|
+
|
|
14
|
+
Analog commands are TCP-only (they need the JSON/sample channel), so this example
|
|
15
|
+
uses a TCP address, not a serial path.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
import openhtf as htf
|
|
22
|
+
from openhtf.output.callbacks import console_summary
|
|
23
|
+
|
|
24
|
+
from embeddedci_openhtf import (
|
|
25
|
+
adc_capture_phase,
|
|
26
|
+
benchpod_plug,
|
|
27
|
+
loopback_measure_phase,
|
|
28
|
+
signal_generate_phase,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def build_test(pod: str) -> htf.Test:
|
|
33
|
+
bench = benchpod_plug(pod) # direct TCP, no cloud
|
|
34
|
+
return htf.Test(
|
|
35
|
+
# 1) DAC+ADC loopback: drive a 10 kHz sine and assert the round trip.
|
|
36
|
+
loopback_measure_phase(
|
|
37
|
+
bench, waveform="sine", freq=10_000, amplitude=120, offset=128,
|
|
38
|
+
samples=4096, pp_range=(180, 255), mean_range=(110, 150),
|
|
39
|
+
),
|
|
40
|
+
# 2) free-run a waveform, then snapshot the ADC on its own.
|
|
41
|
+
signal_generate_phase(bench, waveform="square", freq=1_000, amplitude=100),
|
|
42
|
+
adc_capture_phase(bench, samples=4096, sample_rate_mhz=0.5,
|
|
43
|
+
pp_range=(150, 255)),
|
|
44
|
+
test_name="benchpod_analog_loopback",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def main() -> None:
|
|
49
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
50
|
+
ap.add_argument("--pod", default=os.environ.get("BENCHPOD_CONNECTION"),
|
|
51
|
+
help="BenchPod TCP address host[:port] (default: $BENCHPOD_CONNECTION)")
|
|
52
|
+
ap.add_argument("--sn", default="SN-0001")
|
|
53
|
+
args = ap.parse_args()
|
|
54
|
+
if not args.pod:
|
|
55
|
+
ap.error("no pod connection: pass --pod or set BENCHPOD_CONNECTION")
|
|
56
|
+
|
|
57
|
+
test = build_test(args.pod)
|
|
58
|
+
test.add_output_callbacks(console_summary.ConsoleSummary())
|
|
59
|
+
test.execute(test_start=lambda: args.sn)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
main()
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""OpenHTF test: flash a DUT over the BenchPod's SWD probe, then assert its boot
|
|
3
|
+
banner — connecting **directly** to the pod over TCP (no EmbeddedCI cloud).
|
|
4
|
+
|
|
5
|
+
Run it against a pod on your LAN::
|
|
6
|
+
|
|
7
|
+
pip install embeddedci-openhtf
|
|
8
|
+
python flash_and_boot.py --pod 192.168.1.50:8080 --firmware fw.elf
|
|
9
|
+
|
|
10
|
+
Needs `openocd` on PATH (the pod is the CMSIS-DAP probe; OpenOCD drives the flash
|
|
11
|
+
algorithm). The bench wiring below — which LA channel carries SWCLK/SWDIO/NRST
|
|
12
|
+
and the DUT's UART TX/RX — is specific to your setup; edit the constants.
|
|
13
|
+
|
|
14
|
+
A JSON test record is written next to this script via OpenHTF's standard
|
|
15
|
+
OutputToJSON callback, and a PASS/FAIL summary is printed to the console.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import os
|
|
20
|
+
|
|
21
|
+
import openhtf as htf
|
|
22
|
+
from openhtf.output.callbacks import console_summary, json_factory
|
|
23
|
+
|
|
24
|
+
from embeddedci_openhtf import benchpod_plug, boot_banner_phase, flash_phase
|
|
25
|
+
|
|
26
|
+
# --- bench wiring (edit for your setup): LA channels 1-12 -------------------
|
|
27
|
+
SWCLK, SWDIO, NRESET = 11, 12, 3 # SWD probe -> DUT
|
|
28
|
+
UART_RX, UART_TX = 1, 2 # RX = LA channel sampling the DUT's TX
|
|
29
|
+
OPENOCD_TARGET = "target/stm32f4x.cfg"
|
|
30
|
+
BOOT_BANNER = "APP_OK" # substring the firmware prints when healthy
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def build_test(pod: str, firmware: str) -> htf.Test:
|
|
34
|
+
bench = benchpod_plug(pod) # direct TCP / serial connection, no cloud
|
|
35
|
+
return htf.Test(
|
|
36
|
+
flash_phase(bench, file=firmware, target=OPENOCD_TARGET,
|
|
37
|
+
swclk=SWCLK, swdio=SWDIO, nreset=NRESET),
|
|
38
|
+
boot_banner_phase(bench, rx=UART_RX, tx=UART_TX, expect=BOOT_BANNER,
|
|
39
|
+
power_cycle=True, duration=6.0),
|
|
40
|
+
test_name="benchpod_flash_and_boot",
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def main() -> None:
|
|
45
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
46
|
+
ap.add_argument("--pod", default=os.environ.get("BENCHPOD_CONNECTION"),
|
|
47
|
+
help="BenchPod address: host[:port] or a serial path "
|
|
48
|
+
"(default: $BENCHPOD_CONNECTION)")
|
|
49
|
+
ap.add_argument("--firmware", required=True, help="firmware image to flash")
|
|
50
|
+
ap.add_argument("--sn", default="SN-0001", help="DUT serial number")
|
|
51
|
+
args = ap.parse_args()
|
|
52
|
+
if not args.pod:
|
|
53
|
+
ap.error("no pod connection: pass --pod or set BENCHPOD_CONNECTION")
|
|
54
|
+
|
|
55
|
+
test = build_test(args.pod, args.firmware)
|
|
56
|
+
test.add_output_callbacks(
|
|
57
|
+
json_factory.OutputToJSON("./{dut_id}.{start_time_millis}.json", indent=2),
|
|
58
|
+
console_summary.ConsoleSummary(),
|
|
59
|
+
)
|
|
60
|
+
test.execute(test_start=lambda: args.sn)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
if __name__ == "__main__":
|
|
64
|
+
main()
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""OpenHTF test: a no-flash power + UART smoke test over a **direct serial**
|
|
3
|
+
connection to the BenchPod (no EmbeddedCI cloud, no OpenOCD).
|
|
4
|
+
|
|
5
|
+
Useful as a first bring-up: power the target, watch its boot output, assert a
|
|
6
|
+
banner, and read back a measured value the firmware prints. Run it with the pod
|
|
7
|
+
on a USB-serial port::
|
|
8
|
+
|
|
9
|
+
pip install embeddedci-openhtf
|
|
10
|
+
python serial_smoke.py --pod /dev/ttyACM0 # or COM5 on Windows
|
|
11
|
+
|
|
12
|
+
The custom phase shows the general pattern: declare measurements with
|
|
13
|
+
``@htf.measures``, grab the pod with ``@htf.plug``, and call the SDK directly.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import argparse
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
|
|
20
|
+
import openhtf as htf
|
|
21
|
+
from openhtf.output.callbacks import console_summary
|
|
22
|
+
|
|
23
|
+
from embeddedci import benchpod
|
|
24
|
+
from embeddedci_openhtf import benchpod_plug, record_uart
|
|
25
|
+
|
|
26
|
+
UART_RX, UART_TX = 1, 2 # edit for your wiring (LA channels 1-12)
|
|
27
|
+
BOOT_BANNER = "APP_OK"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def make_smoke_phase(bench: type):
|
|
31
|
+
@htf.PhaseOptions(name="power_and_read")
|
|
32
|
+
@htf.measures(
|
|
33
|
+
htf.Measurement("boot_ok").equals(True).doc("boot banner seen"),
|
|
34
|
+
htf.Measurement("vbat_mv").in_range(3000, 3600).with_units("mV")
|
|
35
|
+
.doc("battery voltage the firmware reports"),
|
|
36
|
+
)
|
|
37
|
+
@htf.plug(bench=bench)
|
|
38
|
+
def _phase(test, bench):
|
|
39
|
+
# power-cycle and capture the boot output in one shot
|
|
40
|
+
cap = bench.power_cycle_and_capture(
|
|
41
|
+
rx=UART_RX, tx=UART_TX, efuse=benchpod.INTERNAL,
|
|
42
|
+
delay=1.0, duration=5.0, until=BOOT_BANNER,
|
|
43
|
+
)
|
|
44
|
+
record_uart(test, cap, name="boot_ok") # sets boot_ok + attaches uart.txt
|
|
45
|
+
|
|
46
|
+
# parse a value the firmware prints, e.g. "VBAT=3301mV"
|
|
47
|
+
m = re.search(r"VBAT=(\d+)mV", cap.text)
|
|
48
|
+
test.measurements.vbat_mv = int(m.group(1)) if m else 0
|
|
49
|
+
|
|
50
|
+
return _phase
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def main() -> None:
|
|
54
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
55
|
+
ap.add_argument("--pod", default=os.environ.get("BENCHPOD_CONNECTION"),
|
|
56
|
+
help="serial path (e.g. /dev/ttyACM0 or COM5), or host:port; "
|
|
57
|
+
"default: $BENCHPOD_CONNECTION")
|
|
58
|
+
ap.add_argument("--sn", default="SN-0001")
|
|
59
|
+
args = ap.parse_args()
|
|
60
|
+
if not args.pod:
|
|
61
|
+
ap.error("no pod connection: pass --pod or set BENCHPOD_CONNECTION")
|
|
62
|
+
|
|
63
|
+
bench = benchpod_plug(args.pod) # direct connection, no cloud
|
|
64
|
+
test = htf.Test(make_smoke_phase(bench), test_name="benchpod_serial_smoke")
|
|
65
|
+
test.add_output_callbacks(console_summary.ConsoleSummary())
|
|
66
|
+
test.execute(test_start=lambda: args.sn)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
if __name__ == "__main__":
|
|
70
|
+
main()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""OpenHTF station loop with a **persistent** BenchPod connection over direct TCP
|
|
3
|
+
(no EmbeddedCI cloud).
|
|
4
|
+
|
|
5
|
+
A manufacturing station tests one DUT after another. Opening a fresh connection
|
|
6
|
+
per DUT adds latency, so this uses ``benchpod_plug(..., persistent=True)``: the
|
|
7
|
+
connection is opened once and reused across every ``test.execute()``, re-checked
|
|
8
|
+
with a ping each run and reconnected if it dropped. It is closed once at the end
|
|
9
|
+
with ``close_persistent_benchpods()``.
|
|
10
|
+
|
|
11
|
+
pip install embeddedci-openhtf
|
|
12
|
+
python station.py --pod 192.168.1.50:8080
|
|
13
|
+
# enter each board's serial number when prompted; Ctrl-C to stop
|
|
14
|
+
|
|
15
|
+
The pod connection stays up for the whole session; only the per-DUT test phases
|
|
16
|
+
repeat.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import os
|
|
21
|
+
|
|
22
|
+
import openhtf as htf
|
|
23
|
+
from openhtf.output.callbacks import console_summary
|
|
24
|
+
from openhtf.plugs import user_input
|
|
25
|
+
|
|
26
|
+
from embeddedci_openhtf import (
|
|
27
|
+
benchpod_plug,
|
|
28
|
+
boot_banner_phase,
|
|
29
|
+
close_persistent_benchpods,
|
|
30
|
+
power_phase,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
UART_RX, UART_TX = 1, 2 # edit for your wiring (LA channels 1-12)
|
|
34
|
+
BOOT_BANNER = "APP_OK"
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def main() -> None:
|
|
38
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
39
|
+
ap.add_argument("--pod", default=os.environ.get("BENCHPOD_CONNECTION"),
|
|
40
|
+
help="BenchPod address host[:port] or serial path "
|
|
41
|
+
"(default: $BENCHPOD_CONNECTION)")
|
|
42
|
+
args = ap.parse_args()
|
|
43
|
+
if not args.pod:
|
|
44
|
+
ap.error("no pod connection: pass --pod or set BENCHPOD_CONNECTION")
|
|
45
|
+
|
|
46
|
+
# persistent=True -> one connection shared across every DUT this session
|
|
47
|
+
bench = benchpod_plug(args.pod, persistent=True)
|
|
48
|
+
test = htf.Test(
|
|
49
|
+
power_phase(bench, on=True),
|
|
50
|
+
boot_banner_phase(bench, rx=UART_RX, tx=UART_TX, expect=BOOT_BANNER,
|
|
51
|
+
power_cycle=True, duration=6.0),
|
|
52
|
+
power_phase(bench, on=False, name="power_off"),
|
|
53
|
+
test_name="benchpod_station",
|
|
54
|
+
)
|
|
55
|
+
test.add_output_callbacks(console_summary.ConsoleSummary())
|
|
56
|
+
|
|
57
|
+
try:
|
|
58
|
+
# OpenHTF returns from execute() after each DUT; loop for the next one.
|
|
59
|
+
while test.execute(test_start=user_input.prompt_for_test_start()):
|
|
60
|
+
pass
|
|
61
|
+
except KeyboardInterrupt:
|
|
62
|
+
print("\nstation stopped")
|
|
63
|
+
finally:
|
|
64
|
+
close_persistent_benchpods() # close the shared connection once
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
main()
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "embeddedci-openhtf"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "OpenHTF plug for driving an EmbeddedCI BenchPod directly over TCP or serial (no cloud)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "Apache-2.0" }
|
|
12
|
+
authors = [{ name = "EmbeddedCI" }]
|
|
13
|
+
keywords = ["embedded", "hardware-in-the-loop", "openhtf", "benchpod", "swd", "manufacturing-test"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"License :: OSI Approved :: Apache Software License",
|
|
17
|
+
"Operating System :: OS Independent",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.10",
|
|
20
|
+
"Programming Language :: Python :: 3.11",
|
|
21
|
+
"Programming Language :: Python :: 3.12",
|
|
22
|
+
"Programming Language :: Python :: 3.13",
|
|
23
|
+
"Topic :: Software Development :: Testing",
|
|
24
|
+
]
|
|
25
|
+
dependencies = [
|
|
26
|
+
"embeddedci>=0.2",
|
|
27
|
+
"openhtf>=1.6",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = ["pytest>=7.0"]
|
|
32
|
+
|
|
33
|
+
[project.urls]
|
|
34
|
+
Homepage = "https://embeddedci.com"
|
|
35
|
+
Repository = "https://github.com/embeddedci-com/embeddedci-python"
|
|
36
|
+
Issues = "https://github.com/embeddedci-com/embeddedci-python/issues"
|
|
37
|
+
|
|
38
|
+
# Inside the workspace, resolve embeddedci from the sibling source tree (see the
|
|
39
|
+
# repo-root pyproject.toml [tool.uv.workspace]). On PyPI this is ignored and the
|
|
40
|
+
# published embeddedci is used.
|
|
41
|
+
[tool.uv.sources]
|
|
42
|
+
embeddedci = { workspace = true }
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/embeddedci_openhtf"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|