UI adapters and worker interface

Measurement classes are independent of a terminal or Qt presentation. A MeasurementUIAdapter carries operator prompts, logging, and manual interrupts across that boundary. Long-running measurement and instrument operations are a separate concern and execute in a worker.

Responsibility boundaries

UI and worker responsibilities

Component

Owns

Must not own

Measurement class

Workflow, autosave, resume, EUT assessment, device lifecycle, safety

Widget state and Qt event processing

UI adapter

Prompts, log transport, operator events, interrupt requests

Measurement decisions and direct device control

Worker

Blocking script or device call, cooperative cancellation, final state

Direct widget modification

GUI thread

Widgets, dialogs, plots, and event loop

Blocking measurement or communication calls

Adapter methods

ask(msg, buttons, level, data) -> int

Present a message and return the zero-based index of the selected button. buttons may be empty for an informational message. level and data are presentation hints; measurement logic must not depend on a specific widget representation.

emit_log(block, *args) -> None

Forward one log block to the UI and configured log sinks. Preserve the complete text of exceptions and safety messages.

poll_key() -> int | None

Poll without blocking. Return one key code or None. Measurement kernels call this at defined intervention and safety points.

check_interrupt() -> int | None

Compatibility name delegating to poll_key().

pre_user_event() and post_user_event()

Mark the beginning and end of an operator interaction, for example to update UI status or pause terminal handling.

run_interactive(obj, banner)

Start an interactive terminal session where supported. A GUI may report that this operation is unavailable.

The concrete TUIAdapter additionally supports replacing its messenger, logger, interrupt tester, pre/post callbacks, and interactive runner. A custom adapter must provide the corresponding setter only if application code calls the related Measure.set_* method after installing that adapter.

Binding to Measure

Measure creates a TUIAdapter by default. set_ui_adapter(adapter) binds these compatibility attributes to the new adapter:

messenger             -> ui.ask
UserInterruptTester   -> ui.check_interrupt
PollKey               -> ui.poll_key
PreUserEvent          -> ui.pre_user_event
PostUserEvent         -> ui.post_user_event

Use set_user_interrupt_tester in maintained code. The historical spelling set_user_interrupt_Tester is only a compatibility alias. Pickle restore recreates the default TUI adapter because UI objects and callbacks are not serialized. A script or runner therefore attaches its desired adapter after loading the history.

resolve_poll_key(handler, caller_locals) accepts an explicit callable and retains caller-frame lookup only as a compatibility fallback. New kernels pass the polling callable explicitly.

Qt adapter

QtUIAdapter communicates with MeasurementControlWidget through queued Qt signals:

prompt_requested

Transfers prompt text, buttons, level, and data into the GUI thread.

log_requested

Appends complete log text without touching the widget from the worker.

status_changed

Updates the visible lifecycle state.

ask blocks the measurement worker on a threading.Condition while the Qt event loop remains responsive. A button calls submit_answer in the GUI thread and wakes the worker. Never call this blocking method from the GUI thread itself.

The shared qt_runner performs the standard wiring:

  1. load the text measurement script as a module;

  2. copy and update its configuration;

  3. record sources and changes of the effective configuration;

  4. remove terminal-only messenger and user_interrupt_tester callbacks;

  5. attach the QtUIAdapter and its manual EUT monitor;

  6. move MeasurementTask into a QThread;

  7. return success, stop, and full exceptions through signals;

  8. stop and delete worker objects when the thread finishes.

The control window separates measurement and effective configuration into tabs. The configuration tab is read-only and shows each value together with the last source that changed it. Stop / RF Off remains outside the tabs, so it stays immediately available while the operator inspects configuration.

Stop and RF-off

The Qt Stop / RF Off button follows two paths:

  • if an active prompt offers Quit, it submits that answer;

  • otherwise it records a thread-safe interrupt request. poll_key returns the configured synthetic key exactly once.

The adapter does not access hardware. The measurement worker receives the request at its next polling point and runs the normal RF-off and finalization path. Responsiveness therefore depends on bounded instrument calls and short, documented polling intervals. A worker blocked indefinitely in a driver cannot be made safe merely by adding a GUI button.

Only use a separate immediate hardware safety path when the device and communication layer explicitly permit it. Serialize access to a shared transport; concurrent commands from GUI and worker threads can corrupt protocol state.

Manual and automatic EUT monitoring

Manual intervention remains available for every immunity exposure. QtUIAdapter.manual_eut_monitor is attached to applicable measurement parameters by qt_runner. During exposure, the operator can submit degraded, failed, or not_evaluated. After exposure, state and recovery are entered separately.

Automatic camera, communication, or measurement-channel monitors supplement this manual path through CompositeEUTMonitor and may run through ThreadedEUTMonitor. They do not replace operator input. The EUT event structure and assessment rules define the data passed back to the measurement kernel. See Using and extending EUT monitoring for the complete usage and extension guide.

Worker design

A worker needs one start slot, bounded operations, cooperative cancellation, and exactly one terminal outcome for success, cancellation, or error. Qt signals should carry small immutable objects. High-rate scans must batch progress data; thousands of queued point signals can make the GUI appear blocked after the instrument has already finished.

Callbacks from ReceiverScanWorker execute in its Python worker thread. GUI applications must bridge them through Qt signals or a thread-safe queue. Its optional safety_stop is called at most once on cancellation or error.

Closing a window while work is active must request stop, wait for a bounded period, and present a clear final state. Calling QThread.quit() only stops the thread event loop; it does not interrupt arbitrary Python code already running in the worker slot.

Implementation checklist

For a new UI:

  • implement every core adapter method with the return semantics above;

  • create and modify widgets only in the GUI thread;

  • execute the complete measurement script in one owned worker;

  • bridge prompts, logs, progress, cancellation, and exceptions explicitly;

  • make manual EUT reporting always reachable;

  • test stop during waits, scans, prompts, leveling, and error handling;

  • preserve the complete RF-off/finalization path and autosave state.

Tests

Run Qt tests with QT_QPA_PLATFORM=offscreen. Verify at least that the UI remains responsive, prompts wake the worker, progress appears incrementally, stop is consumed once and causes RF-off, errors retain their traceback, EUT state and recovery remain distinct, and closing has bounded behavior.

The curated UI API lists the concrete adapter, runner, and worker entry points.