A plant mid-migration runs LabVIEW and Python stations side by side for a year or more, and the way to keep that sane is one TofuPilot procedure per product with two implementations reporting into it. FPY stays comparable across the line, ownership stays clear, and stations move over one at a time instead of in a big bang. This guide covers which stations to move first, how each side reports, who owns what, and what to compare before you switch.
Decide Which Stations Stay on LabVIEW for Now
Score every station on four criteria. The scores decide the order; nothing else does.
| Criterion | Keep on LabVIEW for now | Migrate first | Migrate last |
|---|---|---|---|
| Volume | A few units a week | Every unit passes through it | Medium, steady |
| Change frequency | Limits haven't moved in two years | Limits or sequence change monthly | A few changes a year |
| Who can open it | Two or more people can open and change the VI | One person, or nobody since they left | One person, and they're staying |
| NI DAQ dependence | Tight: FPGA timing on a PXI chassis, cDAQ loops at kHz rates | None or thin: SCPI over VISA, serial, USB | A few DAQmx analog channels (the nidaqmx Python package covers those) |
Migrate first wins on any two of volume, change frequency and who can open it. Keep wins on NI DAQ dependence alone: FPGA timing on PXI is a real reason to stay, and it's the only criterion on the list that doesn't get worse with time.
A station that scores "keep" today is not kept forever. It's kept until the product it tests goes end-of-life, or until the PXI chassis needs replacing, whichever comes first.
One Procedure, Two Implementations
In TofuPilot a procedure is the test definition for a product, with one procedure_id. Both the LabVIEW station and the Python station report their runs into that one procedure. FPY, Cpk per measurement, histograms and station throughput are then filterable per station on the same page, which is the only way to compare the two honestly.
Three rules keep the comparison valid:
| Rule | Why |
|---|---|
| Same phase names, same measurement names, same units | Analytics key on the name. rail_3v3 on one station and Rail 3V3 on the other are two different measurements |
| Same limits, changed on both sides in the same week | A limit that moves on one side only shows up as a yield gap that isn't real |
procedure.yaml is the source of truth for names and limits | The Python side is text, diffs and gets reviewed. The LabVIEW side follows it |
The LabVIEW Side
Two options. Pick A when the LabVIEW engineer is staying and has time. Pick B when they've left, or when the station is on the "migrate last" list and you don't want to touch the VI.
Option A: post results to the REST API from the HTTP Client VIs
The station keeps running as it is, and adds one call at the end: POST https://www.tofupilot.app/api/v2/runs with an Authorization: Bearer <api key> header, Content-Type: application/json, and this body.
run-payload.json26 lines
{ "procedure_id": "9f1c2d3e-4b5a-6c7d-8e9f-0a1b2c3d4e5f", "serial_number": "SN-000123", "part_number": "PCBA-100", "outcome": "PASS", "started_at": "2026-09-21T08:14:02Z", "ended_at": "2026-09-21T08:15:40Z", "phases": [ { "name": "Power Rails", "outcome": "PASS", "started_at": "2026-09-21T08:14:05Z", "ended_at": "2026-09-21T08:14:09Z", "measurements": [ { "name": "rail_3v3", "outcome": "PASS", "measured_value": 3.31, "units": "V", "lower_limit": 3.2, "upper_limit": 3.4 } ] } ]}In LabVIEW that's OpenHandle, AddHeader twice, POST with the JSON string, check the status code, CloseHandle. Build the string with Flatten To JSON from a cluster shaped like the payload, and keep the API key in a config file on the station PC, not in the VI. outcome is one of PASS, FAIL, ERROR, TIMEOUT, ABORTED; the procedure_id is on the procedure page in TofuPilot.
There's no LabVIEW SDK or VIPM package for this. It's a plain HTTPS call, which is also why it doesn't break on the next LabVIEW version.
Option B: wrap the LabVIEW executable in a Python phase
Build the LabVIEW station as an executable that writes a result file, and call it from one phase. The measurements then flow through procedure.yaml like any Python station, and the LabVIEW code becomes an instrument the Python side drives.
phases/labview_fct.py20 lines
# Run the built LabVIEW station as a subprocess, read its result file,# and set the measurements so the run lands in TofuPilot like any other.import jsonimport subprocessfrom pathlib import PathRESULT = Path(r"C:\stations\fct\last_result.json")def labview_fct(measurements, unit): if RESULT.exists(): RESULT.unlink() subprocess.run( [r"C:\stations\fct\FCT.exe", "--serial", unit.serial_number], check=True, timeout=180, ) result = json.loads(RESULT.read_text()) measurements.rail_3v3 = result["rail_3v3"] measurements.idle_current = result["idle_current"]The limits stay in procedure.yaml, so the LabVIEW executable only reports values. That's the point: one place for limits, even while two runtimes exist.
The Python Side
The Python station is a folder under Git: procedure.yaml, phases/, plugs/. The names below match the LabVIEW payload exactly.
procedure.yaml25 lines
# Same names, same units, same limits as the LabVIEW station.name: PCBA-100 FCTversion: 2.3.0unit: serial_number: default_value: "SN-000001" part_number: default_value: "PCBA-100"plugs: - name: dmm python: plugs.dmm:Multimetermain: - name: Power Rails python: phases.power_rails measurements: - name: rail_3v3 unit: V validators: - operator: ">=" expected_value: 3.2 - operator: "<=" expected_value: 3.4The phase is def power_rails(measurements, dmm): measurements.rail_3v3 = dmm.read_voltage(), and the plug wraps PyVISA. The TestStand migration guide covers the sequencer side when TestStand sits on top of the LabVIEW station.
Ownership Rules
Two runtimes with fuzzy ownership is how limits drift. Write these down and put them in each station's README.
| What | Owner | Reviewer | Where it lives |
|---|---|---|---|
| Measurement names, units, limits | Test engineering lead | Quality | procedure.yaml in the Python repo, mirrored to the LabVIEW station in the same week |
| LabVIEW station code | The LabVIEW engineer | The Python engineer reviews the posted payload against a Python run, not the VI | The station's own repo, one per station, built executable tagged |
| Python station code | The Python engineer | The LabVIEW engineer reviews the phase logic (they know the fixture) | The station's own repo, one per station, tag per release |
| API keys | Test engineering lead | Nobody else | A config file on each station PC, one key per station, rotated when someone leaves |
| Which version runs where | Whoever releases | Anyone, in TofuPilot | The run carries the procedure version; the station PC carries the tag |
One repo per station, releases tagged, and the tag on the PC matches the version in the file. The hand-over guide has the README template both sides use. Both engineers will find a bug in the other's station in the first week, which is the review working as intended.
Compare Before You Switch
Run both implementations on the same units for two weeks before the LabVIEW station goes to the spare bench. Same serial numbers through both stations, in the same shift where possible. Everything below is on the procedure page in TofuPilot, filtered by station.
| Compare | Where to look | Switch when |
|---|---|---|
| Measurement means, per measurement | Measurement histogram, one station per series | The offset between stations is smaller than the gauge repeatability you already accept |
| Spread, per measurement | Histogram width, control chart | Python spread is equal or narrower; wider means a timing or settling difference in the plug |
| FPY | Procedure page, per station | Within the noise for the volume, and no new failure mode |
| Failure Pareto | Pareto, per station | Same top three failing measurements on both sides |
| Cycle time | Station throughput | Equal or better; slower usually means a fixed sleep that a settle-and-poll replaces |
| Retest behaviour | Run pages per unit | Units that failed then passed did so on both sides for the same measurement |
A mean offset that's stable and small is a plug difference (different NPLC, different range) and is fixed in the Python plug. A wider spread is a settling problem. A different Pareto means the two stations don't test the same thing, and that's a conversation with quality, not a code change.
What to Do With the LabVIEW Seats
LabVIEW has been subscription-only since 2023, roughly $1,300 per year for Base, $4,300 for Full and $5,500 and up for Professional, per seat. TestStand deployment licences run about $1,000 to $1,500 per station. The pricing guide has the detail.
The plan writes itself from the migration order:
- Stop renewing the deployment licence on each station the week it switches. Deployment licences are per station; they don't transfer to the Python one because it needs none.
- Keep one Full seat for as long as any station is on the "keep" list. Someone has to open the VI to read it during a migration, and to fix the PXI station.
- Don't count on Community Edition as the fallback. It's non-commercial only.
- Retire the last seat when the last kept station retires, not before. A LabVIEW station with no seat in the building is a station nobody can fix.
Budget the freed seats into the migration itself. One Full seat a year pays for a good chunk of the Python engineer's onboarding.
Start With One Station
Pick the station with the highest volume, or the one only one person can open. Rebuild it in Python with the TofuPilot Framework, then run it side by side with the LabVIEW version on the same units for two weeks, using the comparison table above. The LabVIEW station reports into the same procedure the whole time, so the day you switch, the FPY history doesn't restart.
# Install the CLI, run a procedure locally, then run it with upload.curl -fsSL https://www.tofupilot.app/install | shtofupilot run ./procedure.yamltofupilot run ./procedure.yaml --uploadThe TestStand migration guide covers the sequencer rebuild step by step, and the Framework page covers the layout. The Lab tier is free, and tofupilot run works with no account.
