plestylib.service
=================

.. py:module:: plestylib.service


Submodules
----------

.. toctree::
   :maxdepth: 1

   /reference/plestylib/service/tcp_ip_client/index
   /reference/plestylib/service/tcp_ip_server/index


Classes
-------

.. autoapisummary::

   plestylib.service._DeviceTCPIPClient
   plestylib.service._DeviceTCPIPServer
   plestylib.service._AsyncWrapperSafe
   plestylib.service._AsyncDeviceThread


Functions
---------

.. autoapisummary::

   plestylib.service.build_server
   plestylib.service.build_client


Package Contents
----------------

.. py:class:: _DeviceTCPIPClient(address='tcp://localhost:5555', timeout=5000)

   ZeroMQ-based client for remote device control.

   This client provides a transparent interface to a remote device server.
   Methods can be invoked as if the device were local.

   Features:
       - Standard query/write interface
       - Arbitrary remote function calls
       - Automatic method discovery and binding
       - Timeout support
       - Structured error handling

   Initialize the device client.

   :param address: Server address.
   :param timeout: Receive timeout in milliseconds.


   .. py:attribute:: ctx


   .. py:attribute:: socket


   .. py:attribute:: _description


   .. py:method:: _send(payload)

      Send a request to the server and wait for response.

      :param payload: Dictionary payload.

      :returns: Result from server.
      :rtype: Any

      :raises RuntimeError: If server returns an error.
      :raises TimeoutError: If no response is received.



   .. py:method:: query(param, timeout=None)

      Query a device parameter.

      :param param: Parameter name.
      :param timeout: Optional timeout in seconds (server-side).

      :returns: Parameter value.
      :rtype: Any



   .. py:method:: write(param, value, timeout=None)

      Write a device parameter.

      :param param: Parameter name.
      :param value: Value to set.
      :param timeout: Optional timeout in seconds (server-side).

      :returns: Result from device.
      :rtype: Any



   .. py:method:: call(func, *args, timeout=None, **kwargs)

      Call a remote device function.

      :param func: Function name.
      :param args: Positional arguments.
      :param timeout: Optional timeout in seconds (server-side).
      :param kwargs: Keyword arguments.

      :returns: Function result.
      :rtype: Any



   .. py:method:: describe()

      Retrieve available device methods from server.

      :returns: Device description.
      :rtype: dict



   .. py:method:: _build_methods()

      Dynamically attach remote methods as local methods.



   .. py:method:: _make_proxy(name)

      Create a proxy method for a remote function.

      :param name: Function name.

      :returns: Proxy method.
      :rtype: callable



.. py:class:: _DeviceTCPIPServer(device, wrapper_cls, address='tcp://*:5555')

   ZeroMQ-based asynchronous device server.

   This server exposes a synchronous device over TCP/IP using a JSON-based
   RPC protocol. The device is wrapped using an async wrapper (e.g.,
   AsyncWrapperSafe or AsyncDeviceThread) to ensure safe, serialized access.

   The server supports multiple concurrent clients via a ROUTER socket.

   Protocol:

   Request payload:

   .. code-block:: json

       {
         "type": "query | write | call | help | describe",
         "timeout": 3.0
       }

   Response payload:

   .. code-block:: json

       {"status": "ok", "result": "..."}

   Error response payload:

   .. code-block:: json

       {"status": "error", "error": "message", "type": "ExceptionType"}

   Initialize the device server.

   :param device: Synchronous device instance.
   :param wrapper_cls: Async wrapper class (e.g., AsyncDeviceThread).
   :param address: ZMQ bind address.


   .. py:attribute:: device


   .. py:attribute:: ctx


   .. py:attribute:: socket


   .. py:property:: address

      Get the server's bind address.


   .. py:property:: is_running

      Check if the server is currently running.


   .. py:method:: _execute(coro, timeout=None)
      :async:


      Execute a coroutine with optional timeout.

      :param coro: Coroutine to execute.
      :param timeout: Timeout in seconds.

      :returns: Result of coroutine.

      :raises asyncio.TimeoutError: If timeout is exceeded.



   .. py:method:: _handle(request_str)
      :async:


      Handle a single client request.

      :param request_str: JSON-encoded request string.

      :returns: JSON-serializable response.
      :rtype: dict



   .. py:method:: _describe()
      :async:


      Introspect the device and list available methods.

      :returns: Available callable methods.
      :rtype: dict



   .. py:method:: _help()
      :async:



   .. py:method:: run()
      :async:


      Run the server loop indefinitely.

      This method listens for incoming requests and processes them
      sequentially. Each request is handled asynchronously.



   .. py:method:: shutdown()
      :async:


      Gracefully shutdown the server.



