Test Types & Methods

Track Repair and Rework Data

Learn how to structure OpenHTF tests so repair loops, rework actions, and retests all link to the same serial number in TofuPilot.

JJulien Buteau
intermediate8 min readMarch 14, 2026

Production testing catches defects, but the real value comes from closing the loop: diagnosing failures, repairing units, and retesting them. Every retest links to the original serial number, so you get a complete history of each unit's journey through your repair process.

The Repair Loop

A typical repair workflow follows this cycle: a unit fails a test, a technician diagnoses the root cause, performs a repair, and the unit goes back through testing. Without structured data, this history gets lost in spreadsheets or paper logs.

The fix is to key everything to the serial number. Every test run against the same DUT automatically appears in its unit history. You don't need special configuration. Just use the same serial number when retesting.

Structure Your Initial Test

Start with a standard OpenHTF test that measures your DUT and uploads results.

functional_test.py
30 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("output_voltage")    .in_range(minimum=4.8, maximum=5.2)    .with_units(units.VOLT),    htf.Measurement("current_draw")    .in_range(minimum=0.095, maximum=0.105)    .with_units(units.AMPERE),)def functional_check(test):    test.measurements.output_voltage = 5.05    test.measurements.current_draw = 0.1012def main():    test = htf.Test(        functional_check,        procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",  # procedure UUID from the dashboard        part_number="PCBA-100",    )    test.add_output_callbacks(upload())    test.execute(lambda: "SN-20260312-001")if __name__ == "__main__":    main()

When this test fails, the unit enters your repair queue.

Record Repair Actions as Measurements

After a technician diagnoses and repairs the unit, capture that context in the retest. A dedicated phase records what was found and what was done.

retest_after_repair.py
44 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("repair_code"),    htf.Measurement("failure_category"),    htf.Measurement("repair_action"),)def record_repair_info(test):    # These values come from your repair technician's input    test.measurements.repair_code = "RC-042"    test.measurements.failure_category = "solder_bridge"    test.measurements.repair_action = "reworked_U3_solder_joints"@htf.measures(    htf.Measurement("output_voltage")    .in_range(minimum=4.8, maximum=5.2)    .with_units(units.VOLT),    htf.Measurement("current_draw")    .in_range(minimum=0.095, maximum=0.105)    .with_units(units.AMPERE),)def functional_recheck(test):    test.measurements.output_voltage = 5.01    test.measurements.current_draw = 0.1003def main():    test = htf.Test(        record_repair_info,        functional_recheck,        procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",        part_number="PCBA-100",    )    test.add_output_callbacks(upload())    # Same serial number links this retest to the original failure    test.execute(lambda: "SN-20260312-001")if __name__ == "__main__":    main()

The record_repair_info phase stores the diagnosis and repair action alongside the retest measurements, tied to the same unit.

In practice the technician types these values rather than hardcoding them. Prompt for them with openhtf.plugs.user_input so the operator picks from your category list instead of typing free text, which is what keeps the data aggregatable later.

Use Failure Categories Consistently

Define a standard set of failure categories and repair codes across your team. Consistent naming lets you filter and aggregate repair data later.

Common failure categories for PCBA testing:

CategoryDescription
solder_bridgeUnintended solder connection between pads
cold_jointInsufficient solder wetting
missing_componentComponent not placed during assembly
wrong_valueIncorrect component value populated
damaged_componentComponent damaged during handling or reflow
pcb_defectBoard-level issue (trace crack, via failure)

Store these as string measurements so they stay searchable.

A short list beats a thorough one. Categories nobody can tell apart get filled in at random, and the Pareto chart built from them is then worse than no chart at all.

Track Multiple Repair Cycles

Some units need more than one repair attempt. Each retest creates a new run against the same serial number, and the unit history shows the full chain: initial fail, first repair attempt, second repair attempt, and final pass.

second_repair_retest.py
44 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("repair_code"),    htf.Measurement("failure_category"),    htf.Measurement("repair_action"),    htf.Measurement("repair_cycle"),)def record_repair_info(test):    test.measurements.repair_code = "RC-043"    test.measurements.failure_category = "cold_joint"    test.measurements.repair_action = "reflowed_C12_pads"    test.measurements.repair_cycle = 2@htf.measures(    htf.Measurement("output_voltage")    .in_range(minimum=4.8, maximum=5.2)    .with_units(units.VOLT),    htf.Measurement("current_draw")    .in_range(minimum=0.095, maximum=0.105)    .with_units(units.AMPERE),)def functional_recheck(test):    test.measurements.output_voltage = 4.98    test.measurements.current_draw = 0.0998def main():    test = htf.Test(        record_repair_info,        functional_recheck,        procedure_id="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx",        part_number="PCBA-100",    )    test.add_output_callbacks(upload())    test.execute(lambda: "SN-20260312-001")if __name__ == "__main__":    main()

Adding a repair_cycle measurement makes it easy to count how many attempts each unit needed. Set a threshold where a unit stops being repaired and gets scrapped: a board on its fourth rework has usually absorbed more technician time than it is worth, and repeated reflow damages the laminate.

Once repair data flows in, you can answer the questions that matter without writing analysis scripts.

Unit history shows every test run for a serial number in chronological order. You can see exactly when a unit failed, what was repaired, and whether the retest passed.

Failure Pareto charts rank your failure categories by frequency. If solder_bridge dominates, that's a signal to investigate your reflow profile or stencil design.

First pass yield trends reflect your repair effectiveness over time. A rising FPY after process changes confirms the fix is working. A unit that keeps failing the same test after multiple repairs may point to a deeper design issue.

Watch the gap between first pass yield and final yield. A widening gap means the process is degrading while rework absorbs it, which looks fine on a final-yield chart right up until the rework capacity runs out.

Separate Procedures for Initial Test and Retest

For traceability, consider distinct procedures for initial tests and retests. Runs group by procedure, so this separation makes reporting cleaner.

Each procedure has its own UUID from the dashboard, so pointing a script at the retest procedure is a matter of changing procedure_id:

retest_procedure.py
38 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import uploadRETEST_PROCEDURE_ID = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"  # "PCBA Functional Test - Retest"@htf.measures(    htf.Measurement("repair_code"),    htf.Measurement("repair_action"),)def record_repair_info(test):    test.measurements.repair_code = "RC-042"    test.measurements.repair_action = "reworked_U3_solder_joints"@htf.measures(    htf.Measurement("output_voltage")    .in_range(minimum=4.8, maximum=5.2)    .with_units(units.VOLT),)def functional_recheck(test):    test.measurements.output_voltage = 5.02def main():    test = htf.Test(        record_repair_info,        functional_recheck,        procedure_id=RETEST_PROCEDURE_ID,        part_number="PCBA-100",    )    test.add_output_callbacks(upload())    test.execute(lambda: "SN-20260312-001")if __name__ == "__main__":    main()

This way you can compare yield between initial tests and retests independently, while the unit history still ties everything together under one serial number.

The tradeoff: splitting procedures means first pass yield on the initial procedure no longer counts the units that eventually passed on retest. That is usually what you want, but say which number you mean when reporting it.

More Guides

Put this guide into practice