Skip to content
Test Station Setup

Operator Interface with OpenHTF

Build the operator interface for an OpenHTF test on a TofuPilot station: text prompts, confirm buttons, text input, and live images.

CCharlotte Evequoz
intermediate5 min readJune 17, 2026

Operator Interface with OpenHTF

OpenHTF can pause a test to interact with the operator through its user_input plug. When the test runs on a TofuPilot station, these prompts render in the station kiosk: an optional image, your message, and either a confirm button or a text field. This guide covers that full surface with examples, and ends with how it looks on the station.

What the Operator Sees

A prompt appears on one screen with:

  • An optional image, above the message
  • The message text
  • A Continue button, or a text field, depending on the call

The operator never opens a terminal or a separate viewer.

Prerequisites

  • Python 3.10+
  • OpenHTF installed (pip install openhtf)

The UserInput Plug

A single call drives the prompt. Attach the UserInput plug to a phase and call prompt().

signature.py
from openhtf.plugs import user_inputprompts.prompt(    message,            # text shown to the operator    text_input=False,   # False = confirm button, True = text field    timeout_s=None,     # optional, raises PromptUnansweredError on timeout    image_url=None,     # optional image shown inline in the prompt)

It returns the operator's text (an empty string when text_input=False).

Message and Continue Button

power_cycle.py
import openhtf as htffrom openhtf.plugs import user_input@htf.plug(prompts=user_input.UserInput)def power_cycle(test, prompts):    prompts.prompt("Power-cycle the unit, wait for the LED, then click Continue.")

Text Input

scan_serial.py
@htf.plug(prompts=user_input.UserInput)def scan_serial(test, prompts):    serial = prompts.prompt("Scan the serial number:", text_input=True)    test.dut_id = serial

Yes / No Decisions

OpenHTF has no two-button Yes/No widget. The pattern is always the same: ask for a typed answer, then branch on it, usually returning PhaseResult.CONTINUE to go on or PhaseResult.STOP to halt the test. The example below gates the test on an LED check.

led_check.py
@htf.plug(prompts=user_input.UserInput)def led_check(test, prompts):    answer = prompts.prompt("Does the LED blink green? Type y or n:", text_input=True)    return htf.PhaseResult.CONTINUE if answer.strip().lower() == "y" \        else htf.PhaseResult.STOP

The same shape fits many operator decisions, for example: confirming a connector is fully seated before applying power, judging a visual pass or fail on a finish or a label, or deciding whether a unit goes to rework after an inspection.

Show an Image in the Prompt

Pass image_url and the image renders inline, above the message, live during the run. It works with a confirm button and with a text field. The URL is anything an HTML image tag accepts.

Hosted image

connector_check.py
@htf.plug(prompts=user_input.UserInput)def connector_check(test, prompts):    prompts.prompt(        "Connect the cable as shown, then click Continue.",        image_url="http://localhost:8080/reference/connector.png",    )

Local image as a data URI (no server needed)

On a station this is usually the simplest: read a local file and inline it as a base64 data URI, so there is nothing to host.

visual_inspection.py
import base64def data_uri(path, mime="image/png"):    with open(path, "rb") as f:        return f"data:{mime};base64," + base64.b64encode(f.read()).decode("ascii")@htf.plug(prompts=user_input.UserInput)def visual_inspection(test, prompts):    reference = data_uri("/opt/station/reference/board_top.jpg", "image/jpeg")    answer = prompts.prompt(        "Does the board match the reference? Type y or n:",        text_input=True,        image_url=reference,    )    return htf.PhaseResult.CONTINUE if answer.strip().lower() == "y" \        else htf.PhaseResult.STOP

Image captured during the test

confirm_capture.py
@htf.plug(prompts=user_input.UserInput)def confirm_capture(test, prompts):    path = "/tmp/capture.png"    camera.grab_frame(path)  # your capture code    prompts.prompt(        "Is the captured image in focus and centered?",        text_input=True,        image_url=data_uri(path, "image/png"),    )

Notes: use image/png or image/jpeg; the URL must load from the browser running the kiosk, so a data URI is the safest on a station. image_url shows the image live; to also keep it in the run record, attach it with test.attach_from_file(path).

Live Status Text

OpenHTF has no self-updating console line for the operator. To surface progress during a phase, use test.logger: each call emits a log record live, streamed to whatever operator UI is showing the run (OpenHTF's own web GUI, or the TofuPilot kiosk) and kept in the test record. Use it for progress, then a short prompt once the step is done.

discharge.py
@htf.plug(prompts=user_input.UserInput)def discharge(test, prompts):    test.logger.info("Waiting for capacitor to discharge...")  # live log entry    wait_for_discharge()  # your code    prompts.prompt("Capacitor discharged. Click Continue.")

What the Operator Sees in TofuPilot

These prompts render in the TofuPilot station kiosk, the operator UI the CLI serves locally and opens in a browser. Enable the kiosk on the station, or force it for a single run with tofupilot run --kiosk. When a phase calls prompt(), the kiosk shows the image, the message, and the Continue button or text field on one screen; the operator responds and the run continues. To watch from the dashboard, open the station's operator view at <your-tofupilot-url>/<org>/operator/<station-id>.

If you need richer inputs than these OpenHTF prompts offer, such as dropdowns, checklists, sliders, switches, or a live progress bar, you can add them with a TofuPilot framework procedure that declares UI components.

More Guides

Put this guide into practice