plesty.lib.ui.device_link ========================= .. py:module:: plesty.lib.ui.device_link .. autoapi-nested-parse:: How a panel reaches a device, and why it never does so on the GUI thread. A monitor reads a run directory; a device panel drives an instrument, and the instrument may be in this process, in another one, or on another machine. The three cases differ only in the *handle*: - in a module's own GUI, the device object, wrapped by :class:`LocalHandle` - in the bench console, a ``DeviceTCPIPClient`` — it satisfies the protocol as written - from a remote ``plesty-bench``, the same calls relayed by the bench agent :class:`DeviceHandle` is that shared shape, so :class:`~plesty.lib.ui.DevicePanel` is written once and the transport is somebody else's problem. :class:`DeviceWorker` is the second half. Panels are ticked on the shell's clock, which is the GUI thread, and a device read is a round trip to hardware: a shell holding a panel per device would spend its frame budget waiting on serial lines. Every call therefore goes to a worker thread and every result comes back through a queue the panel drains on its next tick — the shell's own clock does the hand-over, so nothing here imports a toolkit or needs a signal. Attributes ---------- .. autoapisummary:: plesty.lib.ui.device_link.JOIN_TIMEOUT_S Classes ------- .. autoapisummary:: plesty.lib.ui.device_link.DeviceHandle plesty.lib.ui.device_link.LocalHandle plesty.lib.ui.device_link.Result plesty.lib.ui.device_link._Request plesty.lib.ui.device_link.DeviceWorker Module Contents --------------- .. py:data:: JOIN_TIMEOUT_S :value: 2.0 .. py:class:: DeviceHandle Bases: :py:obj:`Protocol` The four things a panel asks of a device, wherever the device is. ``DeviceTCPIPClient`` satisfies this as written; an in-process device reaches it through :class:`LocalHandle`; a remote bench relays the same four calls through its agent. .. py:method:: describe() -> dict[str, Any] Return ``{"methods": [...], "doc": {...} | None}`` for the device. .. py:method:: query(key: str) -> Any Read configuration parameter *key* from the device. .. py:method:: write(key: str, value: Any) -> Any Write *value* to configuration parameter *key*. .. py:method:: call(op: str, **kwargs: Any) -> Any Run operation *op* with keyword arguments. .. py:class:: LocalHandle(device: Any) A :class:`DeviceHandle` over a device object in this process. The device already answers :meth:`query` and :meth:`write`; what it does not have is the other two, because in-process callers use ``doc_model()`` and call operations as attributes. Going through the same serialized doc payload the wire uses is deliberate — a module's own GUI then exercises exactly what the console and a remote client will see, so a hole in the payload shows up at the bench and not only across a network. Wrap *device*. :param device: A :class:`~plesty.lib.device.base_device_sync.BaseDeviceSyncModel`. .. py:attribute:: device .. py:method:: describe() -> dict[str, Any] Return the device's methods and its serialized doc model. :returns: The same shape the device server's ``describe`` reply carries. .. py:method:: query(key: str) -> Any Read parameter *key*. :param key: Configuration key. :returns: The value the device reported. .. py:method:: write(key: str, value: Any) -> Any Write *value* to parameter *key*. :param key: Configuration key. :param value: The value to set. :returns: Whatever the device's ``write`` returns. .. py:method:: call(op: str, **kwargs: Any) -> Any Run operation *op*. :param op: Operation name. :param \*\*kwargs: The operation's arguments. :returns: The operation's result. .. py:class:: Result One finished call, waiting to be applied on the GUI thread. :ivar tag: What the panel asked for, as it labelled the request. :ivar value: The return value, or ``None`` when the call raised. :ivar error: The exception message, or ``""`` when the call succeeded. :ivar duration: How long the call took, in milliseconds. .. py:attribute:: tag :type: Any .. py:attribute:: value :type: Any :value: None .. py:attribute:: error :type: str :value: '' .. py:attribute:: duration :type: float :value: 0.0 .. py:property:: ok :type: bool Whether the call returned rather than raised. .. py:class:: _Request One queued call. .. py:attribute:: tag :type: Any .. py:attribute:: verb :type: str .. py:attribute:: args :type: tuple[Any, Ellipsis] :value: () .. py:attribute:: kwargs :type: dict[str, Any] .. py:class:: DeviceWorker(handle: DeviceHandle, name: str = 'device') Runs a handle's calls on one thread and hands the results back by queue. One worker per panel, not one per shell. A device server that has died answers nothing until its client times out, and a shared worker would make that one device stall every panel in the window; a worker of its own means the dead device's panel goes stale and the rest keep their clock. Requests are *coalesced by tag*: asking again for something already queued or in flight is dropped rather than appended. Without it a device slower than the shell's refresh accumulates a poll backlog it can never work off, and the panel ends up showing readings from minutes ago. Prepare a worker over *handle*; it starts on :meth:`start`. :param handle: What the worker calls. :param name: Used for the thread name and log lines. .. py:attribute:: handle .. py:attribute:: name :value: 'device' .. py:attribute:: _requests :type: queue.Queue[Optional[_Request]] .. py:attribute:: _results :type: queue.Queue[Result] .. py:attribute:: _inflight :type: set[Any] .. py:attribute:: _lock .. py:attribute:: _thread :type: Optional[threading.Thread] :value: None .. py:method:: start() -> None Start the worker thread; calling it twice is harmless. .. py:method:: stop(timeout: float = JOIN_TIMEOUT_S) -> None Ask the worker to finish and wait briefly for it. The thread is a daemon and the wait is short on purpose: a call already carries the device's own timeout, and closing a window must not hang on an instrument that stopped answering. :param timeout: Seconds to wait for the thread to end. .. py:property:: running :type: bool Whether the worker thread is alive. .. py:method:: submit(tag: Any, verb: str, *args: Any, **kwargs: Any) -> bool Queue a call, unless one with the same *tag* is already outstanding. :param tag: Label the matching :class:`Result` carries back, and the key requests are coalesced on. :param verb: ``"describe"``, ``"query"``, ``"write"`` or ``"call"``. :param \*args: Positional arguments for the verb. :param \*\*kwargs: Keyword arguments for the verb. :returns: ``True`` when the request was queued, ``False`` when an identical tag was already outstanding and this one was dropped. .. py:method:: results() -> list[Result] Take every finished call off the queue without blocking. :returns: The results that arrived since the last drain, in order. .. py:property:: pending :type: int How many calls are queued or in flight. .. py:method:: _run() -> None Take requests until asked to stop. .. py:method:: _answer(request: _Request) -> Result Run one request and wrap whatever it does into a :class:`Result`. Every exception is caught: a panel reports a device that refused a write in its title bar, and a worker thread that died would instead leave the panel silently frozen. :param request: The queued call. :returns: The result to hand back.