Guides

Migrate from NI TestStand to OpenHTF

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.

Last updated · Verified with OpenHTF 1.6.1

TestStand sequences and OpenHTF tests describe the same thing: ordered steps with limits, run against one unit, producing a report. The vocabulary differs; the shapes line up.

Concept mapping

TestStandOpenHTFNotes
Sequence file (.seq)Python module with htf.Test(...)Text, diffable, in git
MainSequencehtf.Test(*phases)
Setup / Main / Cleanup step groupshtf.PhaseGroup(setup=, main=, teardown=)Cleanup semantics match: teardown runs if setup completed — Phase Groups
Step (Action)PhaseA Python function
Numeric Limit Test step@htf.measures(htf.Measurement(...).in_range(lo, hi).with_units(...))GELE → in_range; GT/LT only → one bound; EQ → .equals
Multiple Numeric Limit TestSeveral Measurements on one phase, or a dimensioned measurement
String Value Test.equals("...") or .matches_regex(...)
Pass/Fail TestBoolean measurement .equals(True), or PhaseResult.FAIL_AND_CONTINUE
Step result "Skipped"PhaseResult.SKIPRecorded as SKIP
Precondition expression@htf.PhaseOptions(run_if=lambda: ...) for static, BranchSequence for result-dependentrun_if cannot see results; branches can — Checkpoints & Branching
Sequence CallA phase list / PhaseSequence reused across tests; Subtest for an independent resultSubtests
Looping stepsforce_repeat + repeat_limit, or a Python loop building phases
Step failure causes sequence failureDefault OpenHTF behaviour (continue, record FAIL); stop_on_first_failure or checkpoint() to stopTest Options
Station Globals / Sequence File GlobalsConfiguration: CONF.declare, YAML per station
Locals / FileGlobals passed between stepsMeasurements (test.get_measurement), plug state, test.test_record.metadata
Process model (serial number prompt, report, loop)test_start=prompt_for_test_start(), output callbacks, while True: around execute()Device Under Test
Operator InterfaceBuilt-in Operator UI or TofuPilot's
Report (ATML / XML / HTML)JSON test record via OutputToJSON
Database LoggerAn output callback — your SQL insert, or TofuPilot's upload()
Code modules (LabVIEW VI, DLL, .NET)Plug methods in Python; call DLLs via ctypes, LabVIEW via its Python connectivity or a rewriteThe main effort
Deployment UtilityPython packaging + a launcher — Production deployment
Batch / parallel process modelsNot built in; one process per socket, or TofuPilot procedures

Worked example

A typical PCB sequence: Setup opens instruments; Main programs firmware, checks rails with numeric limits, runs an RF sub-sequence under a precondition; Cleanup powers down.

pcb01_eol.py
import openhtf as htf
from openhtf.output.callbacks import json_factory
from openhtf.plugs import user_input
from openhtf.util import checkpoints, configuration, units

from plugs import Dmm, Psu, Programmer, RfTester

CONF = configuration.CONF
RF_VARIANT = CONF.declare("rf_variant", default_value=True, description="Board has the radio fitted")


# --- Setup group: instruments open themselves in plug __init__; a phase can verify ---
@htf.plug(psu=Psu, dmm=Dmm)
def instruments_ready(test, psu, dmm):
    test.logger.info("PSU %s, DMM %s", psu.idn, dmm.idn)


# --- Main group ---
@htf.plug(prog=Programmer)
@htf.measures(htf.Measurement("fw_crc").equals("0x9A3C"))            # String Value Test
def program_firmware(test, prog):
    test.measurements.fw_crc = prog.flash("fw_2.1.4.hex")


@htf.plug(psu=Psu, dmm=Dmm)
@htf.measures(                                                          # Multiple Numeric Limit Test
    htf.Measurement("rail_3v3").in_range(3.2, 3.4).with_units(units.VOLT),
    htf.Measurement("rail_1v8").in_range(1.75, 1.85).with_units(units.VOLT),
    htf.Measurement("idle_current").in_range(maximum=0.120).with_units(units.AMPERE),
)
def check_rails(test, psu, dmm):
    psu.set(12)
    test.measurements.rail_3v3 = dmm.vdc("3V3")
    test.measurements.rail_1v8 = dmm.vdc("1V8")
    test.measurements.idle_current = psu.current()


@htf.PhaseOptions(run_if=lambda: CONF.rf_variant)                       # Precondition
@htf.plug(rf=RfTester)
@htf.measures(htf.Measurement("tx_power").in_range(17, 20).with_units(units.DECIBEL_MILLIWATTS))
def rf_tx_power(test, rf):
    test.measurements.tx_power = rf.tx_power_dbm()


# --- Cleanup group ---
@htf.plug(psu=Psu)
def power_down(test, psu):
    psu.off()


def main():
    test = htf.Test(
        htf.PhaseGroup(
            setup=[instruments_ready],
            main=[
                program_firmware,
                checkpoints.checkpoint("programmed"),    # "step failure causes sequence failure" for the expensive part
                check_rails,
                htf.Subtest("rf", rf_tx_power),          # Sequence Call with its own result
            ],
            teardown=[power_down],
        ),
        test_name="PCB01 EOL",
        sequence_version="2.1.4",                        # any FileGlobal worth keeping → metadata
    )
    test.add_output_callbacks(
        json_factory.OutputToJSON("./records/{dut_id}.{start_time_millis}.json", indent=2)
    )
    while True:                                          # process model loop
        test.execute(test_start=user_input.prompt_for_test_start())

if __name__ == "__main__":
    main()

Migrating the code modules

This is where the time goes. Options, roughly in order of preference:

  1. Rewrite in Python against the instrument's SCPI or SDK — PyVISA, pyserial, pymodbus. Usually shorter than expected: a LabVIEW driver VI often wraps a handful of SCPI strings.
  2. Call the existing DLL from a plug with ctypes or a vendor Python binding. Keeps validated code; adds a Windows dependency.
  3. Keep LabVIEW for one instrument and expose it over a local TCP socket or CLI that a plug calls. A bridge, not a destination.

Transition plan

  • Pick one station with a simple sequence and a Python-comfortable owner. Run OpenHTF alongside TestStand on the same units for a week and compare records.
  • Match limits exactly from the sequence file; keep the same measurement names so historical comparisons hold.
  • Route both systems' results to one database so quality does not lose visibility during the switch — an output callback for OpenHTF, the existing logger for TestStand. TofuPilot accepts both OpenHTF records and imported files.
  • Retire TestStand licences per station as each converts; the savings fund the plug work.

On this page

First-pass yield
0%4.1
Track with TofuPilot