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.py30 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.py44 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:
| Category | Description |
|---|---|
solder_bridge | Unintended solder connection between pads |
cold_joint | Insufficient solder wetting |
missing_component | Component not placed during assembly |
wrong_value | Incorrect component value populated |
damaged_component | Component damaged during handling or reflow |
pcb_defect | Board-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.py44 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.
Analyze Repair Trends
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.py38 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.
