You can run a hardware test in Python in 15 minutes. This guide walks you through writing a functional test with OpenHTF and TofuPilot that measures voltages, checks limits, and logs results automatically. No LabVIEW, no TestStand, no license fees.
Prerequisites
- Python 3.9+
- pip (Python package manager)
Step 1: Install the Dependencies
pip install openhtf tofupilotOpenHTF is Google's open-source hardware test framework. TofuPilot connects it to a cloud dashboard for analytics, traceability, and yield tracking.
Step 2: Write Your First Test Phase
A test phase is a Python function that takes measurements. OpenHTF handles the structure: you declare what you're measuring, set limits, and write the logic.
import openhtf as htf
from openhtf.util import units
@htf.measures(
htf.Measurement("voltage_3v3")
.in_range(3.2, 3.4)
.with_units(units.VOLT)
.doc("3.3V rail voltage"),
)
@htf.PhaseOptions(timeout_s=10)
def test_voltage(test):
test.measurements.voltage_3v3 = 3.31 # Replace with real instrument readThe @htf.measures decorator defines what this phase records. .in_range(3.2, 3.4) sets pass/fail limits. .with_units() adds units for analytics. The phase passes if the value falls within the range.
Step 3: Add Multiple Measurements
A single phase can measure several things. This is useful for testing all power rails in one step.
import openhtf as htf
from openhtf.util import units
@htf.measures(
htf.Measurement("voltage_3v3")
.in_range(3.2, 3.4)
.with_units(units.VOLT)
.doc("3.3V rail"),
htf.Measurement("voltage_5v0")
.in_range(4.8, 5.2)
.with_units(units.VOLT)
.doc("5.0V rail"),
htf.Measurement("board_current")
.in_range(0.05, 0.25)
.with_units(units.AMPERE)
.doc("Total board current draw"),
)
def test_power_rails(test):
test.measurements.voltage_3v3 = 3.31
test.measurements.voltage_5v0 = 5.02
test.measurements.board_current = 0.12Step 4: Use a Plug for Instrument Control
Plugs manage instrument connections. OpenHTF calls setUp() before tests and tearDown() after, so your instruments connect and disconnect automatically. Plugs are injected into phases using the @htf.plug() decorator.
import openhtf as htf
from openhtf.plugs import BasePlug
from openhtf.util import units
class MultimeterPlug(BasePlug):
"""Manage multimeter connection lifecycle."""
def setUp(self):
# Replace with real instrument connection
self._readings = iter([3.31, 5.02, 0.12])
def read_dc_voltage(self) -> float:
return next(self._readings)
def read_dc_current(self) -> float:
return next(self._readings)
def tearDown(self):
pass # Replace with instrument disconnect
@htf.measures(
htf.Measurement("rail_3v3")
.in_range(3.2, 3.4)
.with_units(units.VOLT),
)
@htf.plug(dmm=MultimeterPlug)
def test_with_instrument(test, dmm):
test.measurements.rail_3v3 = dmm.read_dc_voltage()The @htf.plug(dmm=MultimeterPlug) decorator tells OpenHTF to create a MultimeterPlug instance and pass it as the dmm argument. Don't use type hints for plug injection (e.g., dmm: MultimeterPlug). The @htf.plug() decorator is required.
Step 5: Add Different Measurement Types
OpenHTF supports numeric ranges, exact matches, and boolean checks.
import openhtf as htf
from openhtf.util import units
# Boolean: pass if True
@htf.measures(
htf.Measurement("led_on").equals(True).doc("Power LED is illuminated"),
)
def test_led(test):
test.measurements.led_on = True
# String: pass if exact match
@htf.measures(
htf.Measurement("firmware_version").equals("2.1.0").doc("Expected firmware"),
)
def test_firmware(test):
test.measurements.firmware_version = "2.1.0"
# Numeric range: pass if within limits
@htf.measures(
htf.Measurement("temperature")
.in_range(20, 30)
.with_units(units.DEGREE_CELSIUS)
.doc("Board temperature"),
)
def test_temperature(test):
test.measurements.temperature = 24.5| Validator | Use Case | Example |
|---|---|---|
.in_range(low, high) | Numeric within limits | Voltage, current, resistance |
.equals(value) | Exact match | Firmware version, boolean flags |
.with_units(unit) | Attach unit for analytics | units.VOLT, units.AMPERE |
.doc(text) | Description for reports | Shown in TofuPilot dashboard |
Step 6: Assemble and Run the Test
Connect the phases into a test, add TofuPilot for cloud logging, and run it.
import openhtf as htf
from openhtf.plugs import BasePlug
from openhtf.util import units
from tofupilot.openhtf import TofuPilot
class MultimeterPlug(BasePlug):
def setUp(self):
self._readings = iter([3.31, 5.02, 0.12])
def read_dc_voltage(self) -> float:
return next(self._readings)
def read_dc_current(self) -> float:
return next(self._readings)
def tearDown(self):
pass
@htf.measures(
htf.Measurement("voltage_3v3")
.in_range(3.2, 3.4)
.with_units(units.VOLT)
.doc("3.3V rail"),
htf.Measurement("voltage_5v0")
.in_range(4.8, 5.2)
.with_units(units.VOLT)
.doc("5.0V rail"),
htf.Measurement("board_current")
.in_range(0.05, 0.25)
.with_units(units.AMPERE)
.doc("Board current draw"),
)
@htf.plug(dmm=MultimeterPlug)
def test_power(test, dmm):
test.measurements.voltage_3v3 = dmm.read_dc_voltage()
test.measurements.voltage_5v0 = dmm.read_dc_voltage()
test.measurements.board_current = dmm.read_dc_current()
@htf.measures(
htf.Measurement("led_on").equals(True),
)
def test_led(test):
test.measurements.led_on = True
@htf.measures(
htf.Measurement("firmware_version").equals("2.1.0"),
)
def test_firmware(test):
test.measurements.firmware_version = "2.1.0"
def main():
test = htf.Test(
test_power,
test_led,
test_firmware,
procedure_id="FCT-001",
part_number="PCBA-100",
)
with TofuPilot(test):
test.execute(test_start=lambda: input("Scan serial number: "))
if __name__ == "__main__":
main()When you run this, OpenHTF prompts for a serial number, executes each phase in order, checks measurements against limits, and TofuPilot uploads the results. You get FPY, Cpk, and control charts in the dashboard with zero extra code.
What Happens Behind the Scenes
| Step | Who Does It | What Happens |
|---|---|---|
| Serial number prompt | OpenHTF | Operator scans or types the DUT serial number |
| Phase execution | OpenHTF | Runs test_power, test_led, test_firmware in order |
| Measurement validation | OpenHTF | Checks each value against declared limits |
| Result upload | TofuPilot | Sends measurements, limits, units, pass/fail to cloud |
| Analytics | TofuPilot | FPY, Cpk, control charts, failure Pareto updated automatically |