plesty.lib.ui.device_panel

The panel that shows a device’s controls, derived from what it declares.

A device already describes its controls completely. This, from a spectrometer’s schema, is a widget specification:

ConfigParameter(name='exposure', dtype=float, unit='ms', default=100.0,
                min_value=0.0, max_value=None, options=None,
                read_only=False, description='Integration time …')

dtype picks the control, min_value/max_value bound it, options makes it a drop-down, read_only makes it a readout, unit is the suffix and description is the tooltip. Hand-writing that duplicates the schema, and the duplicate drifts: the panel written by hand for one spectrometer bounded an exposure at 0.01–60000 ms where the device declares no upper bound at all — a limit that exists nowhere in the instrument, silently constraining whoever uses it.

So controls_for() derives the controls instead, and DevicePanel renders them. What it deliberately does not do:

  • Device-specific views stay out. A spectrum, a beam image, a stage map are not derivable from a schema; they are monitors, and a MonitorPanel docks beside this one.

  • Not every operation belongs on a panel. Homing a stage or replacing a dark reference should not be one click away, so operations declared lifecycle are left out unless asked for by name.

  • A monitor holds no device; this necessarily does. It drives an instrument somebody may be measuring with, which is why it starts read-only.

from plesty.lib.ui import DevicePanel, LocalHandle, run

run([DevicePanel(LocalHandle(spectrometer), name="cct",
                 include=("exposure", "hardware_average"))])

Attributes

CONTROL_KINDS

DEFAULT_SKIP_KINDS

_NUMERIC

REPEATABLE_OP_KINDS

DEFAULT_POLL_S

DEFAULT_GROUP

Classes

Control

One rendered control, as derived from the device's own declaration.

DevicePanel

Sub-window holding one device's controls.

Functions

_label_for(→ str)

Turn a schema key into something readable.

_full_key(→ str)

Address a parameter the way the device resolves it.

_kind_for(→ str)

Decide which control a declaration asks for.

_declared_types(→ frozenset[str])

The types a declaration allows, a union split into its members.

_tuple_or_none(→ Optional[tuple[Any, Ellipsis]])

Return value as a tuple, or None when it holds nothing.

_parameter_control(→ Control)

Build the control one configuration parameter asks for.

_argument_control(→ Control)

Build the control one operation argument asks for.

_operation_control(→ Control)

Build the control one operation asks for.

_output_control(→ Control)

Describe one value an operation returns.

controls_for(, operations, skip_kinds)

Derive a device's controls from its serialized doc model.

Module Contents

plesty.lib.ui.device_panel.CONTROL_KINDS: tuple[str, Ellipsis] = ('number', 'choice', 'toggle', 'text', 'readout', 'action')
plesty.lib.ui.device_panel.DEFAULT_SKIP_KINDS: tuple[str, Ellipsis] = ('lifecycle',)
plesty.lib.ui.device_panel._NUMERIC
plesty.lib.ui.device_panel.REPEATABLE_OP_KINDS: tuple[str, Ellipsis] = ('read', 'acquire')
plesty.lib.ui.device_panel.DEFAULT_POLL_S = 1.0
class plesty.lib.ui.device_panel.Control

One rendered control, as derived from the device’s own declaration.

Variables:
  • key – Configuration key or operation name.

  • kind – One of CONTROL_KINDS.

  • label – Text shown beside the control.

  • dtype – Declared type name, as it crossed the wire.

  • group – Configuration group the parameter belongs to.

  • unit – Physical unit, shown as the editor’s suffix.

  • minimum – Lower bound, or None when the device declares none.

  • maximum – Upper bound, or None when the device declares none.

  • options – Allowed values, which make the control a drop-down.

  • default – Declared default.

  • tooltip – The declaration’s description.

  • read_only – The device reports it; the operator cannot set it.

  • write_only – The operator sets it; it cannot be read back, so it is never polled.

  • arguments – For an action, the operation’s arguments as controls of their own — the same rules, one level down.

  • outputs – For an action, what it returns — name, type and unit — which is what lets a result be shown as a reading rather than as the mapping it arrives in.

  • op_kind – For an action, its OpKind kind.

