
Get the complete source, dependencies and setup instructions from the template repository.
Open the source on GitHub ↗Introduction
Harness Acceptance Overview
A spacecraft harness is the one subsystem built entirely by hand, and every crimp, every pin insertion and every shield termination in it is a place where the drawing and the hardware can disagree. The acceptance test on a harness tester asks four questions of every net: is it connected to the pins the netlist says and nothing else, is its resistance what the wire gauge and the length predict, is it insulated from everything else at the test voltage, and does it hold that voltage without breaking down. A fifth question goes to each overall shield: is it bonded to its backshell. And a sixth goes to the record: how many times has each flight connector been mated, because this test costs one more.

One connector mated to its adapter block on the tester. The adapter is a flight-representative receptacle wired to the tester's switch matrix; its own resistance is in the continuity limit.
The tests are written down. ECSS-Q-ST-20-30C, the electrical harness assembly standard, and IPC/WHMA-A-620 with its space addendum define continuity against the netlist, insulation resistance, dielectric withstanding voltage and shield termination as the acceptance inspection of a finished harness. The voltages, the resistance floor, the leakage ceiling and the number of mating cycles a flight connector is allowed are the programme's, taken from its harness specification and the connector's qualification, and this template holds them as recipe constants. The one number the recipe computes rather than states is the continuity limit: the wire's ohms per metre times the length plus two contacts plus the adapters, so that a harness of a different length gets the limit its wire deserves rather than a round figure.
Test Purpose
The procedure records one acceptance record per harness:
- Netlist revision against the drawing revision, and each connector's mating log against the allowance, before the harness is mated to the tester
- Continuity: every net four-wire, opens and miswires counted, and the spread between nets of the same gauge and length
- Insulation: every net against every other net and the shields at 500 V DC, the worst pair per net
- Dielectric withstanding: 500 V DC for 60 s per net, leakage recorded, breakdowns counted
- Shield bonding to the backshell
- The mate written back to each connector's log in teardown

The mock harness on the tester: sixteen nets between 176 and 190 mΩ with a 14 mΩ spread, insulation no worse than 7.9 GΩ, leakage no higher than 5.5 µA, no opens, no miswires, no breakdowns, shields at 3.5 to 4.9 mΩ. A net at 260 mΩ is a crimp that did not take; a net at 30 mΩ over its neighbours is one that half took, and the spread limit is what catches it inside the absolute limit.
The framework mechanics on show are a limit derived in the recipe from physical constants, counts validated with == 0 beside a resistance ceiling, a spread aggregation that catches a fault inside the absolute limit, a consumable per connector read in setup:, gated, and written back in teardown:, a boolean derived from two revisions and validated with ==, and one curve per electrical test each with one judged aggregation.
Equipment & Setup
To accept harnesses at the integration bench, the following are required:
- A harness tester with a switch matrix, four-wire continuity, insulation and DWV at the test voltage, and an API
- Adapter blocks for the harness's connectors, flight-representative receptacles wired to the matrix
- The netlist for the drawing revision, loaded on the tester
- The Device Under Test (DUT): a harness segment, here two circular connectors, 16 nets of AWG 26 over 1.2 m, two overall shields
- A TofuPilot Framework procedure to sequence the gate, the four tests and the log
- The TofuPilot Dashboard to keep the mating log per serial as unit metadata and to see every net's resistance across the lot
Hardware Components
Harness Tester
A Cirris CH2 or a CableEye HVX does all four electrical tests from one connection: four-wire continuity per net, insulation at up to 1500 V DC between every pin pair, DWV with leakage readback and breakdown detection, and low-resistance readings for the shields. The tester holds the netlist and reports per-net results, and the procedure asks for each test in turn and validates what comes back. The four-wire continuity matters at these resistances: a two-wire reading would add the matrix and the adapter cabling, which is more than the harness.

