plesty.server.view.control ========================== .. py:module:: plesty.server.view.control .. autoapi-nested-parse:: 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 :class:`~plesty.lib.ui.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 :class:`~plesty.server.services.DeviceLink` and its own worker thread, so a device server that stopped answering costs its panel and nothing else. Attributes ---------- .. autoapisummary:: plesty.server.view.control.TITLE plesty.server.view.control.INTERVAL_MS plesty.server.view.control.EMPTY_HINT Classes ------- .. autoapisummary:: plesty.server.view.control.ControlWindow Functions --------- .. autoapisummary:: plesty.server.view.control._release Module Contents --------------- .. py:data:: TITLE :value: 'PLESTY Control' .. py:data:: INTERVAL_MS :value: 500 .. py:data:: EMPTY_HINT :value: Multiline-String .. raw:: html
Show Value .. code-block:: python """No device controls open. Controls need a running device server: start one from the console, then open it here with Device > Control... (Ctrl+D).""" .. raw:: html
.. py:class:: ControlWindow(bench: plesty.server.presenter.Bench, parent: Optional[PySide6.QtWidgets.QWidget] = None, title: Optional[str] = None) Bases: :py:obj:`plesty.lib.ui.qt.shell.Shell` A shell holding one :class:`DevicePanel` per device being controlled. Create the window; it holds no panels until one is opened. :param bench: The presenter, asked for a device's link. :param parent: 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. :param title: 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. .. py:attribute:: bench .. py:attribute:: closed :value: False .. py:attribute:: _placeholder :value: False .. py:method:: say_empty(text: str) -> None Fill an empty window with *text* instead of leaving it blank. A :class:`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. :param text: What to show; replaces any previous placeholder. .. py:method:: _sync_placeholder() -> None Show the placeholder when there are no panels, and never beside one. .. py:method:: 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. :param name: The device to control. :param writable: 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. .. py:method:: 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. :param name: The device to control. :param handle: What the panel drives. :param host: The node it runs on, empty for this bench's own. :param writable: Start with writes enabled. The default is read-only. :returns: The panel showing that device. .. py:method:: 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. :param names: The devices to open. :returns: ``(name, reason)`` for each device that could not be opened. .. py:method:: close_device(name: str) -> bool Remove the panel of *name*, releasing its connection. :param name: The device whose panel to drop. :returns: Whether there was one. .. py:property:: devices :type: list[str] The devices that currently have a panel, in the order they were opened. .. py:method:: 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. .. py:method:: hold(name: str) -> bool Stop the panel of *name* calling its server while it is stopped. Released by :meth:`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. .. py:method:: closeEvent(event: Any) -> None Stop every panel's worker, then release the connections. :param event: The Qt close event. .. py:function:: _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. :param panel: The panel whose handle to release.