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
scriptin the config points here, soProcessTilerunspython 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": Truethis class is used instead of"class". If it is omitted, a hardware-free stand-in is generated from"class"automatically (seedefault_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:
objectWhere a named device lives and which RPC target serves it.
For a plain device the device is its own server, so
owner_conf is confandtarget_nameisNone(the server has one target;AutoTargetfinds it). For a composite’s sub-device,owner_confis the composite’s entry — that is wherehost/portlive — andtarget_nameselects the sub-device.- property is_sub_device¶
- class pytweezer.servers.device_server.DeviceServerSpec(target_name=None, target=None, description='', teardown=None, targets=None, failed=())[source]¶
Bases:
objectEverything
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 waytargetsis the normalized form passed tosimple_server_loop.teardown(if given) is called in afinallyafter the loop ends, for backends that need an explicit disconnect.failednames the composite sub-devices that could not be built and are therefore absent fromtargets.- Parameters:
- pytweezer.servers.device_server.build_spec(name, conf=None)[source]¶
Return the
DeviceServerSpecfor the device namedname.confdefaults to that device’sCONFIG["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 targetget_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
KeyErrorif two devices share a name (case- and whitespace-insensitively), since such a name could not be resolved unambiguously.
- pytweezer.servers.device_server.resolve_address(name)[source]¶
Return the
DeviceAddressforname, matched leniently.Accepts any whitespace-/case-insensitive spelling of a top-level device, a composite, or a composite’s sub-device. Raises
KeyErrorlisting 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 passRbHamCamorrb hamcamfor"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, defaultFalse) drops the lock sipyco holds across each RPC call. It has no effect while every target method is a plain ``def``:Server._process_actionawaits a method’s result only when it is a coroutine, and an uncontendedasyncio.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 isasync def(which then also wantsawait asyncio.to_thread(...)for its blocking work, plus its own per-backend lock, since the sipyco lock currently supplies mutual exclusion for free).