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.

Device lifecycle

When the DRIVER entry changes, a test UI shuts down the previous driver instance before adopting the new one. Safety-relevant devices first receive their corresponding stop command: RFOff for signal generators, Standby for amplifiers, and Move(0) for motor controllers. A safe shutdown failure aborts the switch.

Multi-channel UIs close every known channel instance before resetting shared controller state. The motor-controller UI does not send Stop and Quit in parallel with a running worker. A stop or close request made during a call is queued and executed after that call finishes. Nonzero error codes in driver results are treated like exceptions; explicitly defined NB pending states remain valid.

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.

Signal generator test UI

In mpylab.device.signalgenerator_ui, RF On, RF Off and the RF status remain visible above all tabs. Red means ON, green OFF and amber an unknown or failed state; text also identifies the state. RF On is disabled while a driver operation is running. RF Off remains available: its request is queued behind the active bounded bus call and runs next on the same serialized device worker. This avoids concurrent transport access. RF Off on close remains in place.

Values selected for a smoke test are copied in the GUI thread before the task is submitted. The worker never reads Qt widgets directly.

In the RF / Level tab, clicking dBm, dBuV, mW or µV immediately applies the entered number in that unit. Actual level displays the driver readback in the selected unit. Readback preserves the setpoint; match or mismatch indicates the comparison. Unavailable or unconvertible values are explicitly indicated. Conversion uses the existing UConv conventions.

The SMC100A driver reads RF state after Init and on Refresh using OUTP:STAT?. Invalid replies are treated as errors; a previously known state is then no longer displayed as current.

Display conversion between power and voltage units assumes 50 ohms, matching the signal-generator base driver SetLevel convention.

Active level units use the same blue highlight as frequency units. Actual level shows dBm, dBµV, mW and µV together; individual conversions that cannot be displayed are marked unavailable.

Receiver level display

The Measurement tab shows dBµV, dBm, µV and mW together. Power/voltage conversion uses tools/uconv.py with an explicitly displayed 50-ohm assumption. Logarithmic values have at most two decimal places; linear values use four significant digits, with scientific notation for small values where needed. The original Quantity, including uncertainty, remains unchanged and is available in the tooltip. Displayed values are nominal.

Pending measurements, errors and missing readings have distinct status messages. A previous valid value is explicitly labelled as the last valid reading. Init clears the previous display. Unrepresentable conversions, such as logarithmic levels at zero power, are marked unavailable. Last Reading in the Status tab shows the same values.

Level readings also show standard uncertainties: u for linear units, and asymmetric upper/lower deviations obtained by transforming nominal value ± standard uncertainty with uconv.quantity_to_db for dB units. Linear power/voltage uncertainty propagation is first order at an exact assumed impedance of 50 ohms. A nonpositive lower bound is shown as unbounded (−∞) in logarithmic units.

Device test UIs run driver tasks on exactly one persistent worker thread by default. All calls from one UI are therefore serialized on the same thread while the Qt event loop remains responsive. --no-threaded enables the synchronous GUI-thread fallback when required. Slow instrument replies can then trigger an operating-system “not responding” notification. Compatibility with the VISA/GPIB backend must be checked on the actual setup. Refreshing detector/preamp displays does not send setting commands.

Spectrum analyzer frequency axis

The spectrum view automatically selects Hz, kHz, MHz or GHz for its frequency axis. Tick values are absolute frequencies in that unit; no additional Matplotlib offset such as +1e8 is used. CSV export and the driver interface continue to keep frequencies unchanged in hertz.

The Spectrum tab also provides compact quick controls for center, span, start, stop, RBW, VBW, reference level, attenuation, preamplifier, detector, and trace mode. Frequency values are applied with the unit buttons; the other values have separate Apply and Read buttons. Readbacks and match/mismatch state are synchronized with the full Controls tab and use the same driver calls.

After an acquisition, a trace status line shows the frequency range, point count, RBW, detector, trace, and trace mode. It uses existing readbacks and does not trigger additional instrument queries.

Retrigger repeats GetSpectrumNB with a configurable delay. Calls do not overlap: the next acquisition is scheduled only after the previous one has completed. Turning retrigger off, leaving the Spectrum tab, Init, Quit, and closing stop the sequence. The generic synchronous GetSpectrumNB fallback runs on the persistent device worker by default. With --no-threaded, slow hardware can block the UI until one acquisition ends.