End-of-line (EOL) test is the last verification a product receives before it is packed. Everything upstream tested parts of it; EOL tests the thing the customer will actually receive, fully assembled, in its enclosure, behaving as a product rather than as a board.
What EOL Covers
| Area | Typical checks |
|---|---|
| Power-on behaviour | Boots, correct current draw, no fault indications |
| Functional | Every feature the datasheet claims, exercised |
| Safety | Hipot, ground bond, leakage where applicable |
| Calibration | Final coefficients measured and written |
| Interfaces | Connectors, buttons, displays, LEDs, audio |
| Wireless | RF power, sensitivity, pairing, antenna presence |
| Sensors | Readings against a reference, in the final enclosure |
| Identity | Serial number, MAC, firmware version, labels |
| Cosmetic | Scratches, gaps, label placement |
The enclosure matters more than it looks. A board that passed FCT can fail EOL because an antenna is detuned by the case, a sensor reads differently once enclosed, or a connector does not align with its cutout. Those are precisely the defects EOL exists to find.
EOL vs FCT
| FCT | EOL | |
|---|---|---|
| Unit | Bare PCBA | Fully assembled product |
| Tests | Board-level function | Product-level function |
| Catches | Assembly and component faults | Integration, mechanical, enclosure faults |
| Access | Test points, pogo fixture | Product connectors only |
| Cycle time | Seconds to a minute | Often minutes |
| Rework | Cheap; board is accessible | Expensive; disassembly required |
The access difference drives the design. At EOL you generally cannot probe a test point, so the test talks to the product the way a customer would: over USB, the console, or the radio. This is why firmware self-test hooks matter so much at this stage.
Cycle Time Is the Constraint
EOL usually sits at the end of a line with a takt time, and a test that overruns it becomes the bottleneck for the whole factory. Two structural levers matter.
Run independent checks concurrently. Most EOL checks do not depend on each other. Declaring them as independent phases lets them run in parallel rather than in sequence.
Test several units at once. When the fixture holds more than one product, fixture slots test them concurrently, each producing its own run and outcome.
procedure.yaml92 lines
name: Product EOL Testversion: 1.0.0description: End-of-line test for the assembled productunit: serial_number: default_value: "PRD000001" part_number: default_value: "PRD-100"plugs: - name: fixture python: plugs.fixture:Fixture - name: dut python: plugs.dut:Dut - name: dmm python: plugs.dmm:Multimetermain: - name: Safety python: phases.safety measurements: - name: Enclosure Closed validators: - operator: "==" expected_value: true - name: Power On python: phases.power_on depends_on: [safety] measurements: - name: Inrush Current unit: A validators: - operator: "<=" expected_value: 1.5 - name: Boot Time unit: ms validators: - operator: "<=" expected_value: 3000 - name: Identity python: phases.identity depends_on: [power_on] measurements: - name: Firmware Version validators: - operator: "in" expected_value: ["2.1.0", "2.1.1"] - name: Serial Format validators: - operator: "matches" expected_value: "^PRD[0-9]{6}$" - name: Sensors python: phases.sensors depends_on: [power_on] measurements: - name: Temperature Reading unit: "°C" description: Against the fixture reference, in the enclosure validators: - operator: ">=" expected_value: 20.0 - operator: "<=" expected_value: 30.0 - name: Interfaces python: phases.interfaces depends_on: [power_on] measurements: - name: Button Response validators: - operator: "==" expected_value: true - name: LED Check validators: - operator: "==" expected_value: true - name: Wireless python: phases.wireless depends_on: [power_on] measurements: - name: TX Power unit: dBm validators: - operator: ">=" expected_value: 8.0 - operator: "<=" expected_value: 14.0Identity, sensors, interfaces and wireless all depend on power-on but not on each other, so the four run concurrently and the phase group costs the longest of them rather than their sum.
def power_on(measurements, fixture, dut, dmm): fixture.power_on() measurements.inrush_current = dmm.peak_current() measurements.boot_time = dut.wait_for_boot_ms()def identity(measurements, dut): measurements.firmware_version = dut.firmware_version() measurements.serial_format = dut.serial_number()def sensors(measurements, dut): measurements.temperature_reading = dut.read_temperature()Run it with tofupilot run ./procedure.yaml.
For a multi-up fixture:
execution: slots: - name: Position 1 - name: Position 2 - name: Position 3 - name: Position 4A failure in one position stops that position and leaves the others testing.
Firmware Self-Test Carries EOL
Because probe access is gone, the product has to report on itself. A structured self-test command is the highest-value EOL feature a firmware team can provide:
import jsonclass Dut: def selftest(self) -> dict: raw = self._query("selftest --json") return json.loads(raw) def _query(self, cmd: str) -> str: return '{"flash_ok": true, "imu_ok": true, "adc_counts": 2048}'def selftest(measurements, dut): result = dut.selftest() measurements.flash_ok = result["flash_ok"] measurements.imu_ok = result["imu_ok"] measurements.adc_counts = result["adc_counts"]JSON rather than prose, so a firmware wording change does not break production.
Calibration at EOL
Final calibration usually belongs here, because the sensor is in its final mechanical and thermal environment. Record both the raw measurement and the coefficient written:
def calibrate(measurements, dut, fixture): reference = fixture.reference_temperature() raw = dut.read_raw_temperature() offset = reference - raw measurements.reference_temperature = reference measurements.raw_temperature = raw measurements.temperature_offset = offset dut.write_calibration(offset)Storing the coefficient per serial means a field complaint can be checked against what was actually programmed into that unit.
Labels and Identity
EOL is where the product gets its identity, and where mismatches are cheapest to catch. Verify that what is printed matches what is programmed:
def label_check(measurements, dut, scanner): programmed = dut.serial_number() printed = scanner.read_label() measurements.label_matches = programmed == printedA unit whose label and firmware disagree is untraceable for its entire life. This check costs one scan.
What EOL Cannot Do
EOL is a short test on a finished product. It does not find defects that take time to appear, which is what burn-in and ongoing reliability testing are for. A product can pass EOL and fail at 400 hours, and no amount of EOL coverage changes that.
It also cannot diagnose at component level. An EOL failure usually means disassembly and a trip back to a board-level test, which is why upstream coverage still matters.
If You Already Run OpenHTF
eol.py19 lines
import openhtf as htffrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures( htf.Measurement("boot_time_ms") .in_range(maximum=3000) .with_units(units.MILLISECOND), htf.Measurement("firmware_version").equals("2.1.0"),)def phase_power_on(test): test.measurements.boot_time_ms = 1850 test.measurements.firmware_version = "2.1.0"test = htf.Test(phase_power_on, part_number="PRD-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan product serial: "))Install with pip install "tofupilot[openhtf]", or run it with tofupilot run.
Common Mistakes
| Mistake | Consequence | Fix |
|---|---|---|
| Sequential phases on a takt-time line | EOL becomes the bottleneck | Declare independent phases; use slots |
| Assuming FCT coverage is enough | Enclosure and integration faults ship | Test the assembled product as a product |
| Firmware self-test printing prose | Production breaks on a wording change | Structured JSON output |
| Not verifying label against firmware | Untraceable units in the field | Scan and compare |
| Calibration before final assembly | Coefficients wrong in the real environment | Calibrate at EOL |
| Treating EOL as a reliability test | Time-dependent failures still escape | Add burn-in or ORT |