key: str
kind: str
label: str
dtype: str = 'Any'
group: str = 'default'
unit: str | None = None
minimum: float | None = None
maximum: float | None = None
options: tuple[Any, Ellipsis] | None = None
default: Any = None
tooltip: str = ''
read_only: bool = False
write_only: bool = False
arguments: tuple[Control, Ellipsis] = ()
outputs: tuple[Control, Ellipsis] = ()
op_kind: str = ''
required: bool = True
property types: frozenset[str]

The types this declaration allows, a union split into its members.

None is not among them: it is not a type an editor can offer, and what is left is what the control has to be able to send. Switch on this rather than on dtype, which is one string and may be a union.

Return type:

frozenset[str]

property omittable: bool

Whether leaving this argument alone means sending nothing.

An optional argument the device gives no default for has an answer already — its own — and the panel has no way to show one. A spin box always holds a number, so an untouched one said 0, and 0 is a real value: wait_settled read it as a zero-second budget and timed out at once. Such an argument is shown as unset and left out of the call instead.

Return type:

bool

property repeatable: bool

Whether holding this control should keep asking the device.

Any readable parameter. An operation only when the module declares it REPEATABLE_OP_KINDS — a reading or an acquisition, never a move or a stop.

Return type:

bool

property readable: bool

Whether polling this control makes sense.

Return type:

bool

property writable: bool

Whether the operator can send this control anywhere.

Return type:

bool

plesty.lib.ui.device_panel._label_for(name: str) → str

Turn a schema key into something readable.

Parameters:

name (str) – The declared key, conventionally lower_snake_case.

Returns:

The key with separators as spaces and the first letter capitalised.

Return type:

str

plesty.lib.ui.device_panel.DEFAULT_GROUP = 'default'
plesty.lib.ui.device_panel._full_key(group: str, name: str) → str

Address a parameter the way the device resolves it.

A grouped parameter is addressed as group.parameter and a device refuses the bare key outright — Grouped parameter 'CustomZero' requires full key format 'group_prefix.param_key'. The panel used to derive its key from the parameter’s name alone, so every read and every write of a grouped parameter was refused by the device it was built from.

The group’s own name is what is used here. A device may also give a group a command prefix, but it resolves either, and the prefix is not in the serialized model this panel is built from.

Parameters:
  • group (str) – The configuration group the parameter was declared in.

  • name (str) – The parameter’s declared name.

Returns:

The key the device answers to.

Return type:

str

plesty.lib.ui.device_panel._kind_for(dtype: str, options: Any, read_only: bool) → str

Decide which control a declaration asks for.

Order matters: a read-only parameter is a readout whatever its type, so that an enumerated reading is not offered as a drop-down that cannot be set.

Parameters:
  • dtype (str) – Declared type name.

  • options (Any) – Declared allowed values, if any.

  • read_only (bool) – Whether the device refuses writes to it.

Returns:

One of CONTROL_KINDS.

Return type:

str

plesty.lib.ui.device_panel._declared_types(dtype: str) → frozenset[str]

The types a declaration allows, a union split into its members.

An optional number is declared float | None, and read whole it is neither float nor int: the parameter fell through to a text field, which hands the device the string an operator typed. A timeout entered as 10000 then reached the instrument as "10000" and the device failed comparing a str to an int. None is not a type an editor can offer, so it is dropped: what is left is what the control has to be able to send.

Parameters:

dtype (str) – The declared type name, possibly a union.

Returns:

The member names, without None.

Return type:

frozenset[str]

plesty.lib.ui.device_panel._tuple_or_none(value: Any) → tuple[Any, Ellipsis] | None

Return value as a tuple, or None when it holds nothing.

Parameters:

value (Any)

Return type:

Optional[tuple[Any, Ellipsis]]

plesty.lib.ui.device_panel._parameter_control(spec: dict[str, Any], group: str) → Control

Build the control one configuration parameter asks for.

Parameters:
  • spec (dict[str, Any]) – One entry of a serialized doc model’s parameters.

  • group (str) – The configuration group it came from.

Returns:

