Tutorial — Test a Resistor with OpenHTF
Step-by-step OpenHTF tutorial. Build a resistor test with a power supply and multimeter plug (PyVISA, with a simulation mode), add a measurement with limits, read the console summary, and upload the record.
Last updated · Verified with OpenHTF 1.6.1
Build a complete test in six steps: apply a voltage with a programmable supply, read the current with a multimeter, compute the resistance with Ohm's law and check it against a tolerance. This mirrors the resistor tutorial in the upstream repository, rewritten so every step runs without hardware.
You will use phases, measurements with validators and units, plugs with a simulation flag, and configuration to switch between the bench and the simulator.
An empty test
Create main_test.py with one phase that does nothing yet:
import openhtf as htf
def resistor_test(test):
"""Placeholder phase; filled in below."""
def main():
test = htf.Test(resistor_test)
test.execute(lambda: "R-0001")
if __name__ == "__main__":
main()======================= test: openhtf_test outcome: PASS ======================A phase that returns nothing is a CONTINUE, so the test passes.
Declare the measurement
Tell OpenHTF the phase will record resistor_val:
@htf.measures(
htf.Measurement("resistor_val")
.doc("Computed resistor value")
)
def resistor_test(test):
"""Placeholder phase; filled in below."""======================= test: openhtf_test outcome: FAIL ======================It fails: a declared measurement that is never set is a failure (see allow_unset_measurements in Configuration). To see why it failed, add a console summary callback:
from openhtf.output.callbacks import console_summary
def main():
test = htf.Test(resistor_test)
test.add_output_callbacks(console_summary.ConsoleSummary())
test.execute(lambda: "R-0001")openhtf_test:FAIL
failed phase: resistor_test [ran for 0.00 sec]
failed_item: resistor_val (Outcome.UNSET)
measured_value: UNSET
validators:
======================= test: openhtf_test outcome: FAIL ======================Set a placeholder value and the test passes again:
def resistor_test(test):
test.measurements["resistor_val"] = 10Write the plugs
Instruments live in plugs. These two wrap a multimeter and a power supply over PyVISA and take a simulate flag so the tutorial runs on any laptop. Copy them into resistor_plugs.py:
import random
import time
from openhtf.core import base_plugs
class MultimeterPlug(base_plugs.BasePlug):
"""Digital multimeter over SCPI. `simulate=True` returns random values."""
def __init__(self, simulate: bool = False) -> None:
self.simulate = simulate
self.rm = None
self.dmm = None
def connect(self) -> None:
if self.simulate:
return
import pyvisa # only needed on the bench
self.rm = pyvisa.ResourceManager()
self.dmm = self.rm.open_resource("USB0::6833::8458::DM8A265201811::0::INSTR")
self.logger.info("Connected to %s", self.dmm.query("*IDN?").strip())
def read_current(self) -> float:
if self.simulate:
current = random.uniform(0.0006, 0.0008) # ~4 V across ~5.6 kΩ
else:
current = float(self.dmm.query("MEASure:CURRent:DC? AUTO,1E-3"))
self.logger.info("Current: %.6f A", current)
return current
def tearDown(self) -> None:
if self.rm:
self.rm.close()
class PowerSupplyPlug(base_plugs.BasePlug):
"""Programmable power supply over SCPI."""
def __init__(self, simulate: bool = False) -> None:
self.simulate = simulate
self.rm = None
self.supply = None
def connect(self) -> None:
if self.simulate:
return
import pyvisa
self.rm = pyvisa.ResourceManager()
self.supply = self.rm.open_resource("USB0::6833::42152::DP9D264501253::0::INSTR")
self.supply.write(":OUTP ALL, OFF")
def set_voltage(self, voltage: float, channel: str = "CH1") -> None:
if not self.simulate:
self.supply.write(f":APPL {channel}, {voltage}")
self.supply.write(f":OUTP {channel}, ON")
self.logger.info("Set %s to %.2f V", channel, voltage)
def tearDown(self) -> None:
if self.supply:
self.supply.write(":OUTP ALL, OFF")
if self.rm:
self.rm.close()tearDown() runs after the test whether it passed or not, so the supply is always switched off. The VISA resource strings are the Rigol DM858 and DP932E used upstream; replace them with your own (pyvisa.ResourceManager().list_resources() lists what is connected — see the PyVISA plug guide).
Use the plugs in the phase
Inject both plugs and compute the resistance:
import time
import openhtf as htf
from openhtf.output.callbacks import console_summary
from openhtf.util import units
import resistor_plugs
@htf.measures(
htf.Measurement("resistor_val")
.doc("Computed resistor value")
.with_units(units.OHM)
)
@htf.plug(dmm=resistor_plugs.MultimeterPlug)
@htf.plug(supply=resistor_plugs.PowerSupplyPlug)
def resistor_test(test, dmm, supply):
supply.connect()
dmm.connect()
input_voltage = 4.0 # V
supply.set_voltage(input_voltage)
time.sleep(0.5) # let the current settle
current = dmm.read_current()
measured_r = round(input_voltage / current, 1)
test.measurements["resistor_val"] = measured_r
test.logger.info("R = %.1f Ω", measured_r)
def main():
test = htf.Test(resistor_test)
test.add_output_callbacks(console_summary.ConsoleSummary())
test.execute(lambda: "R-0001")
if __name__ == "__main__":
main()Right now the plugs are constructed with simulate=False, so this step needs the bench. The next step fixes that.
Switch simulation on with configuration
The plugs take simulate in __init__, but @htf.plug(dmm=MultimeterPlug) constructs them with no arguments. bind_init_args binds constructor arguments to configuration values, so the same script runs on the bench (simulate: false) and on a laptop (simulate: true):
from openhtf.util import configuration
CONF = configuration.CONF
SIMULATE = CONF.declare("simulate", default_value=False, description="Bypass the instruments")
MultimeterPlug = configuration.bind_init_args(resistor_plugs.MultimeterPlug, SIMULATE)
PowerSupplyPlug = configuration.bind_init_args(resistor_plugs.PowerSupplyPlug, SIMULATE)
@htf.measures(htf.Measurement("resistor_val").doc("Computed resistor value").with_units(units.OHM))
@htf.plug(dmm=MultimeterPlug)
@htf.plug(supply=PowerSupplyPlug)
def resistor_test(test, dmm, supply):
...Run it simulated without touching the code:
python main_test.py --config-value simulate=true -vI 10:42:07 <plug: PowerSupplyPlug> - Set CH1 to 4.00 V
I 10:42:08 <plug: MultimeterPlug> - Current: 0.000712 A
I 10:42:08 <phase: resistor_test> - R = 5617.9 Ω
openhtf_test:PASS
======================= test: openhtf_test outcome: PASS ======================Add limits
A 5.6 kΩ ±5 % resistor must read between 5320 Ω and 5880 Ω. Add the validator:
@htf.measures(
htf.Measurement("resistor_val")
.doc("Computed resistor value")
.in_range(5320, 5880)
.with_units(units.OHM)
)With a 220 Ω part on the bench (or by forcing current = 0.018 in the simulator) the summary now explains the failure:
openhtf_test:FAIL
failed phase: resistor_test [ran for 0.52 sec]
failed_item: resistor_val (Outcome.FAIL)
measured_value: 222.2
validators:
validator: 5320 <= x <= 5880
======================= test: openhtf_test outcome: FAIL ======================You have a deployable resistor test. From here:
- Save every run:
test.add_output_callbacks(json_factory.OutputToJSON("./records/{dut_id}.{start_time_millis}.json", indent=2))— Output Callbacks. - Ask the operator for the serial number instead of hard-coding it — Device Under Test.
- Give the operator a browser UI — Operator UI.
- Track yield across thousands of resistors:
test.add_output_callbacks(upload())— Manufacturing Test Analytics.
Related
First Test
Learn how to install OpenHTF and create your first test script with step-by-step instructions to set up a simple test phase and execute it.
Concepts
The OpenHTF vocabulary — DUT, station, test, phase, measurement, validator, plug, attachment, test record, output callback — each defined in one paragraph with a link to its page.