Deploying OpenHTF to a Production Test Station
Take an OpenHTF test from a developer laptop to a factory station — project layout, pinned dependencies, per-station YAML configuration, launcher scripts and autostart on Windows and Linux, kiosk browser for the Operator UI, record storage and upload, updates and rollback.
Last updated · Verified with OpenHTF 1.6.1
OpenHTF has no deployment tool of its own; a station is a Python environment, your test package, a config file and something that starts it at boot. This page is the checklist that works.
Project layout
pcb01-eol/
├── pyproject.toml # or requirements.txt — pinned
├── pcb01_eol/
│ ├── __init__.py
│ ├── main.py # htf.Test assembly + entry point
│ ├── phases/ # one module per functional area
│ ├── plugs/ # instruments, DUT interfaces
│ └── limits.yaml # optional: limits per part number
├── config/
│ ├── station-eol-01.yaml
│ └── station-eol-02.yaml
├── scripts/
│ ├── run.sh / run.ps1
│ └── install.sh / install.ps1
└── tests/ # pytest for plugs and helpersKeep phases and plugs importable as a package so the same code runs from main.py, from pytest and from a REPL.
Pin everything
openhtf==1.6.1
pyvisa==1.14.1
pyvisa-py==0.7.2
pyserial==3.5
tofupilot==2.* # if uploading recordsRecreate the environment from this file on every station; never pip install interactively on a station. pip freeze > requirements.lock on the reference machine gives you the transitive pins for an audit trail. See Installation.
One config file per station
Everything that differs between stations lives in YAML loaded with --config-file; the code is identical everywhere.
station_id: EOL-01
station_server_port: 4444
dmm_resource: USB0::0x2A8D::0x0101::MY53220001::INSTR
psu_resource: TCPIP0::192.168.10.21::inst0::INSTR
uart_adapter_serial: A50285BI
rf_variant: true
records_dir: /var/lib/pcb01/recordsDeclare each key with CONF.declare(...) and bind plug constructors with bind_init_args — Configuration, PyVISA guide. python -m pcb01_eol.main --config-help prints every key a station needs.
Entry point
import openhtf as htf
from openhtf.output.callbacks import json_factory
from openhtf.output.servers import station_server
from openhtf.output.web_gui import web_launcher
from openhtf.plugs import user_input
from openhtf.util import configuration
from pcb01_eol.phases import build_phases
CONF = configuration.CONF
RECORDS_DIR = CONF.declare("records_dir", default_value="./records")
def main():
with station_server.StationServer(history_path=CONF.records_dir) as server:
web_launcher.launch(f"http://localhost:{CONF.station_server_port}")
while True:
test = htf.Test(*build_phases(), test_name="PCB01 EOL")
test.add_output_callbacks(
json_factory.OutputToJSON(
f"{CONF.records_dir}/{{dut_id}}.{{start_time_millis}}.json", indent=2),
server.publish_final_state,
)
test.execute(test_start=user_input.prompt_for_test_start())
if __name__ == "__main__":
main()Reading CONF.station_server_port after the flags are parsed works because execute()/configure() parse them; if you need the value earlier, call test.configure() first or parse with configuration.ARG_PARSER yourself (CLI flags).
Launcher
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")/.."
source .venv/bin/activate
exec python -m pcb01_eol.main --config-file "config/station-$(hostname).yaml" -vSet-Location (Split-Path $PSScriptRoot -Parent)
& .\.venv\Scripts\Activate.ps1
python -m pcb01_eol.main --config-file "config\station-$env:COMPUTERNAME.yaml" -vNaming config files after the hostname means one launcher for the whole line.
Autostart
[Unit]
Description=PCB01 end-of-line test
After=network-online.target
[Service]
User=operator
WorkingDirectory=/opt/pcb01-eol
ExecStart=/opt/pcb01-eol/scripts/run.sh
Restart=always
RestartSec=3
Environment=TOFUPILOT_API_KEY=%i
StandardOutput=append:/var/log/pcb01-eol.log
StandardError=inherit
[Install]
WantedBy=multi-user.targetsudo systemctl enable --now pcb01-eolRestart=always brings the station back after a crash or an operator Ctrl-C. Put secrets in an EnvironmentFile= rather than the unit.
Create a task that runs powershell -File C:\pcb01-eol\scripts\run.ps1 At log on of the operator account, with "Run whether user is logged on or not" off (the Operator UI needs a desktop session for the browser) and "If the task fails, restart every 1 minute". Set TOFUPILOT_API_KEY as a user environment variable for the operator account.
Alternatively wrap the script with NSSM to run it as a service and start the kiosk browser separately at logon.
Kiosk browser
web_launcher.launch() opens the default browser; on a station you want a full-screen kiosk pointed at the fixed port instead:
chromium --kiosk --noerrdialogs --disable-infobars http://localhost:4444Start it from the same launcher (after the server is up) or from the desktop session's autostart, and remove web_launcher.launch() from the code. See Operator UI.
Records
- Write JSON to a local directory first (
records_dir); local disk survives network outages. - Ship them onward with an output callback — a database insert, an S3 sync, or TofuPilot's
upload()which queues and retries. Output Callbacks, Manufacturing Test Analytics. - Rotate or archive the local directory; a busy station produces thousands of files a month.
- Do not inline large attachments (
inline_attachments=False) if the records are indexed.
Updates and rollback
- Tag releases of the test package (
v2.1.4) and record the tag in metadata:htf.Test(..., test_version="2.1.4"). It lands in every record, so a yield change can be correlated with a test change. - Deploy with
git pull && pip install -r requirements.txtin the launcher's directory, then restart the service. Rollback isgit checkout v2.1.3and restart. - Never change limits on a station by hand; change
limits.yamlin git and redeploy.
Health checks
--config-helpon a fresh station lists every key; a missing one fails loudly at start rather than mid-shift.- A
self_testphase group that talks to every instrument and stops on failure catches unplugged cables before the first unit. - Log to a file (
-vplusStandardOutput=append:orStart-Transcript) so the console is not the only record of a crash.
Related
From TestStand
Map NI TestStand concepts to OpenHTF — sequences and step groups to tests and phase groups, numeric limit steps to measurements, preconditions to run_if and branches, station globals to configuration, report and database loggers to output callbacks — and plan a station-by-station transition.
FAQ
Answers to common OpenHTF questions and errors — blank Operator UI, unset measurements failing, ERROR vs FAIL outcomes, phase timeouts, tornado version conflicts, Python support, and how OpenHTF relates to TofuPilot.