The derived control.

Return type:

Control

plesty.lib.ui.device_panel._argument_control(spec: dict[str, Any]) → Control

Build the control one operation argument asks for.

An argument declares its bounds as a range pair rather than as two fields, and is never read-only — otherwise the rules are the parameters’.

Parameters:

spec (dict[str, Any]) – One entry of an operation’s iparams.

Returns:

The derived control.

Return type:

Control

plesty.lib.ui.device_panel._operation_control(spec: dict[str, Any]) → Control

Build the control one operation asks for.

Parameters:

spec (dict[str, Any]) – One entry of a serialized doc model’s functions.

Returns:

An action control carrying the operation’s arguments.

Return type:

Control

plesty.lib.ui.device_panel._output_control(spec: dict[str, Any]) → Control

Describe one value an operation returns.

Parameters:

spec (dict[str, Any]) – One entry of an operation’s oparams.

Returns:

A readout control — it is a value the device reports, which is what a result is.

Return type:

Control

plesty.lib.ui.device_panel.controls_for(doc: dict[str, Any] | None, *, include: Sequence[str] | None = None, exclude: Iterable[str] = (), operations: Sequence[str] | None = None, skip_kinds: Iterable[str] = DEFAULT_SKIP_KINDS) → list[Control]

Derive a device’s controls from its serialized doc model.

This is the whole schema-to-widget mapping, and it is deliberately free of any toolkit: it is where the bounds, units and options come from, so it is where a drifting panel would be caught, and a plain test can catch it.

Parameters:
  • doc (Optional[dict[str, Any]]) – A to_dict() payload — from a describe reply or from the device itself. None (an older server, or a device whose model failed to build) yields no controls rather than an error.

  • include (Optional[Sequence[str]]) – Parameter keys to show, in this order. None shows every declared parameter. A grouped parameter is named as the device addresses it, group.parameter.

  • exclude (Iterable[str]) – Parameter keys to leave out, applied after include.

  • operations (Optional[Sequence[str]]) – Operation names to offer, in this order. None offers every operation whose kind is not in skip_kinds; naming one overrides that, so an operation can always be asked for explicitly.

  • skip_kinds (Iterable[str]) – Operation kinds left out when operations is None.

Returns:

The parameters in declaration order, then the operations.

Return type:

list[Control]

class plesty.lib.ui.device_panel.DevicePanel(handle: plesty.lib.ui.device_link.DeviceHandle, *, doc: dict[str, Any] | None = None, include: Sequence[str] | None = None, exclude: Iterable[str] = (), operations: Sequence[str] | None = None, skip_kinds: Iterable[str] = DEFAULT_SKIP_KINDS, writable: bool = False, poll_s: float = DEFAULT_POLL_S, theme: plesty.lib.ui.theme.Theme | None = None, **kwargs: Any)

Bases: plesty.lib.ui.panel.Panel

Sub-window holding one device’s controls.

Constructing the panel touches neither a toolkit nor the device: it is built when a shell adds it, and it asks the device what it offers on its own worker thread, so a slow or absent instrument delays nothing. Until that answer arrives the panel says so.

The panel starts read-only. It may be pointed at an instrument an experiment is driving, and a stray write into a running measurement is worse than a click: set_writable() is what a Take control toggle calls.

Wrap handle into a dockable control panel.

Parameters:
  • handle (plesty.lib.ui.device_link.DeviceHandle) – What the panel drives — see DeviceHandle.

  • doc (Optional[dict[str, Any]]) – The device’s serialized doc model when the caller already has it (a console that just called describe); None makes the panel fetch it.

  • include (Optional[Sequence[str]]) – Parameter keys to show, in order.

  • exclude (Iterable[str]) – Parameter keys to leave out.

  • operations (Optional[Sequence[str]]) – Operations to offer, in order.

  • skip_kinds (Iterable[str]) – Operation kinds left out by default.

  • writable (bool) – Start with writes and operations enabled. The default is read-only.

  • poll_s (float) – Seconds between re-reads of the device.

  • theme (Optional[plesty.lib.ui.theme.Theme]) – Palette; None loads the packaged one.

  • **kwargs (Any) – Forwarded to Panel.

