Test Station Setup

What Is an Operator Interface

An operator interface is the screen a technician uses to run a test. What it must show, what it must prevent, and how to build one without frontend work.

JJulien Buteau
beginner9 min readMarch 14, 2026

The operator interface is the screen a technician looks at all shift. It decides how fast the line runs, how many retests happen for no reason, and whether a real failure gets noticed or waved through. It is usually the last part of a test system to get attention and the first part to cause trouble.

What It Has to Do

RequirementWhy
Identify the unitScan a barcode; typing serial numbers produces typos and untraceable results
Show progressA technician needs to know whether a 90-second test is running or hung
Show the verdict unambiguouslyPass and fail must be distinguishable across a noisy room at a glance
Show what failed"FAIL" alone sends the board to a diagnostic bench that did not need to be involved
Collect operator inputVisual checks, torque values, cable connections, lot codes
Prevent unsafe actionsNo energising with the fixture open
Work offlineThe line does not stop because the network did

The fourth row is the one most often missed. A screen that shows only a red bar trains operators to retest rather than investigate, and a retest that passes hides a marginal board.

What Usually Gets Built Instead

Most teams start with a terminal running a Python script. It works for a prototype and fails on a line for predictable reasons: a scrolling console gives no glanceable state, operators learn to press Enter through prompts they stop reading, and there is no way to stop a run cleanly.

The next step is usually a small desktop app, and that is where the cost appears. A production-grade operator UI needs live state from the running test, safe interruption, input validation, and a build for every station. That is a frontend project with its own release cycle, competing for time with the tests themselves.

Declaring the Interface Instead

The alternative is to declare what the operator sees as part of the procedure, so there is no frontend to build or deploy. Each phase can carry a ui.components list, and each component has a key, a type and a label.

procedure.yaml
55 lines
name: PCBA Functional Testversion: 1.0.0unit:  serial_number:    default_value: "PCBA000001"  part_number:    default_value: "PCBA-100"plugs:  - name: fixture    python: plugs.fixture:Fixture  - name: dmm    python: plugs.dmm:Multimetermain:  - name: Fixture Setup    python: phases.fixture_setup    ui:      components:        - key: instructions          type: text          label: "Insert the board, close the lid, then continue."        - key: preflight          type: checklist          label: "Confirm before starting"          required: true          options:            - label: "Board seated flat"              value: "seated"            - label: "Lid latched"              value: "latched"            - label: "No debris on pogo pins"              value: "clean"  - name: Safety    python: phases.safety    depends_on: [fixture_setup]    measurements:      - name: Lid Closed        validators:          - operator: "=="            expected_value: true  - name: Power Rails    python: phases.power_rails    depends_on: [safety]    measurements:      - name: Rail 3V3        unit: V        validators:          - operator: ">="            expected_value: 3.2          - operator: "<="            expected_value: 3.4
phases/fixture_setup.py
def fixture_setup():    """The checklist blocks until the operator completes it."""    pass
phases/safety.py
def safety(measurements, fixture):    measurements.lid_closed = fixture.lid_closed()

Run it with tofupilot run ./procedure.yaml. The CLI renders the interface, and the same declaration works on a bench laptop and on a station in kiosk mode.

The Components Worth Knowing

ComponentUse
textInstructions, warnings, live values
text_input / textareaNotes, lot codes, free text
number_inputTorque values, manual readings
checklistMulti-step confirmations before a risky action
switch / radio / select / multiselectChoices, variants, pass/fail judgements
imageShow the operator what a good joint looks like
sliderBounded manual adjustment
progressLong-running phases

An image next to a visual inspection prompt is worth more than any amount of written instruction, and it costs a few lines in the procedure.

Operator Input Is Data, Not Just Flow Control

The mistake that quietly destroys traceability is treating a prompt as a gate and discarding the answer. If an operator records a torque value, that value belongs in the record as a measurement with limits.

The bind field does exactly that: it routes the component's value straight into a measurement, with no phase code in between.

procedure.yaml
- name: Torque Check    python: phases.torque_check    ui:      components:        - key: torque          type: number_input          label: "Screw torque (Nm)"          required: true          bind: measurements.screw_torque    measurements:      - name: Screw Torque        unit: Nm        validators:          - operator: ">="            expected_value: 0.45          - operator: "<="            expected_value: 0.55
phases/torque_check.py
def torque_check(measurements):    """The bound UI value populates screw_torque; nothing to do here."""    pass

Now a torque that drifts across a shift is visible on a control chart, rather than being a number somebody typed and nobody kept. If it falls outside the limits, the phase fails like any other measurement.

bind also routes to unit fields and run attribution:

procedure.yaml
- name: Scan Unit    python: phases.scan_unit    ui:      components:        - key: serial_number          type: text_input          label: "Serial Number"          bind: unit.serial_number        - key: operator_badge          type: text_input          label: "Operator badge"          bind: run.operated_by

A badge scanner filling run.operated_by is how competence records get captured without anyone maintaining a separate log.

Running Without an Operator

The same procedure has to run unattended in CI or on a soak rack. Rather than maintaining a second copy, resolve the prompts externally with a JSON file keyed by phase and component:

ci/run.sh
tofupilot run ./procedure.yaml --json --ui-values ci/ui.json --ui-timeout 300
ci/ui.json
{  "fixture_setup": { "preflight": ["seated", "latched", "clean"] },  "torque_check": { "torque": 0.5 }}

--ui-timeout matters here. Without it, an unanswerable prompt blocks forever; with it, the phase fails and the job moves on.

Kiosk Mode on a Station

On a production station, the interface runs full-screen with no browser chrome, so operators cannot navigate away or close it. Deployments update it on a Git push, and runs queue locally when the network drops. See how to set up kiosk mode for test stations.

If You Already Run OpenHTF

OpenHTF has a built-in Station Server and user input plugs, and existing suites run natively on the TofuPilot CLI, which provides the operator UI:

operator_test.py
import openhtf as htffrom openhtf.plugs.user_input import UserInputfrom openhtf.util import unitsfrom tofupilot.openhtf import upload@htf.measures(    htf.Measurement("rail_3v3").in_range(3.2, 3.4).with_units(units.VOLT),)@htf.plug(prompts=UserInput)def test_power(test, prompts):    prompts.prompt("Close the fixture lid, then press OK.")    test.measurements.rail_3v3 = 3.31test = htf.Test(test_power, part_number="PCBA-100")test.add_output_callbacks(upload())test.execute(test_start=lambda: input("Scan serial: "))

Install with pip install "tofupilot[openhtf]", or run it with tofupilot run for the richer interface. See operator interface with OpenHTF.

Common Mistakes

MistakeConsequenceFix
Showing FAIL with no detailBoards go to diagnostics that did not need toShow which measurement failed and its value
Typed serial numbersTypos, untraceable resultsbind: unit.serial_number with a scanner
Discarding operator inputManual readings vanish from the recordbind: measurements.<key> with limits
Prompts an operator can click through blindConfirmations stop meaning anythingUse checklists for genuinely risky steps
A separate unattended copy of the procedureThe two drift apartOne procedure, --ui-values for CI
No timeout on promptsAn unattended run hangs forever--ui-timeout
Building a custom desktop app firstA frontend project competing with test workDeclare the UI in the procedure

More Guides

Put this guide into practice