plesty.lib.service.tcp_ip_server ================================ .. py:module:: plesty.lib.service.tcp_ip_server .. autoapi-nested-parse:: ZeroMQ-based asynchronous TCP/IP device server for Plesty. Classes ------- .. autoapisummary:: plesty.lib.service.tcp_ip_server.DeviceTCPIPServer Module Contents --------------- .. py:class:: DeviceTCPIPServer(device, wrapper_cls, address='tcp://*:5555', resources=None) 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. :param resources: Optional flat collection or nested dict tree of lockable resources. .. py:attribute:: resource_manager .. py:attribute:: client_metadata :type: dict[str, dict] .. py:attribute:: device .. py:attribute:: ctx .. py:attribute:: socket .. py:attribute:: host .. py:attribute:: port .. py:property:: address :type: str Get the server's bind address. .. py:property:: is_running :type: bool 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:: _client_id(identity: bytes) -> str :staticmethod: Return a stable string form of a ZeroMQ ROUTER identity frame. .. py:method:: _log_orphan_reply(identity: bytes, request_str: str, exc: Exception) -> None Report a reply that had nowhere to go, at the severity it deserves. Two very different things produce an unroutable reply, and until now they produced the same warning. A ``disconnect`` is routine: the client asks to be released and its composite gives that acknowledgement a one-second window before dropping the socket, so a server that is a moment slow — because it is finishing an acquisition, the loop being sequential — answers a client that has already gone. Nothing is lost, and a teardown per run turns the log into noise nobody reads. Anything else is a lost answer to work the device has already done: an exposure taken, a parameter written, a stage moved. The caller saw a timeout and will decide the operation failed. That deserves a warning, and it deserves to name the operation, because "some reply was lost" is not something anyone can act on afterwards. :param identity: ROUTER identity frame of the vanished client. :param request_str: The request being answered, as received. :param exc: The routing error. .. py:method:: _requested_target(request_str: str) -> str :staticmethod: Return what a request acted on (function or parameter), for the log. .. py:method:: _handle(request_str, identity: bytes) :async: Handle a single client request. :param request_str: JSON-encoded request string. :param identity: ZeroMQ ROUTER identity frame bytes. :returns: JSON-serializable response. :rtype: dict .. py:method:: _encode(response: dict, identity: bytes) -> bytes Serialize a response, turning a failure into that client's error. ``_handle`` catches everything a device call can raise and answers with an error dict — but the encoding happened in :meth:`run`, outside that boundary. A result JSON cannot carry therefore raised in the server loop, hit its outer handler, and shut the server down: one client calling one method that returns an ndarray, a dataclass or a set took the instrument away from every other client, and the failure looked like a timeout because nothing answered. That is finding 3 of the 2026-08-04 round, and every device that returns anything richer than a JSON primitive is one call away from it. A method whose return value cannot cross the protocol is a defect in that method. It is not a reason to end the session. :param response: The response ``_handle`` produced. :param identity: ROUTER identity of the requesting client, for the log. :returns: The encoded response, or an encoded error naming the type that could not be serialized. .. py:method:: _describe() :async: Introspect the device and list available methods. :returns: Available callable methods. :rtype: dict .. py:method:: _help() :async: .. py:method:: run() -> None :async: Run the server loop indefinitely. This method listens for incoming requests and processes them sequentially. Each request is handled asynchronously. .. py:method:: shutdown() -> None :async: Gracefully shutdown the server.