.. py:class:: _AsyncWrapperSafe(obj)

   Bases: :py:obj:`AsyncWrapperBase`


   Asynchronous wrapper for synchronous device objects using thread offloading.

   This class converts all callable attributes of a synchronous object into
   asynchronous methods by executing them in a background thread via
   `asyncio.to_thread`. A per-instance asyncio lock ensures that only one
   operation is executed at a time, making it safe for thread-unsafe hardware
   interfaces.

   The wrapper is transparent: methods can be called using `await` without
   modifying the original device implementation.

   A global weak registry ensures that only one wrapper exists per device
   instance, preventing accidental concurrent access through multiple wrappers.

   .. rubric:: Example

   .. code-block:: python

       sync_device = Monochromator(...)
       device = AsyncWrapperSafe(sync_device)

       await device.goto(532)
       await device.set_grating(1)

   Internal State:
       Uses an internal asyncio lock to serialize access and a class-level
       weak registry to maintain one wrapper instance per wrapped object.

   .. rubric:: Notes

   - All wrapped methods are executed in a thread using
     `asyncio.to_thread`, which avoids blocking the event loop.
   - Calls are serialized using an asyncio lock, ensuring safe access
     to non-thread-safe hardware.
   - If the wrapped object already provides async methods, they are
     returned unchanged and not wrapped again.
   - Non-callable attributes are forwarded directly.

   .. warning::

      - Do not mix synchronous and asynchronous access to the same device
        instance, as this can lead to race conditions or hardware conflicts.
      - Creating multiple wrappers for different instances controlling the
        same physical hardware (e.g., same COM port or TCP endpoint) is not
        prevented by this class. Use an external resource registry if needed.
      - Frequent high-rate calls may incur overhead due to thread creation;
        consider a dedicated worker thread model in such cases.

   :raises RuntimeError: May propagate exceptions raised by the underlying device
       methods during execution.

   When to Use:
       - When you need a quick, low-overhead way to integrate synchronous
         device APIs into an asyncio-based application.
       - When device calls are relatively infrequent and simplicity is preferred
         over maximum performance.

   When Not to Use:
       - For high-frequency polling or streaming applications.
       - When strict single-thread execution is required (use a dedicated
         worker-thread model instead).


   .. py:attribute:: _lock


   .. py:method:: _call(func, *args, **kwargs)
      :async:


      Execute a synchronous function asynchronously.
      Must be implemented by subclasses.



.. py:class:: _AsyncDeviceThread(obj)

   Asynchronous wrapper for synchronous device objects using a dedicated worker thread.

   This class provides a transparent async interface for a synchronous device by
   executing all method calls in a single background thread. Calls are queued and
   processed sequentially (FIFO), ensuring strict ordering and eliminating
   concurrent access to the underlying device.

   Unlike thread-per-call approaches, this design avoids repeated thread creation
   and is well-suited for high-frequency or continuous device interactions.

   The wrapper is transparent: callable attributes of the underlying device can
   be accessed as async methods using `await`.

   .. rubric:: Example

   .. code-block:: python

       sync_device = Wavemeter(...)
       device = AsyncDeviceThread(sync_device)

       wavelength = await device.get_wavelength()
       await device.set_mode("fast")

   Internal State:
       Uses a per-instance call queue plus a dedicated worker thread and a
       class-level weak registry to maintain one wrapper per object.

   .. rubric:: Notes

   - All device operations are executed in a single dedicated thread,
     guaranteeing thread safety for non-thread-safe hardware APIs.
   - Calls are processed in FIFO order, ensuring deterministic execution.
   - Results and exceptions are safely communicated back to the asyncio
     event loop using Futures.
   - If the wrapped object already provides async methods, they are returned
     unchanged and not wrapped again.
   - Non-callable attributes are forwarded directly.

   .. warning::

      - Do not mix synchronous and asynchronous access to the same device
        instance, as this may lead to race conditions or undefined behavior.
      - Creating multiple device instances for the same physical hardware
        (e.g., same COM port or TCP endpoint) is not prevented by this class.
        Use a resource registry or device server for distributed coordination.
      - Long-running or blocking device calls will block the worker thread
        and delay subsequent queued operations.

   :raises RuntimeError: May propagate exceptions raised by the underlying device
       methods during execution.
   :raises asyncio.TimeoutError: If a timeout is specified and exceeded.

   When to Use:
       - When working with thread-unsafe hardware (serial, TCP, DLL).
       - For high-frequency polling or continuous control loops.
       - When strict ordering and single-thread execution are required.
       - When performance matters and thread creation overhead must be avoided.

   When Not to Use:
       - For simple or infrequent device interactions where a lightweight
         wrapper is sufficient.
       - When the underlying API is already fully asynchronous.


   .. py:attribute:: _registry


   .. py:attribute:: _queue


   .. py:attribute:: _thread


   .. py:method:: _worker()

      Worker thread: executes device calls sequentially.



   .. py:method:: _call(func, *args, **kwargs)
      :async:


      Schedule a function call in the worker thread and await result.



.. py:function:: build_server(device, fixed_threading=False, address='tcp://*:5555')

   Factory function to create a DeviceTCPIPServer instance.

   :param device: Synchronous device instance.
   :param fixed_threading: If True, use AsyncDeviceThread; otherwise, use AsyncWrapperSafe.
   :param address: ZMQ bind address.

   :returns: DeviceTCPIPServer instance.


.. py:function:: build_client(address='tcp://localhost:5555', timeout=5000)

   Factory function to create a DeviceTCPIPClient instance.

   :param address: Server address.
   :param timeout: Receive timeout in milliseconds.

   :returns: DeviceTCPIPClient instance.


