plesty.server.view.control

The Control window: one device panel per running device, in one shell.

Several devices get driven at once — a stage and the meter reading it — so the controls cannot be a modal dialog: it would freeze the console’s table, its log tail and its job list for as long as it is open, which is the reason docs/design/console.md already refuses modals for failures. Nor is it one loose window per device, which nothing arranges and nothing remembers.

It is a Shell instead — plesty-lib’s docking window, opened inside the console’s own application (Shell takes a parent and starts no event loop of its own). Docking, tabbing, dragging a panel to a second screen and the arrangement surviving a restart all come with it, and one clock ticks every panel. The console’s main window keeps its own layout: this is a separate window, not a change to it.

Each panel holds its own DeviceLink and its own worker thread, so a device server that stopped answering costs its panel and nothing else.

Attributes

TITLE

INTERVAL_MS

EMPTY_HINT

Classes

ControlWindow

A shell holding one DevicePanel per device being controlled.

Functions

_release(→ None)

Close the link of an already-closed panel.

Module Contents

plesty.server.view.control.TITLE = 'PLESTY Control'
plesty.server.view.control.INTERVAL_MS = 500
plesty.server.view.control.EMPTY_HINT = Multiline-String
Show Value
"""No device controls open.

Controls need a running device server: start one from the console, then open it here with Device > Control... (Ctrl+D)."""
class plesty.server.view.control.ControlWindow(bench: plesty.server.presenter.Bench, parent: PySide6.QtWidgets.QWidget | None = None, title: str | None = None)

Bases: plesty.lib.ui.qt.shell.Shell

A shell holding one DevicePanel per device being controlled.

Create the window; it holds no panels until one is opened.

Parameters:
  • bench (plesty.server.presenter.Bench) – The presenter, asked for a device’s link.

  • parent (Optional[PySide6.QtWidgets.QWidget]) – The console’s main window. Qt keeps a parented window on top of it and closes it with it, which is what stops a control window outliving the console that opened it.

  • title (Optional[str]) – What the window is called. The bench’s own wall takes the default; a window opened for one device is named for it, so two of them on a second screen can be told apart.

bench
closed = False
_placeholder = False
say_empty(text: str) → None

Fill an empty window with text instead of leaving it blank.

A Shell has no central widget — its panels are the layout — so one holding no panels paints a menu bar, a status bar and nothing at all. Opening it and being shown an empty frame reads as a broken window, not as an empty one, which is exactly how it was first reported.

Parameters:

text (str) – What to show; replaces any previous placeholder.

Return type:

None

_sync_placeholder() → None

Show the placeholder when there are no panels, and never beside one.

Return type:

None

open_device(name: str, writable: bool = False) → plesty.lib.ui.DevicePanel

Show the controls of name, or focus them if they are already up.

Asking twice for one device must not give it two panels: the second would poll the same instrument on a second connection and show its own idea of the values.

Parameters:
  • name (str) – The device to control.

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

Returns:

The panel showing that device.

Raises:

BenchError – When the device is not in a state to be controlled.

Return type:

plesty.lib.ui.DevicePanel

open_handle(name: str, handle: object, host: str = '', writable: bool = False) → plesty.lib.ui.DevicePanel

Show the controls of name on an already-built handle.

What a panel drives is a protocol — describe, query, write, call — and a device on a node satisfies it through the agent’s relay exactly as one on this bench satisfies it directly. So a remote device needs no panel of its own, only its own handle and its own key: two benches may each declare a pm100d, and one panel must not stand for both.

Parameters:
  • name (str) – The device to control.

  • handle (object) – What the panel drives.

  • host (str) – The node it runs on, empty for this bench’s own.

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

Returns:

The panel showing that device.

Return type:

plesty.lib.ui.DevicePanel

open_devices(names: collections.abc.Sequence[str]) → list[tuple[str, str]]

Open a panel for each of names, skipping the ones that refuse.

One device that cannot be controlled must not cost the operator the others: a bench of four with one server down should show three panels and say what happened to the fourth.

Parameters:

names (collections.abc.Sequence[str]) – The devices to open.

Returns:

(name, reason) for each device that could not be opened.

Return type:

list[tuple[str, str]]

close_device(name: str) → bool

Remove the panel of name, releasing its connection.

Parameters:

name (str) – The device whose panel to drop.

Returns:

Whether there was one.

Return type:

bool

property devices: list[str]

The devices that currently have a panel, in the order they were opened.

Return type:

list[str]

reread(name: str) → bool

Have the panel of name read its device again; False when it has none.

After the server is started or restarted the panel has made no call, so the error banner of the last one that failed stayed up over a device that now answers (#22). A fresh read replaces the values the old server reported and, when it succeeds, clears the banner.

Parameters:

name (str)

Return type:

bool

hold(name: str) → bool

Stop the panel of name calling its server while it is stopped.

Released by reread() once the server is back. A handle that cannot be held — a node’s relay — is left as it is.

Returns:

Whether there was a panel to hold.

Parameters:

name (str)

Return type:

bool

closeEvent(event: Any) → None

Stop every panel’s worker, then release the connections.

Parameters:

event (Any) – The Qt close event.

Return type:

None

plesty.server.view.control._release(panel: Any) → None

Close the link of an already-closed panel.

Order matters, which is why this is never called before the panel is: the link’s socket was opened on the panel’s worker thread, and a ZMQ socket may only move to another thread once nothing else can touch it. Closing the panel joins that worker, and the join is the barrier that makes closing the socket here safe.

A worker that did not stop — a call still stuck in an instrument past the join’s timeout — keeps its socket. Closing it under a thread that is still using it is the one thing that could crash the console, and the cost of leaving it is a socket on a daemon thread that the process exit reclaims.

Parameters:

panel (Any) – The panel whose handle to release.

Return type:

None