# Migrate from NI TestStand to OpenHTF
URL: /guides/migrate-from-teststand
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.
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.
TestStand
OpenHTF
Notes
Sequence file (`.seq`)
Python module with `htf.Test(...)`
Text, diffable, in git
MainSequence
`htf.Test(*phases)`
Setup / Main / Cleanup step groups
`htf.PhaseGroup(setup=, main=, teardown=)`
Cleanup semantics match: teardown runs if setup completed — Phase Groups
Step (Action)
Phase
A 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 Test
Several `Measurement`s on one phase, or a dimensioned measurement
String Value Test
`.equals("...")` or `.matches_regex(...)`
Pass/Fail Test
Boolean measurement `.equals(True)`, or `PhaseResult.FAIL_AND_CONTINUE`
Step result "Skipped"
`PhaseResult.SKIP`
Recorded as SKIP
Precondition expression
`@htf.PhaseOptions(run_if=lambda: ...)` for static, `BranchSequence` for result-dependent
`run_if` cannot see results; branches can — Checkpoints & Branching
Sequence Call
A phase list / `PhaseSequence` reused across tests; `Subtest` for an independent result
Subtests
Looping steps
`force_repeat` + `repeat_limit`, or a Python loop building phases
Step failure causes sequence failure
Default OpenHTF behaviour (continue, record FAIL); `stop_on_first_failure` or `checkpoint()` to stop
Test Options
Station Globals / Sequence File Globals
Configuration: `CONF.declare`, YAML per station
Locals / FileGlobals passed between steps
Measurements (`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 Interface
Built-in Operator UI or TofuPilot's
Report (ATML / XML / HTML)
JSON test record via `OutputToJSON`
Database Logger
An 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 rewrite
The main effort
Deployment Utility
Python packaging + a launcher — Production deployment
Batch / parallel process models
Not built in; one process per socket, or TofuPilot procedures
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.
This is where the time goes. Options, roughly in order of preference:
**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.
**Call the existing DLL** from a plug with `ctypes` or a vendor Python binding. Keeps validated code; adds a Windows dependency.
**Keep LabVIEW for one instrument** and expose it over a local TCP socket or CLI that a plug calls. A bridge, not a destination.
**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.