How to Build a Test Operator UI in Python
Most test engineers start with terminal output. That works during development, but production operators need a dedicated screen with pass/fail indicators, prompts, and input fields. This guide shows how to build an operator UI using OpenHTF and TofuPilot without writing any frontend code.
What the Operator Sees
The finished operator interface runs in a browser and shows:
- Serial number entry (barcode scanner or manual input)
- Live phase progress as the test runs
- Operator prompts with input fields (text, numbers, dropdowns, image-based choices)
- Pass/fail result with color-coded display
- Measurement values with limit status
The operator never sees a terminal, a file browser, or Python code.
Prerequisites
- Python 3.10+
- OpenHTF installed (
pip install openhtf) - TofuPilot Python SDK 1.11.0+ installed (
pip install tofupilot)
Step 1: Create Test Phases with Prompts
Use OpenHTF's user_input plug to add operator interactions. Each prompt pauses the test until the operator responds.
import openhtf as htf
from openhtf.plugs import user_input
from openhtf.util import units
@htf.plug(prompts=user_input.UserInput)
def phase_load_dut(test, prompts):
"""Wait for the operator to load the DUT into the fixture."""
prompts.prompt(
"Place the board in the test fixture and close the clamp. "
"Press Enter when ready."
)
@htf.plug(prompts=user_input.UserInput)
@htf.measures(
htf.Measurement("label_present").with_args(docstring="Label inspection result"),
)
def phase_visual_check(test, prompts):
"""Ask the operator to verify the label is present and correct."""
result = prompts.prompt(
"Is the product label present and correctly aligned?",
text_input=True,
)
test.measurements.label_present = resultStep 2: Add Automated Measurements
Mix operator prompts with automated instrument measurements. The operator sees both types of phases in the same interface.
@htf.measures(
htf.Measurement("supply_voltage_V")
.in_range(minimum=4.9, maximum=5.1)
.with_units(units.VOLT),
htf.Measurement("boot_time_ms")
.in_range(maximum=2000)
.with_units(units.MILLISECOND),
)
def phase_power_and_boot(test):
"""Automated: measure supply voltage and boot time."""
test.measurements.supply_voltage_V = 5.02
test.measurements.boot_time_ms = 1340
@htf.measures(
htf.Measurement("wifi_rssi_dBm")
.in_range(minimum=-70, maximum=-20)
.with_units(units.DBM),
)
def phase_wireless_check(test):
"""Automated: verify WiFi signal strength."""
test.measurements.wifi_rssi_dBm = -42Step 3: Add a Final Operator Step
After automated tests complete, ask the operator to unload the DUT and apply a pass/fail sticker.
@htf.plug(prompts=user_input.UserInput)
def phase_unload(test, prompts):
"""Instruct the operator to remove the DUT and apply the label."""
prompts.prompt(
"Remove the board from the fixture. "
"Apply a PASS sticker if the test passed. "
"Press Enter when done."
)Step 4: Connect to TofuPilot and Run
Wire all phases together and connect to TofuPilot. The operator UI streams automatically to a browser URL.
from tofupilot.openhtf import TofuPilot
test = htf.Test(
phase_load_dut,
phase_visual_check,
phase_power_and_boot,
phase_wireless_check,
phase_unload,
)
with TofuPilot(test):
test.execute(test_start=lambda: input("Scan serial: "))When you run this script, TofuPilot prints a URL in the console. Open it in a browser on the operator's station. The operator sees each phase in sequence, responds to prompts, and gets a clear pass/fail result.
Step 5: Configure the Station for Production
| Setting | How |
|---|---|
| Kiosk mode | Launch Chrome with --kiosk --app=<streaming-url> |
| Auto-start | Add the command to the OS startup script |
| Barcode scanner | Configure as USB HID keyboard (scan goes into the active input field) |
| Large text | Set browser zoom to 150% |
| Touchscreen | TofuPilot prompts work with touch input, no mouse needed |
| Multiple stations | Each station gets its own streaming URL |
Input Types Available
TofuPilot's operator UI supports structured input types beyond simple text:
| Input Type | Use Case |
|---|---|
| Text | Serial numbers, operator notes |
| Number | Manual measurements, counts |
| Slider | Analog adjustments, subjective ratings |
| Radio buttons | Choose one option from a list |
| Checkbox | Confirm multiple items (checklist) |
| Dropdown | Select from a long list of options |
| Image choice | Pick from photos (component orientation, defect type) |
| Toggle | Yes/no decisions |
These inputs render automatically in the browser. The operator fills them in, and the values flow back to the test script as measurement data.
Common Patterns
Fail-Fast with Operator Notification
from openhtf import PhaseResult
@htf.measures(
htf.Measurement("power_good").equals("PASS"),
)
def phase_power_check(test):
"""Stop early if power-up fails. No point testing a dead board."""
result = "PASS"
test.measurements.power_good = result
if result != "PASS":
return PhaseResult.STOPWhen this phase fails, the operator sees the failure immediately in the browser with the failing measurement highlighted. They don't wait for the remaining phases to time out.
Conditional Operator Steps
@htf.plug(prompts=user_input.UserInput)
@htf.measures(
htf.Measurement("rework_action").with_args(docstring="Rework performed"),
)
def phase_rework_prompt(test, prompts):
"""If a previous phase flagged an issue, ask operator to rework."""
action = prompts.prompt(
"The solder inspection flagged pad U3. "
"Rework the joint and press Enter when done.",
text_input=True,
)
test.measurements.rework_action = action