plesty.lib.service.tcp_ip_server
ZeroMQ-based asynchronous TCP/IP device server for Plesty.
Attributes
Exceptions
The server's own time budget for one request ran out. |
Classes
ZeroMQ-based asynchronous device server. |
Functions
|
Deliver a Windows |
Module Contents
- plesty.lib.service.tcp_ip_server.TRAFFIC_LOGGER = 'plesty.server.traffic'
- plesty.lib.service.tcp_ip_server.TRAFFIC_MARKER = '[TRAFFIC]'
- plesty.lib.service.tcp_ip_server.LIVENESS_TYPES
- exception plesty.lib.service.tcp_ip_server._BudgetExceeded
Bases:
ExceptionThe server’s own time budget for one request ran out.
Kept apart from
TimeoutErrorso a device that raises one of its own (“axis 2 did not reach target within 15 s”) is reported with its message, not as the server giving up.Initialize self. See help(type(self)) for accurate signature.
- plesty.lib.service.tcp_ip_server.stop_on_break() collections.abc.Iterator[None]
Deliver a Windows
CTRL_BREAK_EVENTasKeyboardInterrupt, as Ctrl+C is.CTRL_BREAK_EVENTis the only console control event that can be aimed at a single process group, so it is what a supervisor sends to stop one server without touching its neighbours. Python delivers it asSIGBREAK, whose default action on Windows is to terminate the process outright (exit0xC000013A): thefinallythat closes the device never runs, and whatever the instrument holds — a vendor subprocess, a camera, a serial handle — is leaked, blocking the next start.Inside this context
SIGBREAKis handled by the interpreter’s ownSIGINThandler, so a stop raisesKeyboardInterruptin the main thread and takes the ordinary shutdown path. The previous handler is put back on the way out.Does nothing off Windows, and nothing outside the main thread of the main interpreter, where signal handlers cannot be installed at all.
- Return type:
collections.abc.Iterator[None]
- class plesty.lib.service.tcp_ip_server.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:
{ "type": "query | write | call | help | describe", "timeout": 3.0 }
Response payload:
{"status": "ok", "result": "..."}
Error response payload:
{"status": "error", "error": "message", "type": "ExceptionType"}
Initialize the device server.
- Parameters:
device – Synchronous device instance.
wrapper_cls – Async wrapper class (e.g., AsyncDeviceThread).
address – ZMQ bind address.
resources – Optional flat collection or nested dict tree of lockable resources.
- resource_manager
- client_metadata: dict[str, dict]
- device
- ctx
- socket
- host
- port
- _send_lock
- property address: str
Get the server’s bind address.
- Return type:
str
- property is_running: bool
Check if the server is currently running.
- Return type:
bool
- async _execute(coro, timeout=None)
Execute a coroutine with optional timeout.
- Parameters:
coro – Coroutine to execute.
timeout – Timeout in seconds.
- Returns:
Result of coroutine.
- Raises:
_BudgetExceeded – If timeout runs out. A
TimeoutErrorthe device raises itself propagates unchanged, message and all.
- static _client_id(identity: bytes) str
Return a stable string form of a ZeroMQ ROUTER identity frame.
- Parameters:
identity (bytes)
- Return type:
str
- _log_traffic(request_str: str, identity: bytes, response: dict, elapsed: float, delivered: bool) None
Write one record for a served request, for a monitor to read.
What was asked, of what, by whom, how it ended and how long it took — the metadata of the exchange and none of its payload. A result can be a spectrum or a stack of frames; logging it would dwarf the log and put measurement data somewhere it was never meant to go. The error type is recorded, not its message, for the same reason: the message can carry a path, an address or an instrument’s own words.
deliveredis false when the reply could not be routed — the client timed out and reconnected while the device was working. The operation happened and the outcome is real; only the answer was lost. That is the case a traffic pane most needs to show, because the caller saw a failure that did not occur.A
LIVENESS_TYPESrequest is recorded atDEBUGrather thanINFO, so a supervisor polling the server does not drown the calls that actually moved the instrument. It is recorded, not dropped.Never raises: a monitor that can break the thing it monitors is worse than no monitor. A malformed request, an unserializable field or a logging handler that throws costs the record, not the request.
- Parameters:
request_str (str) – The request as received.
identity (bytes) – ROUTER identity frame of the client.
response (dict) – What
_handle()answered.elapsed (float) – Seconds from receipt to the end of the send attempt.
delivered (bool) – Whether the reply reached the client.
- Return type:
None
- _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
disconnectis 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.
- Parameters:
identity (bytes) – ROUTER identity frame of the vanished client.
request_str (str) – The request being answered, as received.
exc (Exception) – The routing error.
- Return type:
None
- static _requested_target(request_str: str) str
Return what a request acted on (function or parameter), for the log.
- Parameters:
request_str (str)
- Return type:
str
- async _handle(request_str, identity: bytes)
Handle a single client request.
- Parameters:
request_str – JSON-encoded request string.
identity (bytes) – ZeroMQ ROUTER identity frame bytes.
- Returns:
JSON-serializable response.
- Return type:
dict
- _encode(response: dict, identity: bytes) bytes
Serialize a response, turning a failure into that client’s error.
_handlecatches everything a device call can raise and answers with an error dict — but the encoding happened inrun(), 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.
- Parameters:
response (dict) – The response
_handleproduced.identity (bytes) – 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.
- Return type:
bytes
- async _describe()
Introspect the device: its callable methods and its documented schema.
methodsis the original reply and stays first, so a client written against it keeps working.docis the device’sDeviceDocModelserialized — units, bounds, options and descriptions — which is what a caller needs to offer a parameter without inventing its constraints. A device whose model cannot be built still answers with its method list.- Returns:
{"methods": [...], "budgets": {...}, "doc": {...} | None}.- Return type:
dict
- static _doc_payload(device: Any) dict[str, Any] | None
Return the device’s serialized doc model, or
Nonewhen it has none.A describe must answer. Building the model walks the parameter and function registries and every
@expose_to_apisignature, so a module with a malformed annotation would otherwise take down the one request a supervisor uses to decide the device is alive.- Parameters:
device (Any) – The unwrapped device.
- Returns:
The doc model as JSON primitives, or
None.- Return type:
dict[str, Any] | None
- async _help()
- async run() None
Run the server loop indefinitely.
This method listens for incoming requests and processes them sequentially. Each request is handled asynchronously.
A stop is delivered as
KeyboardInterrupton every platform (seestop_on_break()), so thefinallybelow closes the device whichever signal the supervisor sent.- Return type:
None
- async _answer(identity: bytes, request: str) None
Handle one request and deliver its reply.
Runs as its own task so the accept loop keeps receiving. Sends are serialized on
_send_lock— the tasks share one ROUTER socket, and a reply is two frames that must not be interleaved with another’s.- Parameters:
identity (bytes) – ROUTER identity frame of the requesting client.
request (str) – The request, as received.
- Return type:
None
- async shutdown() None
Gracefully shutdown the server.
- Return type:
None