kind: ClassVar[str] = 'device'

Panel category, used for layout keys and styling hooks.

handle
theme = None
writable = False
poll_s = 1.0
live: dict[str, dict[str, Any]]
controls: list[Control] = []
values: dict[str, Any]
worker
form: Any = None
_selection
_doc = None
_note = ''
_tone = 'idle'
_latency = 0.0
_polled_at = 0.0
_selection_kwargs() → dict[str, Any]

Return the selection arguments controls_for() was configured with.

Return type:

dict[str, Any]

open() → None

Start the worker and ask the device what it offers.

Separate from build() so the device-facing half of the panel — which is all of the interesting behaviour — can be driven with no display and no toolkit: open it, tick() it, and read controls and values.

Return type:

None

build(parent: Any = None) → Any

Create the form, then open the device.

Parameters:

parent (Any) – The parent widget provided by the shell.

Returns:

The form widget.

Return type:

Any

tick() → None

Apply whatever the worker finished, then poll if polling is on.

Both halves run on the shell’s clock and neither blocks: this is the only place results cross back to the GUI thread, and the coalescing in DeviceWorker is what keeps a slow device from building a backlog it can never work off.

Polling is off until it is asked for — see set_live().

Return type:

None

control(key: str) → Control | None

The control named key, or None.

Parameters:

key (str)

Return type:

Optional[Control]

_read(key: str, arguments: dict[str, Any] | None = None) → None

Ask the device about one control: a parameter’s value, or an operation.

Parameters:
  • key (str)

  • arguments (Optional[dict[str, Any]])

Return type:

None

read_once(key: str | None = None, arguments: Any = None) → None

Ask the device once: for key, or for every value the panel shows.

Parameters:
  • key (Optional[str]) – The control to read; None reads every readable one.

  • arguments (Any) – For an operation, what to call it with.

Return type:

None

set_live(key: str, live: bool, arguments: Any = None) → None

Start or stop re-reading one control on the shell’s clock.

Parameters:
  • key (str) – The control to keep reading.

  • live (bool) – Whether to keep reading it.

  • arguments (Any) – For an operation, what to keep calling it with. Taken once, when it starts: an operation repeating on a timer should go on asking the same question, not whatever the form happens to hold when the timer next fires.

Return type:

None

is_live(key: str) → bool

Whether key is being re-read.

Parameters:

key (str)

Return type:

bool

status() → str

Return the line shown in the panel’s title bar.

Returns:

What the panel is doing, or what went wrong.

Return type:

str

tone() → str

Return the state colour for the panel’s dot.

Returns:

error when the last call failed, warning when the device offered no schema, running once controls are on screen.

Return type:

str

close() → None

Stop the worker thread.

Return type:

None

describe() → dict[str, Any]

Return the panel description including what it drives.

Returns:

The panel’s placement plus its controls and whether it is writable.

Return type:

dict[str, Any]

set_writable(writable: bool) → None

Enable or disable every editor and button.

Parameters:

writable (bool) – Whether the operator may drive the device.

Return type:

None

request_write(key: str, value: Any) → None

Send a parameter change to the device.

Parameters:
  • key (str) – Configuration key.

  • value (Any) – The value the operator entered.

Return type:

None

request_call(op: str, arguments: dict[str, Any] | None = None) → None

Run an operation on the device.

Parameters:
  • op (str) – Operation name.

  • arguments (Optional[dict[str, Any]]) – The operation’s arguments.

Return type:

None

_apply(result: plesty.lib.ui.device_link.Result) → None

Fold one finished call into the panel’s state and its form.

Parameters:

result (plesty.lib.ui.device_link.Result) – What the worker handed back.

Return type:

None

_receive_doc(describe: Any) → None

Derive the controls from a describe reply and fill the form.

Parameters:

describe (Any) – The reply, or None.

Return type:

None

_fail(result: plesty.lib.ui.device_link.Result) → None

Record a failed call in the title bar.

Parameters:

result (plesty.lib.ui.device_link.Result) – The failed result.

Return type:

None