How to Migrate from LabVIEW TestExec to Python with TofuPilot
LabVIEW TestExec served its purpose. But the licensing costs keep climbing, finding LabVIEW developers is getting harder, and your CI/CD pipeline doesn't speak G code. Python gives you the language, OpenHTF gives you the test framework, and TofuPilot gives you the data platform.
Here's how to make the switch without losing your test coverage.
Why Teams Migrate
| Pain Point | LabVIEW TestExec | Python + TofuPilot |
|---|---|---|
| Licensing | Per-seat, annual renewal | Free and open source |
| Developer pool | Shrinking, specialized | Large, growing |
| Version control | Binary VIs, merge conflicts | Text files, standard Git |
| CI/CD integration | Custom adapters needed | Native pytest/OpenHTF support |
| Instrument drivers | NI ecosystem only | PyVISA, any SCPI instrument |
| Data storage | Local TDM/TDMS files | Cloud database with API |
Prerequisites
- Python 3.8+ installed
pip install openhtf tofupilot pyvisa- Access to your existing LabVIEW test sequences and specs
- Instrument documentation (SCPI command references)
Step 1: Map Your TestExec Architecture
LabVIEW TestExec and OpenHTF share similar concepts with different names:
| LabVIEW TestExec | OpenHTF + TofuPilot | Notes |
|---|---|---|
| Test Sequence | htf.Test() | Top-level test container |
| Test Step | Phase function | Decorated Python function |
| Limit | htf.Measurement validator | .in_range(), .at_most(), etc. |
| Step Result | Measurement value | Stored with pass/fail status |
| Sequence File (.seq) | Python script (.py) | Version-controlled text |
| Operator Interface | TofuPilot station UI | Web-based, no LabVIEW runtime |
| Report | TofuPilot dashboard | Automatic, real-time |
| Station Global | Module-level config or env var | Standard Python patterns |
Step 2: Convert Test Steps to Python Phases
A typical LabVIEW TestExec step that reads a voltage becomes a Python phase:
Before (LabVIEW TestExec pseudo-code):
Step: "Read 5V Rail"
Type: Numeric Limit Test
Instrument: DMM_1 (NI PXI-4072)
Command: Measure DC Voltage
Low Limit: 4.95
High Limit: 5.05
Units: V
After (Python with OpenHTF):
import openhtf as htf
from openhtf.util import units
import pyvisa
@htf.measures(
htf.Measurement("voltage_5v_rail")
.with_units(units.VOLT)
.in_range(4.95, 5.05)
.doc("5V rail output voltage"),
)
def read_5v_rail(test, dmm):
"""Read and validate 5V rail voltage."""
voltage = float(dmm.query("MEAS:VOLT:DC?"))
test.measurements.voltage_5v_rail = voltageThe pattern is always the same:
- Define measurements with limits (replaces the Limit columns)
- Write instrument control in the phase body (replaces the VI)
- Assign measured values (replaces the step result)
Step 3: Handle Instrument Connections
LabVIEW TestExec uses NI's instrument driver architecture. Python uses PyVISA, which works with NI, Keysight, Rigol, and any SCPI-compatible instrument.
import pyvisa
def connect_instruments():
"""Replace TestExec station globals with PyVISA connections."""
rm = pyvisa.ResourceManager()
instruments = {
"dmm": rm.open_resource("TCPIP::192.168.1.10::INSTR"),
"psu": rm.open_resource("TCPIP::192.168.1.11::INSTR"),
"scope": rm.open_resource("USB0::0x0957::0x1796::INSTR"),
}
# Configure instruments
instruments["dmm"].timeout = 5000
instruments["psu"].write("*RST")
return instrumentsIf you're using NI PXI instruments, the NI-VISA backend works with PyVISA. Your GPIB, USB, and TCP/IP instruments work without any NI software.
Step 4: Convert Sequence Flow Control
LabVIEW TestExec has built-in flow control (preconditions, post-actions, branching). In OpenHTF, you use standard Python:
Conditional execution:
import openhtf as htf
@htf.measures(
htf.Measurement("board_variant")
.with_allowed_values("A", "B", "C"),
)
def detect_variant(test):
"""Read board variant from EEPROM or resistor divider."""
variant = read_board_variant()
test.measurements.board_variant = variant
test.state["variant"] = variant
@htf.measures(
htf.Measurement("bluetooth_rssi")
.with_units(units.DECIBEL)
.at_least(-70),
)
def bluetooth_test(test):
"""Only runs on variant B (Bluetooth-equipped boards)."""
if test.state.get("variant") != "B":
return # Skip for non-BT variants
rssi = scan_bluetooth()
test.measurements.bluetooth_rssi = rssiSetup and teardown:
import openhtf as htf
@htf.PhaseOptions(timeout_s=10)
def setup_fixture(test, fixture_controller):
"""Replaces TestExec Setup step group."""
fixture_controller.clamp()
fixture_controller.connect_probes()
@htf.PhaseOptions(run_if=lambda: True) # Always runs
def teardown_fixture(test, fixture_controller):
"""Replaces TestExec Cleanup step group. Runs even on failure."""
fixture_controller.release()
fixture_controller.disconnect_probes()Step 5: Set Up Data Collection
LabVIEW TestExec stores results in local files (TDM, TDMS, XML). TofuPilot stores everything in the cloud with full traceability.
import openhtf as htf
from tofupilot import TofuPilotClient
def main():
instruments = connect_instruments()
test = htf.Test(
setup_fixture,
detect_variant,
read_5v_rail,
bluetooth_test,
teardown_fixture,
phase_kwargs={
"dmm": instruments["dmm"],
"psu": instruments["psu"],
},
)
test.add_output_callbacks(
TofuPilotClient().as_openhtf_callback(
procedure_id="pcba-fct-v3",
procedure_name="PCBA FCT (migrated from TestExec)",
)
)
test.execute(test_start=htf.PhaseDescriptor.wrap(
lambda test: setattr(test, "dut_id", input("Scan serial: "))
))
if __name__ == "__main__":
main()Migration Strategy
Don't rewrite everything at once. Migrate station by station:
- Pick one test station with the simplest sequence
- Document the existing test spec (steps, limits, instruments, flow)
- Write the Python equivalent following the patterns above
- Run both systems in parallel for one production batch
- Compare results to verify equivalence
- Cut over when results match
- Move to the next station
Keep your LabVIEW TestExec sequences as reference documentation. The Python scripts replace them, but having the original spec helps during validation.
Common Gotchas
| Issue | Solution |
|---|---|
| NI drivers need LabVIEW runtime | PyVISA with NI-VISA backend works without LabVIEW |
| TestExec parallel step groups | Use Python threading or asyncio |
| TestExec callbacks (on fail, on pass) | OpenHTF phase options and output callbacks |
| Station model/serial tracking | TofuPilot station configuration properties |
| TestExec report generation | TofuPilot generates reports automatically |
| Operator interface buttons | TofuPilot's web-based station UI or custom Tkinter |
The hardest part isn't the code. It's validating that the new tests catch the same failures as the old ones. Run both systems in parallel until you're confident.