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.
@@ -0,0 +1,13 @@
1
+ # Python
2
+ __pycache__/
3
+ *.py[cod]
4
+ *.egg-info/
5
+ .pytest_cache/
6
+
7
+ # Build artifacts
8
+ build/
9
+ dist/
10
+
11
+ # Virtualenvs
12
+ .venv/
13
+ venv/
@@ -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"]