# Remote Access `plesty.lib.service` puts a device on the network. A device class is written once, synchronously, against the instrument in front of it; the service tier serves that same object over ZeroMQ so an experiment on another machine calls it with the syntax it would use locally. This is the top half of the six device layers — the part nobody writes per device. ```text DeviceTCPIPClient → ZMQ → DeviceTCPIPServer → async wrapper → your device ``` ## Serving a Device `build_server(device, fixed_threading=True, address="tcp://*:5555")` wraps a synchronous device and runs it behind a ROUTER socket that accepts several clients at once. ```python from plesty.lib.service import build_server from my_device import MyPowerMeter device = MyPowerMeter("USB0::0x1313::0x8078::P0000001::INSTR") server = build_server(device, address="tcp://*:5555") server.run() ``` `run()` serves until `shutdown()`; `address` and `is_running` report what the server is doing. **`fixed_threading` is the argument that matters.** It defaults to `True`, which pins every device call to one dedicated worker thread (`AsyncDeviceThread`). This became the default after the 2026-08 field round: instrument drivers turned out to have thread *affinity*, not merely thread-unsafety, and migrating between pool threads starved a .NET completion callback and lost ZMQ messages under real latency. `False` selects `AsyncWrapperSafe`, which serializes calls but runs them on rotating executor threads — correct for a pure-Python driver, wrong for a vendor DLL. ## Using a Remote Device `build_client(address, timeout=5000, resources=None, client_id=None)` returns a client that discovers the served methods on connect and binds them as attributes, so a remote call reads like a local one. ```python from plesty.lib.service import build_client with build_client("tcp://192.168.1.40:5555") as pm: pm.connect() pm.write("wavelength", 1064.0) print(pm.query("wavelength")) print(pm.read_power()) # a served operation, bound on connect ``` `query` / `write` reach the parameter system, `call(name, **kwargs)` reaches any served operation, and `describe()` returns the device's `doc_model()` — the same structure the docs generator renders, so a client can list what it may call without importing the device module. `timeout` is in **milliseconds**; `set_default_timeout()` changes it for later calls. The client presents no `get_config_list()`. Tools that need the key list — the [client field test](device/test_helper.md#9-client_field_test--the-gates-over-the-wire), for one — are told the keys explicitly rather than discovering them. ## Multi-Client Access Control One server can hold several clients, which means two of them can reach for the same physical channel. `ResourceManager` arbitrates. The lockable resources belong to the **device**, not to the server: `build_server` reads them from `device._resources`, which the device fills by calling `register_resources()` — usually in its own `init`. A client then names what it needs when it connects. ```python device.register_resources( {"Dev1": {"port0": ["line0", "line1"], "port1": ["line0", "line1"]}} ) server = build_server(device, address="tcp://*:5555") # one client takes a whole subtree, another takes a single line scanner = build_client(address, resources="Dev1/port0") shutter = build_client(address, resources=["Dev1/port1/line0"]) ``` A request is a string (`"Dev1/port0"` claims every leaf beneath it, `"Dev1/port0/line0"` claims one), a flat collection, or a nested dictionary. The manager grants what is free and refuses what is held; `client_resources()` reports who holds what, and disconnecting releases everything that client took. `connection_info` on the client carries the outcome of its own request. A device decides what a client may do with its allocation by overriding `_user_permission_check(user_resources, key)` on the device class — the default is permissive. ## Composite Devices `CompositeDevice` consumes clients built here, which is how a multi-instrument rig becomes one object with resilient remote calls — a timeout rebuilds the client and retries before it propagates. See [Composite device](device/composite_device.md). ## Related Pages 1. [Base device](device/base_device.md) — the object being served 2. [Composite device](device/composite_device.md) — grouping local and remote devices 3. [Test helpers](device/test_helper.md) — `client_field_test` exercises this whole path 4. [Simulation](sim.md) — `DemoDevice` gives you something to serve without an instrument