pytweezer.servers.device_server module

Generic device RPC-server launcher.

Every device in CONFIG["Devices"] runs a sipyco RPC server. Rather than give each driver module its own argparse + config-reading + simple_server_loop boilerplate, this module provides a single launcher:

  • pytweezer-device <device_name> (console script) — start the server for the named device from the command line.

  • The device manager launches the same thing: each device’s script in the config points here, so ProcessTile runs python device_server.py <name>.

A device’s config entry points directly at its backend class, so adding a device or a whole new driver type means editing only config.py. A plain device entry carries:

  • "class": the real backend, as a "module.path:ClassName" string.

  • "sim_class" (optional): the simulated/dummy backend, same form. When the entry sets "simulate": True this class is used instead of "class". If it is omitted, a hardware-free stand-in is generated from "class" automatically (see default_simulated()), so a device can always be simulated; supply "sim_class" only for an interesting fake.

  • "teardown" (optional): the name of a zero-argument method to call when the server stops (e.g. "close", "disconnect").

  • driver-specific keyword arguments (stream_name, sdk_dll, …).

build_spec() imports the chosen class and constructs it automatically: it reads __init__’s signature and passes the config entries whose keys match parameter names, so no per-driver “unpack the config into the constructor” glue is needed. Anything a backend needs beyond receiving those values — resolving a path, starting a helper process, connecting to hardware — it does in its own __init__ from the arguments it is given (e.g. the MotMaster interface takes a config-file name, resolves it, ensures the app is running, and connects). Classes are named as strings and imported lazily — only when actually built — so importing this launcher never pulls in a hardware library that may be absent (e.g. pylablib for the ImagEM).

A composite device (any entry with a "devices" sub-dict) serves several devices from one process and one port, one RPC target each, optionally alongside a coordinator target — a class named by "coordinator" (again "module:Class") that drives those backends through direct Python calls rather than RPC. That is how a camera-to-SLM step avoids serializing a frame. Sub-devices stay individually addressable: get_device("RbHamCam").

class pytweezer.servers.device_server.DeviceAddress(name, conf, owner_name, owner_conf, target_name=None)[source]

Bases: object

Where a named device lives and which RPC target serves it.

For a plain device the device is its own server, so owner_conf is conf and target_name is None (the server has one target; AutoTarget finds it). For a composite’s sub-device, owner_conf is the composite’s entry — that is where host/port live — and target_name selects the sub-device.

Parameters:
conf: dict
property is_sub_device
name: str
owner_conf: dict
owner_name: str
target_name: str | None = None
class pytweezer.servers.device_server.DeviceServerSpec(target_name=None, target=None, description='', teardown=None, targets=None, failed=())[source]

Bases: object

Everything run_device_server() needs to serve one device.

A spec carries either a single target (target_name/target) or several (targets, a {target_name: target} dict — see _make_composite()), never both. Either way targets is the normalized form passed to simple_server_loop. teardown (if given) is called in a finally after the loop ends, for backends that need an explicit disconnect. failed names the composite sub-devices that could not be built and are therefore absent from targets.

Parameters:
description: str = ''
failed: tuple = ()
target: object | None = None
target_name: str | None = None
targets: dict[str, object] | None = None
teardown: Callable[[], None] | None = None
pytweezer.servers.device_server.build_spec(name, conf=None)[source]

Return the DeviceServerSpec for the device named name.

conf defaults to that device’s CONFIG["Devices"] entry; pass an explicit dict to override. An entry with a "devices" sub-dict is a composite (see _make_composite()); otherwise the backend named by the config’s "class"/"sim_class" is imported and constructed automatically.

pytweezer.servers.device_server.composite_target_name(device_name)[source]

RPC target name a composite serves a sub-device under.

Sub-devices are named like any other device ("Rb Feedback Cam"), but sipyco target names cannot contain whitespace, so the display name is folded down. Clients never type this — resolve_address() supplies it.

pytweezer.servers.device_server.coordinator_target_name(conf)[source]

RPC target name a composite serves its coordinator under.

Defaults to "coordinator"; override per-rig with the config key of the same name. This is the target get_device() binds when asked for the composite’s own name, so its absence from a running server means the coordinator stood down.

pytweezer.servers.device_server.device_index()[source]

Return {normalized_name: DeviceAddress} over every addressable device.

Composite sub-devices are flattened into the same namespace as top-level devices, which is what lets get_device("Rb Feedback Cam") work without the caller knowing that camera happens to share a process with a DAC. A composite’s own name resolves to its coordinator target.

Raises KeyError if two devices share a name (case- and whitespace-insensitively), since such a name could not be resolved unambiguously.

pytweezer.servers.device_server.main()[source]
pytweezer.servers.device_server.resolve_address(name)[source]

Return the DeviceAddress for name, matched leniently.

Accepts any whitespace-/case-insensitive spelling of a top-level device, a composite, or a composite’s sub-device. Raises KeyError listing every addressable device if nothing matches.

pytweezer.servers.device_server.resolve_device(name)[source]

Return (canonical_name, conf) for a launchable device.

Only top-level CONFIG["Devices"] entries are launchable — a composite’s sub-device has no server of its own. Matches the config key exactly first, then falls back to a whitespace-/case-insensitive match so command-line callers can pass RbHamCam or rb hamcam for "Rb HamCam".

pytweezer.servers.device_server.run_device_server(name, host=None, port=None, allow_parallel=None)[source]

Build and serve the RPC server for the device named name (blocks).

allow_parallel (config key of the same name, default False) drops the lock sipyco holds across each RPC call. It has no effect while every target method is a plain ``def``: Server._process_action awaits a method’s result only when it is a coroutine, and an uncontended asyncio.Lock.acquire() does not suspend — so with synchronous methods there is no suspension point between acquiring and releasing that lock, and nothing can contend for it. What serializes calls today is the single-threaded event loop, not the lock. The flag becomes meaningful only once a target method is async def (which then also wants await asyncio.to_thread(...) for its blocking work, plus its own per-backend lock, since the sipyco lock currently supplies mutual exclusion for free).