alicatlib.manager¶
Multi-device orchestrator with port-aware concurrent dispatch. See Logging and acquisition for recorder integration and Design §5.13 for the architecture.
alicatlib.manager ¶
Multi-device orchestrator — :class:AlicatManager.
The manager coordinates many :class:~alicatlib.devices.base.Device
instances across one or more serial ports. Operations across different
physical ports run concurrently through
:func:anyio.create_task_group; operations against the same port
serialise through that port's shared
:class:~alicatlib.protocol.client.AlicatProtocolClient lock.
Port identity is canonicalised before comparison so a device
referenced via both /dev/ttyUSB0 and
/dev/serial/by-id/usb-FTDI-… (or COM3 and com3 on
Windows) collapses to one client — critical for the single-in-flight
invariant. Pre-built :class:Transport / :class:AlicatProtocolClient
sources use the object's :func:id as the key so caller-owned
transports aren't accidentally shared.
Error handling is controlled by :class:ErrorPolicy:
- :attr:
ErrorPolicy.RAISE— manager collects all results, and if any device failed, raises an :class:ExceptionGroupat the end (never silently drops results). - :attr:
ErrorPolicy.RETURN— every device produces a :class:DeviceResultcontainer; callers inspect.error.
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
client's close.
Design reference: docs/design.md §5.13.
AlicatManager ¶
Coordinator for many devices 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-device failures are surfaced per
:attr:error_policy:
- :attr:
ErrorPolicy.RAISE: the manager still collects results from every device, then raises an :class:ExceptionGroupif any failed. - :attr:
ErrorPolicy.RETURN: the mapping's values carry :class:DeviceResultcontainers with.valueor.error.
Usage::
async with AlicatManager() as mgr:
await mgr.add("fuel", "/dev/ttyUSB0")
await mgr.add("air", "/dev/ttyUSB1")
frames = await mgr.poll()
Source code in src/alicatlib/manager.py
__aenter__
async
¶
__aexit__
async
¶
Close every managed device + port on exit.
add
async
¶
Register and open a device under name.
The source discriminates lifecycle ownership:
Device— pre-built (via :func: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 :class:~alicatlib.transport.serial.SerialTransportand :class:AlicatProtocolClient, canonicalises the port key, and reuses them across multi-device buses (RS-485).- :class:
Transport— duck-typed transport. The manager wraps it in a new client but does not take transport ownership (the caller keeps open/close responsibility). - :class:
AlicatProtocolClient— use as-is; the manager does not close it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
name
|
str
|
Unique manager-level identifier. Must not already exist on this manager. |
required |
source
|
Device | str | Transport | AlicatProtocolClient
|
One of the four lifecycle shapes above. |
required |
unit_id
|
str
|
Bus-level letter for the device. |
'A'
|
serial
|
SerialSettings | None
|
:class: |
None
|
timeout
|
float
|
Default command timeout passed through to
:func: |
0.5
|
Returns:
| Type | Description |
|---|---|
Device
|
The identified :class: |
Device
|
class: |
Raises:
| Type | Description |
|---|---|
AlicatValidationError
|
|
AlicatConnectionError
|
The manager is closed. |
Source code in src/alicatlib/manager.py
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 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 | |
close
async
¶
Tear down every managed device and port (LIFO).
Idempotent: safe to call from both :meth:__aexit__ and
explicit user code. Individual close failures are caught and
logged so one device's shutdown error doesn't strand the
others.
Source code in src/alicatlib/manager.py
execute
async
¶
Dispatch a per-device Command across the requested names.
requests_by_name chooses both which devices participate and
what arguments each gets — supporting the common case of
"same command, different setpoint per device".
Source code in src/alicatlib/manager.py
get ¶
Return the device registered under name (raises if unknown).
Source code in src/alicatlib/manager.py
poll
async
¶
Poll every (or named) device concurrently across ports.
Returns a mapping from device name to :class:DeviceResult
even under :attr:ErrorPolicy.RAISE — but under that policy,
any failed device's error is re-raised as an
:class:ExceptionGroup after all devices have completed.
Source code in src/alicatlib/manager.py
remove
async
¶
Unregister and close the device named name.
If name was the last device on a shared port, the
transport and client for that port are closed too. A
pre-built :class:Device source is only dropped from the
manager's registry — the caller retains lifecycle ownership.
Source code in src/alicatlib/manager.py
request
async
¶
Run :meth:Device.request across devices concurrently.
Every targeted device receives the same statistic list and
averaging window — mirroring the primer's DV semantics.
Source code in src/alicatlib/manager.py
DeviceResult
dataclass
¶
Per-device result container — value or error, never both.
The union is encoded as two optional fields (rather than an
Either / Result ADT) so mypy's narrowing on ok reads
cleanly at call sites without pattern matching.
Attributes:
| Name | Type | Description |
|---|---|---|
value |
T | None
|
The successful result, or |
error |
AlicatError | None
|
The captured :class: |
ErrorPolicy ¶
Bases: Enum
How the manager surfaces per-device failures.
Under :attr:RAISE, the manager collects every device'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 device produces a :class:DeviceResult
and the caller inspects .error per entry.
Design reference: docs/design.md §5.13.