OpenHTF on TofuPilot
Last updated on May 21, 2026
OpenHTF is a popular open-source Python hardware test framework maintained by Google, documented by TofuPilot at openhtf.com. It ships many hardware-test-specific features that general-purpose frameworks lack, making it an excellent choice for various test scenarios.
With TofuPilot, you can deploy OpenHTF scripts to your stations via Git push and stream live test data and the operator UI to both the kiosk and the dashboard, with zero configuration.
Getting started
To get started with OpenHTF on TofuPilot:
- Clone our OpenHTF starter template to your favorite Git provider and deploy it on TofuPilot.
- If you have an OpenHTF procedure, push it to your Git provider and import it from the New Procedure wizard.
Integration
OpenHTF is a code-first Python test framework that uses decorators to attach measurements and shared resources to phase functions, then runs them sequentially against the unit under test.
import openhtf as htf
from openhtf.util import units
from openhtf.plugs.user_input import UserInput
# Plug
class DMM(htf.plugs.BasePlug):
def measure_voltage(self) -> float:
return 5.03
# Phase
@htf.plug(dmm=DMM) # inject plug
@htf.measures(htf.Measurement("voltage").in_range(4.8, 5.2).with_units(units.VOLT)) # validator
def measure_voltage(test, dmm):
test.logger.info("Reading DMM") # log
test.measurements.voltage = dmm.measure_voltage() # measurement
test.attach_from_file("data/scope.png") # attachment
# Operator prompt
@htf.plug(user_input=UserInput)
def prompt_serial(test, user_input):
test.measurements.serial_number = user_input.prompt("Serial number?", text_input=True) # operator input
htf.Test(prompt_serial, measure_voltage).execute() # run phases on the unit under testThe TofuPilot CLI runs your OpenHTF scripts natively and streams phases, measurements, UI prompts, logs, and attachments to the dashboard.
Phases
In OpenHTF, a phase is a Python function that receives a test context, runs one step of the procedure, and returns a PhaseResult to control whether the next phase runs.
import openhtf as htf
def power_on(test):
return htf.PhaseResult.CONTINUE
htf.Test(power_on).execute()TofuPilot maps OpenHTF phases to the phase resource as follows:
| OpenHTF | TofuPilot | Notes |
|---|---|---|
name | name | Capped at 200 chars. |
outcome | outcome | Same enum: PASS, FAIL, SKIP, ERROR. |
start_time_millis | startedAt | Epoch ms → ISO 8601 timestamp. |
end_time_millis | endedAt | Epoch ms → ISO 8601 timestamp. |
codeinfo.docstring | docstring | Capped at 50000 chars. |
result | retryCount | Only PhaseResult.REPEAT is tracked, as the retry count. Other control-flow values are not stored. |
measurements | measurements | Internally linked to the phase. |
attachments | attachments | Flattened to the run level; phase association is not preserved. |
marginal | not persisted | Marginal validators on measurements are expanded into >= / <= validator entries on the measurement. |
options, subtest_name, diagnosers, diagnosis_results, failure_diagnosis_results | not persisted |
Measurements
In OpenHTF, a measurement is a typed value declared with @htf.measures on a phase, optionally constrained by validators and tagged with units, then assigned at runtime via test.measurements.<name>.
import openhtf as htf
from openhtf.util import units
@htf.measures(
htf.Measurement("voltage").in_range(4.8, 5.2).with_units(units.VOLT)
)
def measure_voltage(test):
test.measurements.voltage = 5.03TofuPilot maps OpenHTF measurements to the measurement resource as follows:
| OpenHTF | TofuPilot | Notes |
|---|---|---|
name | name | Capped at 200 chars. |
outcome | outcome | OpenHTF PASS / FAIL map 1:1. UNSET, PARTIALLY_SET, SKIPPED are collapsed to UNSET. |
units | units | OpenHTF unit object reduced to its suffix string. Capped at 60 chars. |
docstring | docstring | Capped at 50000 chars. |
measured_value | value | Routed to a typed table: numeric_measurement, string_measurement, boolean_measurement, or json_measurement. |
dimensions + measured_value | multi_dimensional_measurement + data_series | Multi-dim measurements stored separately; per-axis validators not supported. |
validators | expanded | Each validator becomes a per-limit >= / <= entry on the measurement. |
marginal | expanded | Marginal validators are emitted as separate >= / <= entries flagged as marginal. |
set_time_millis, allow_fail, conditional_validators | not persisted |
Logs
In OpenHTF, every phase receives a test.logger — a standard Python logger that captures records into the test for later inspection.
def measure_voltage(test):
test.logger.info("Calibration started")TofuPilot maps OpenHTF log records to the log resource as follows:
| OpenHTF | TofuPilot | Notes |
|---|---|---|
level | level | Same enum: DEBUG, INFO, WARNING, ERROR, CRITICAL. |
timestamp_millis | timestamp | Epoch ms → ISO 8601 timestamp. |
message | message | Capped at 50000 chars. |
source | sourceFile | Capped at 200 chars. |
lineno | lineNumber | |
logger_name | not persisted |
Logs are stored at the run level; per-phase association is not preserved.
Attachments
In OpenHTF, an attachment is binary content tied to a phase, declared via test.attach(...) or test.attach_from_file(...).
def capture(test):
test.attach_from_file("data/scope.png")TofuPilot maps OpenHTF attachments to the upload resource as follows:
| OpenHTF | TofuPilot | Notes |
|---|---|---|
| attachment key | name | Capped at 200 chars. |
mimetype | contentType | Capped at 60 chars. |
size | size | Bytes. |
sha1 | etag | Stored as the upload's S3 ETag; the OpenHTF SHA-1 itself is dropped. |
Attachments are linked to the run via run_attachments; per-phase association is not preserved.
Operator UI
OpenHTF ships an openhtf.plugs.user_input.UserInput plug to ask the operator for input mid-run. TofuPilot replaces it with a CLI-side implementation that streams the prompt to the kiosk and the dashboard.
import openhtf as htf
from openhtf.plugs.user_input import UserInput
@htf.plug(user_input=UserInput)
def prompt_serial(test, user_input):
serial = user_input.prompt("Serial number?", text_input=True)
test.measurements.serial_number = serialTofuPilot supports the following UserInput.prompt arguments:
| OpenHTF | TofuPilot | Notes |
|---|---|---|
message | supported | Displayed verbatim on the kiosk and the dashboard. |
text_input | supported | Renders a text input when True, otherwise an acknowledge button. |
timeout_s | supported | Auto-cancels the prompt after timeout_s seconds. |
image_url | supported | Rendered inline next to the prompt. |
cli_color | ignored | TofuPilot has no CLI-color concept on the kiosk or dashboard. |
Plugs
In OpenHTF, a plug is a class subclassing BasePlug, declared with @htf.plug(name=PlugClass) on a phase. The framework instantiates the plug, injects it as a phase argument, and tears it down when the test ends.
import openhtf as htf
class DMM(htf.plugs.BasePlug):
def measure_voltage(self) -> float:
return 5.03
@htf.plug(dmm=DMM)
def measure(test, dmm):
test.logger.info(f"voltage: {dmm.measure_voltage()}")TofuPilot runs OpenHTF plugs as-is in Python. setUp, tearDown, and dependency injection work unchanged, but the dashboard does not surface plug instances, lifecycle events, or FrontendAwareBasePlug state. Only the data plugs emit (measurements, logs, attachments, prompts) is captured. UserInput is the one exception, replaced by a TofuPilot implementation that streams prompts.
Run outcome
In OpenHTF, test_record.outcome summarizes the whole test (PASS, FAIL, ERROR, TIMEOUT, ABORTED).
TofuPilot maps the test outcome to the run resource as follows:
| OpenHTF | TofuPilot | Notes |
|---|---|---|
outcome PASS / FAIL | outcome | 1:1 mapping. |
outcome ERROR | outcome | Collapsed to ERROR; unhandled exceptions land here. |
outcome TIMEOUT | outcome | Collapsed to ERROR. |
outcome ABORTED | outcome | Collapsed to ERROR. |
DUT ID
In OpenHTF, the DUT ID is resolved by the callable passed to test.execute(...) and stored on test_record.dut_id.
import openhtf as htf
def power_on(test):
return htf.PhaseResult.CONTINUE
htf.Test(power_on).execute(lambda: "SN-0001")TofuPilot uses the OpenHTF DUT ID as the unit serial number on the run. The UserInput plug can also drive it interactively when the operator types it into the kiosk prompt.
Test metadata
In OpenHTF, test_record.metadata is a free-form dict mutated by phases (e.g. test.metadata["serial_number"] = read_from_eeprom()).
TofuPilot reads a known set of keys from test_record.metadata and promotes them to the run:
| OpenHTF metadata key | TofuPilot | Notes |
|---|---|---|
procedure_id | procedureId | Procedure identifier on the run. |
part_number | partNumber | Part number on the run. |
revision | revision | Part revision. |
batch_number | batchNumber | Production batch. |
sub_units | subUnits | List of {"serial_number": "..."} for assembled units. |
| any other key | not persisted | Stays on the Python test.metadata dict, not promoted. |
PhaseGroup
OpenHTF's PhaseGroup runs phases in three slots — setup, main, teardown — and guarantees teardown even if setup or main fails.
import openhtf as htf
def power_on(test): ...
def measure(test): ...
def power_off(test): ...
htf.Test(htf.PhaseGroup(setup=[power_on], main=[measure], teardown=[power_off])).execute()TofuPilot honors the OpenHTF runtime contract — phases execute in the right order and teardown still fires — but flattens the group at capture time. Phase rows appear in execution order with no setup / main / teardown tag; the grouping itself is not persisted.
Offline upload
When the bench can lose connectivity, the CLI queues runs locally and uploads them when the network comes back. Each queued run keeps its original timestamp, so analytics stay accurate even when uploads land hours later. See tofupilot queue.
Python connector (self-managed)
For users running their own deployment pipeline who want to skip the TofuPilot CLI, the tofupilot Python package exposes a TofuPilot context manager that wraps htf.Test(...) directly. You handle distribution and execution; TofuPilot just receives the run.
pip install tofupilot openhtfimport openhtf as htf
from tofupilot.openhtf import TofuPilot
def phase_one(test):
return htf.PhaseResult.CONTINUE
def main():
test = htf.Test(
phase_one,
procedure_id="FVT1",
part_number="PCB01",
)
with TofuPilot(test):
test.execute(lambda: "SN-0001")
if __name__ == "__main__":
main()| Field | Required | Description |
|---|---|---|
procedure_id | Yes | Dashboard procedure UUID or external identifier. |
part_number | Yes | Part number for the unit. |
revision | No | Part revision. Defaults to A. |
batch_number | No | Production batch. |
sub_units | No | List of {"serial_number": "..."} for assembled units. |
For offline benches, write a JSON report during the run and upload it later from any machine with network access.
from openhtf.output.callbacks import json_factory
from tofupilot import TofuPilotClient
test.add_output_callbacks(json_factory.OutputToJSON("./report.json", indent=2))
test.execute(lambda: "SN-0001")
client = TofuPilotClient()
client.create_run_from_openhtf_report("./report.json")Unsupported features
The following OpenHTF features run as-is in Python, but TofuPilot does not surface them on the dashboard.
| Feature | Status |
|---|---|
Monitors (@openhtf.monitors(...)) | Sampling thread runs, but emitted values are not captured. |
Diagnoses (@PhaseDiagnoser, @TestDiagnoser, DiagnosesStore, Diagnosis) | Run as-is; diagnosis_results, failure_diagnosis_results, and diagnosers are dropped. |
Conditional validators (_ConditionalValidator) | Validator runs only when its diagnosis fires; not surfaced as a separate validator entry. |
Subtests (Subtest, PhaseResult.FAIL_SUBTEST) | Phases execute, but subtest grouping and per-subtest outcomes are not persisted. |
Branches and checkpoints (BranchSequence, PhaseFailureCheckpoint, DiagnosisCheckpoint) | Control flow is honored; the branch / checkpoint nodes themselves are not persisted. |
with_plugs (plug-type substitution) | Runs as plain Python; no extra metadata on the dashboard. |
FrontendAwareBasePlug state | The plug runs, but its frontend state stream is ignored. |
PhaseOptions (timeout_s, run_if, repeat_limit, force_repeat, ...) | Honored by OpenHTF at runtime; the options object itself is not persisted on the phase. |
User-registered output.callbacks (json_factory, mfg-inspector, ...) | Still fire; TofuPilot does not consume the resulting files. |
OpenHTF output.servers.web_gui | Use the TofuPilot dashboard instead. |
How is this guide?