The bench: both connectors mated to their adapters on the tester, the instrument beside it. The harness is mated once for the whole sequence, which is why the mate is counted once.
Adapters and the Mating Log
Flight connectors are qualified for a finite number of mating cycles, and each programme allocates a share of them to ground testing. The adapter blocks are what the harness mates to on the bench, and every mate goes on the log. The procedure reads the log for both connectors in setup, refuses to mate a harness whose worst connector has no allowance left, and writes the mate back in teardown whatever the outcome, so a harness that failed continuity still has its log right.
The Continuity Limit
AWG 26 copper is 0.1339 Ω/m at 20 °C. Over 1.2 m that is 161 mΩ, plus two contacts at 5 mΩ each, plus a margin for the adapters and the matrix, gives 231 mΩ. The recipe computes it from those constants so a 3 m harness of the same design gets 470 mΩ without anyone editing a limit. The spread limit of 40 mΩ between the best and worst net is the number that finds a marginal crimp on a net that still passes the absolute limit.
Where the Limits Come From
The structure of the test is ECSS-Q-ST-20-30C and IPC/WHMA-A-620. The 500 V DC insulation voltage and the 100 MΩ floor, the 500 V DC DWV for 60 s with 100 µA of leakage, the 10 mΩ shield bond and the 50-mate allowance are the programme's harness specification. The continuity limit is computed from the wire. A customer sets the voltages from the harness's working voltage and the connector's rating, and tightens the spread limit from the distribution the dashboard shows after a few dozen harnesses.
Test Procedure
Overview
The procedure maps the acceptance test onto the framework's three stages. The revision check and the mating gate live in setup: so the harness is never mated on a wrong netlist or past its allowance. The four electrical tests are chained with depends_on because they share one tester and one mate. The log write lives in teardown: so it happens whatever the outcome.
- Setup: tester, netlist revision against the drawing, mates remaining.
- Main: continuity, opens, miswires, spread.
- Main: insulation at 500 V DC.
- Main: DWV at 500 V DC for 60 s, leakage, breakdowns.
- Main: shield bonding.
- Teardown: mate written to each connector's log.
Every metric validates against limits declared in procedure.yaml, and results stream to TofuPilot for per-serial trending across the lot.
Why TofuPilot Framework?
TofuPilot Framework is a YAML + Python test framework built for hardware manufacturing. Instead of writing all your test logic, measurements, and limits inside Python code, you describe what the test does in a procedure.yaml file, and how in small Python phase files. The framework handles:
- Automatic Python environment management (via
uv) - Operator UI (no frontend code needed)
- Measurement validation and live charts
- Process isolation between phases and equipment plugs
Project Structure
You can find the full source on GitHub. The HarnessTester plug is a mock of the tester and its adapters, synthesizing a healthy 16-net harness with nets around 180 mΩ, insulation over 5 GΩ, a few microamps of leakage, shields bonded under 5 mΩ and a mating log at 12 and 7 mates, so the procedure runs end-to-end without a tester or a harness connected.
tofupilot run .For CI or bench automation, the same run executes headless:
tofupilot run . --no-tui --no-kiosk --jsonThe Procedure File
procedure.yaml declares the unit, the tester plug, and the three stages with every measurement and limit:
procedure.yaml · 147 lines
name: Harness Continuity and Hipot Testversion: 0.1.0description: Acceptance of a spacecraft harness segment on a harness tester. Netlist revision and the connectors' mating log in setup, 4-wire continuity of every net against the gauge-and-length limit with opens and miswires counted, insulation at 500 V DC pin-pair, dielectric withstanding at 500 V DC with leakage and breakdowns, shield-to-backshell bonding, and the mate written back to the log in teardown.unit: auto_identify: true serial_number: description: "Scan the harness label" placeholder: "HRN-PWR-0000" pattern: "^HRN-PWR-\\d{4}$" default_value: "HRN-PWR-0142" part_number: default_value: "HRN-PWR-16N" batch_number: default_value: "LOT-2026-09"plugs: - name: Harness Tester description: "Cirris or CableEye-class harness tester with adapters for both connectors, netlist loaded, mating log from the record (mock)" python: plugs.harness_tester:HarnessTester key: benchsetup: - name: Identify key: identify python: phases.identify measurements: - name: Tester key: tester_id validators: - {operator: matches, expected_value: "^CH2-\\d{2}$"} - name: Netlist Matches Drawing key: netlist_matches_drawing validators: - {operator: "==", expected_value: true} - name: Mates Remaining key: mates_remaining_min description: Mating cycles left on the worst connector before this test; the test costs one. validators: - {operator: ">=", expected_value: 1}main: - name: Continuity key: continuity python: phases.continuity measurements: - name: Nets key: nets title: 4-wire resistance of every net x_axis: legend: Net y_axis: - legend: Resistance key: resistance unit: mΩ aggregations: - type: max_mohm unit: mΩ validators: - {operator: "<=", expected_value: 230.7} - type: spread_mohm unit: mΩ validators: - {operator: "<=", expected_value: 40.0} - name: Opens key: opens validators: - {operator: "==", expected_value: 0} - name: Miswires key: miswires validators: - {operator: "==", expected_value: 0} - name: Insulation key: insulation python: phases.insulation depends_on: [continuity] measurements: - name: IR key: ir title: Worst insulation resistance per net at 500 V DC x_axis: legend: Net y_axis: - legend: Worst pair key: worst unit: MΩ aggregations: - type: min_mohm unit: MΩ validators: - {operator: ">=", expected_value: 100.0} - name: DWV key: dwv python: phases.dwv depends_on: [insulation] timeout: 30m measurements: - name: DWV key: dwv title: Leakage per net at 500 V DC for 60 s x_axis: legend: Net y_axis: - legend: Leakage key: leakage unit: µA aggregations: - type: max_ua unit: µA validators: - {operator: "<=", expected_value: 100.0} - name: Breakdowns key: breakdowns validators: - {operator: "==", expected_value: 0} - name: Shield Bond key: shield_bond python: phases.shield_bond depends_on: [dwv] measurements: - name: Shields key: shields title: Shield to backshell resistance x_axis: legend: Shield y_axis: - legend: Resistance key: resistance unit: mΩ aggregations: - type: max_mohm unit: mΩ validators: - {operator: "<=", expected_value: 10.0}teardown: - name: Demate key: demate python: phases.demate measurements: - name: Mates Remaining After key: mates_remaining_after validators: - {operator: ">=", expected_value: 0}Framework features to notice:
- A computed limit. The 230.7 mΩ in the procedure file is the value
CONTINUITY_MOHM_MAXtakes inutils/recipe.pyfrom the gauge, the length, the contacts and the adapters; the recipe is where it lives, the procedure file is where it is declared. - Counts with
== 0.opens,miswiresandbreakdownsare integers the tester reports; the validator is equality with zero. A count is not a resistance and is not judged as one. - A spread beside a ceiling.
nets.resistance.max_mohmbounds the worst net;spread_mohmbounds the difference between the best and the worst, which is where a half-taken crimp shows. - A consumable through the run.
mates_remaining_min >= 1in setup gates the mate;mates_remaining_after >= 0in teardown records it; both connectors' counts sit on the unit metadata before and after. - A boolean from two revisions.
netlist_matches_drawingis computed in the phase from the tester's loaded netlist and the drawing on the traveller, and validated with== true.
Identify
The setup phase reads the tester's identity, compares the netlist revision to the drawing, reads the mating log for both connectors against the allowance, and puts the revision and the counts on the unit metadata:
from utils.recipe import CONNECTORS, MATES_ALLOWEDdef identify(measurements, bench, unit, log): """Setup: tester and netlist revision against the drawing, and the mating log of each flight connector against the programme's allowance. This test costs one mate per connector; a harness that has none left is not mated again.""" ident = bench.identify() measurements.tester_id = ident["tester"] measurements.netlist_matches_drawing = ident["netlist_rev"] == ident["drawing_rev"] remaining = [MATES_ALLOWED - ident["mates"][c] for c in CONNECTORS] measurements.mates_remaining_min = int(min(remaining)) unit.metadata["netlist_rev"] = ident["netlist_rev"] for c in CONNECTORS: unit.metadata[f"mates_{c}"] = ident["mates"][c] log.info(f"Harness {unit.serial_number}: netlist rev {ident['netlist_rev']} vs drawing {ident['drawing_rev']}, mates " + ", ".join(f"{c} {ident['mates'][c]}/{MATES_ALLOWED}" for c in CONNECTORS))Continuity
Every net four-wire against the netlist; the resistances go on one curve with the maximum and the spread as aggregations, and the opens and miswires the tester reports go on two counts:
import numpy as npfrom utils.recipe import CONTINUITY_MOHM_MAXdef continuity(measurements, bench, log): """Every net 4-wire against the netlist. The resistance limit is the gauge and the length; an open or a miswire is a count that must be zero, not a resistance that is large.""" c = bench.continuity() r = np.array(c["resistance_mohm"]) measurements.nets.x_axis = c["net"] measurements.nets.y_axis.resistance = c["resistance_mohm"] measurements.nets.y_axis.resistance.aggregations.max_mohm = float(r.max()) measurements.nets.y_axis.resistance.aggregations.spread_mohm = float(r.max() - r.min()) measurements.opens = int(c["opens"]) measurements.miswires = int(c["miswires"]) log.info(f"{r.size} nets: {r.min():.1f}..{r.max():.1f} mohm (limit {CONTINUITY_MOHM_MAX:.0f}), {c['opens']} open, {c['miswires']} miswired")Insulation
Every net against every other net and the shields at 500 V DC; the tester reports the worst pair per net and the phase validates the minimum:
import numpy as npfrom utils.recipe import IR_Vdef insulation(measurements, bench, log): """Insulation resistance of every net against every other net and the shields at 500 V DC; the tester reports the worst pair per net.""" ir = bench.insulation(IR_V) worst = np.array(ir["worst_mohm"]) measurements.ir.x_axis = ir["net"] measurements.ir.y_axis.worst = ir["worst_mohm"] measurements.ir.y_axis.worst.aggregations.min_mohm = float(worst.min()) log.info(f"Insulation at {IR_V:.0f} V: worst pair {worst.min():.0f} Mohm")DWV
500 V DC for 60 s per net; the leakage goes on a curve with its maximum, and any breakdown the tester detected goes on a count:
import numpy as npfrom utils.recipe import DWV_S, DWV_Vdef dwv(measurements, bench, log): """Dielectric withstanding voltage, 500 V DC for 60 s per net, leakage recorded and any breakdown counted.""" d = bench.dwv(DWV_V, DWV_S) leak = np.array(d["leakage_ua"]) measurements.dwv.x_axis = d["net"] measurements.dwv.y_axis.leakage = d["leakage_ua"] measurements.dwv.y_axis.leakage.aggregations.max_ua = float(leak.max()) measurements.breakdowns = int(d["breakdowns"]) log.info(f"DWV {DWV_V:.0f} V DC {DWV_S:.0f} s: leakage up to {leak.max():.1f} uA, {d['breakdowns']} breakdown")Shield Bond
Each overall shield to its backshell, low-resistance:
import numpy as npdef shield_bond(measurements, bench, log): """Each overall shield to its backshell, low-resistance; a shield that is not bonded is an antenna.""" s = bench.shield_bond() r = np.array(s["resistance_mohm"]) measurements.shields.x_axis = s["shield"] measurements.shields.y_axis.resistance = s["resistance_mohm"] measurements.shields.y_axis.resistance.aggregations.max_mohm = float(r.max()) log.info(f"Shield bonds {r.min():.1f}..{r.max():.1f} mohm")Demate
The teardown writes the mate this test cost to each connector's log and records the allowance left:
from utils.recipe import CONNECTORS, MATES_ALLOWEDdef demate(measurements, bench, unit, log): """Teardown: the mate this test cost is written to the log of each connector, whatever the outcome.""" mates = bench.record_mate() for c in CONNECTORS: unit.metadata[f"mates_{c}"] = mates[c] measurements.mates_remaining_after = int(min(MATES_ALLOWED - mates[c] for c in CONNECTORS)) log.info("Demated, mates now " + ", ".join(f"{c} {mates[c]}/{MATES_ALLOWED}" for c in CONNECTORS))Mock Plug
HarnessTester stands in for the tester, its adapters and the mating log. It draws sixteen nets at the wire's resistance plus contacts, adapters and a few milliohms of scatter, insulation between 5 and 15 GΩ, leakage of 3 to 6 µA, two shields at 3 to 5 mΩ, no opens, miswires or breakdowns, and a mating log at 12 and 7 that the teardown increments. Every method returns plain Python types because plug calls cross a JSON boundary; a measurement read back from measurements.<key> returns a proxy, so the phases keep locals for their log lines.
On a real station, the class speaks the tester's API over USB or Ethernet: load the netlist for the drawing revision, run each test, read the per-net results back. Keep the mating log per connector in the dashboard as unit metadata and let every mate, this test's included, increment it. Set IR_V, DWV_V, DWV_LEAK_UA_MAX and MATES_ALLOWED to the programme's harness specification, and WIRE_GAUGE_AWG, OHM_PER_M and LENGTH_M to the harness's drawing. The phases, measurements and limits stay the same.
