watlowlib.manager¶
WatlowManager — multi-port, multi-device orchestration with
ref-counted shared ProtocolClients, per-port locking, and one
protocol per port. See Streaming and
Design §6.
Public surface¶
watlowlib.manager ¶
Multi-controller orchestrator — :class:WatlowManager.
The manager coordinates many :class:~watlowlib.devices.controller.Controller
instances across one or more serial ports. Operations on different
physical ports run concurrently through :func:anyio.create_task_group;
operations on the same port serialise through that port's shared
:class:~watlowlib.protocol.base.ProtocolClient lock. The shared client
is address-agnostic — each managed controller's :class:Session
passes its own bus address into every execute call, so multi-drop
RS-485 segments with two or more devices work correctly.
Port identity is canonicalised before comparison so a controller
referenced via both /dev/ttyUSB0 and /dev/serial/by-id/...
(or COM3 and com3 on Windows) collapses to one client —
critical for the single-in-flight invariant. Pre-built
:class:Transport sources use the object's :func:id as the key so
caller-owned transports aren't accidentally shared.
Per-port protocol lock. The same RS-485 segment can only carry one
wire protocol at a time. The manager locks the port to the protocol
of the first device added; subsequent add(...) calls on that port
must use the same protocol or raise :class:WatlowConfigurationError.
Resource lifecycle goes through an internal tracking structure that
unwinds LIFO on :meth:close or __aexit__. Per-port clients are
ref-counted so the last :meth:remove on a shared port triggers the
transport close. Pre-built :class:Controller sources have no port
entry — the caller retains lifecycle ownership.
Design reference: docs/design.md §6.
DeviceResult
dataclass
¶
Per-device result container — value or error, never both.
:attr:protocol is populated from the controller's session so error
rows from the streaming layer can still record which protocol
produced the failure.
ErrorPolicy ¶
Bases: Enum
How the manager surfaces per-device failures.
Under :attr:RAISE, the manager collects every controller's result
and — if any call failed — raises an :class:ExceptionGroup
containing the per-device exceptions after the task group joins.
Under :attr:RETURN, each controller produces a
:class:DeviceResult and the caller inspects .error per entry.
WatlowManager ¶
Coordinator for many controllers across one or more serial ports.
Operations run concurrently across different physical ports (via
:func:anyio.create_task_group) and serialise on the same-port
client lock. Per-controller failures are surfaced per
:attr:error_policy:
- :attr:
ErrorPolicy.RAISE: the manager still collects results from every controller, then raises an :class:ExceptionGroupif any failed. - :attr:
ErrorPolicy.RETURN: per-name :class:DeviceResultcontainers carry.valueor.error.
Usage::
async with WatlowManager() as mgr:
await mgr.add("ctl1", "/dev/ttyUSB0", address=1)
await mgr.add("ctl2", "/dev/ttyUSB1", address=1)
samples = await mgr.poll(["process_value", "setpoint"])
Source code in src/watlowlib/manager.py
add
async
¶
add(
name,
source,
*,
protocol=ProtocolKind.STDBUS,
address=1,
serial_settings=None,
family=ControllerFamily.UNKNOWN,
)
Register and open a controller under name.
The source discriminates lifecycle ownership:
- :class:
Controller— pre-built (via :func:watlowlib.open_deviceoutside the manager). The manager only tracks the name mapping; it does not take lifecycle ownership. str— serial port path ("/dev/ttyUSB0","COM3"). The manager creates a transport, canonicalises the port key, and shares the transport + client across controllers on the same bus. Mixing Std Bus and Modbus on a shared physical port is refused; one serial link has one active protocol.- :class:
Transport— duck-typed transport. The manager builds a session against it but does not take transport ownership.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Unique manager-level identifier. |
required |
source
|
Controller | str | Transport
|
One of the three lifecycle shapes above. |
required |
protocol
|
ProtocolKind
|
Wire protocol ( |
STDBUS
|
address
|
int
|
Bus address. Std Bus accepts |
1
|
serial_settings
|
SerialSettings | None
|
Override default serial framing. Only
honoured when |
None
|
family
|
ControllerFamily
|
Best-known :class: |
UNKNOWN
|
Returns:
| Type | Description |
|---|---|
Controller
|
The opened :class: |
Raises:
| Type | Description |
|---|---|
WatlowValidationError
|
|
WatlowConfigurationError
|
protocol mismatches an existing
lock on the same port, or |
WatlowConnectionError
|
Manager is closed. |
Source code in src/watlowlib/manager.py
250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 | |
close
async
¶
Tear down every managed controller and port (LIFO).
Per-device teardown errors are collected; if any occurred,
they are raised after the close completes as an
:class:ExceptionGroup. This makes explicit await mgr.close()
calls fail loud on resource leaks. The async-CM exit path
swallows the errors instead so an in-flight exception still
wins (see :meth:__aexit__).
Source code in src/watlowlib/manager.py
execute_each
async
¶
Run op(controller) on every (or named) controller concurrently.
General-purpose dispatcher used for cross-device snapshots
(identify, read_pid, etc.) where each controller runs
the same coroutine and the result is keyed by name. Cross-port
runs concurrently; same-port serialises on the shared client
lock.
Under :attr:ErrorPolicy.RAISE the method still returns a
complete result mapping but re-raises an :class:ExceptionGroup
of every per-device error after the task group joins.
Source code in src/watlowlib/manager.py
get ¶
Return the controller registered under name.
Source code in src/watlowlib/manager.py
poll
async
¶
Poll every (or named) controller concurrently across ports.
Returns a flat list of :class:Sample — one per (device,
parameter, instance) read that succeeded. Failed reads are
dropped from the list and logged at WARN. Cross-port reads run
concurrently; same-port reads serialise on the shared client
lock.
This satisfies the :class:watlowlib.streaming.PollSource
Protocol so a manager can drive :func:watlowlib.streaming.record
directly.
Source code in src/watlowlib/manager.py
remove
async
¶
Unregister and close the controller named name.
If name was the last controller on a shared port, the
transport for that port is closed too. A pre-built
:class:Controller source is only dropped from the manager's
registry — the caller retains lifecycle ownership.