Skip to content
Getting Started

Build Your First Hardware Test

Build and run a hardware test in Python in 15 minutes using OpenHTF and TofuPilot, with measurements, limits, and automatic data logging.

JJulien Buteau
beginner12 min readMarch 14, 2026

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

install.sh
pip install openhtf tofupilot

OpenHTF 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.

first_test.py
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 read

The @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.

multi_measurement.py
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.12

Step 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.

plug_example.py
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.

measurement_types.py
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
ValidatorUse CaseExample
.in_range(low, high)Numeric within limitsVoltage, current, resistance
.equals(value)Exact matchFirmware version, boolean flags
.with_units(unit)Attach unit for analyticsunits.VOLT, units.AMPERE
.doc(text)Description for reportsShown in TofuPilot dashboard

Step 6: Assemble and Run the Test

Connect the phases into a test, add TofuPilot for cloud logging, and run it.

full_test.py
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

StepWho Does ItWhat Happens
Serial number promptOpenHTFOperator scans or types the DUT serial number
Phase executionOpenHTFRuns test_power, test_led, test_firmware in order
Measurement validationOpenHTFChecks each value against declared limits
Result uploadTofuPilotSends measurements, limits, units, pass/fail to cloud
AnalyticsTofuPilotFPY, Cpk, control charts, failure Pareto updated automatically

More Guides

Put this guide into practice