The new LoadingTray capability (ef1764f) adds open()/close() for plate reader trays. I think this could be generalized into an Accessing capability that works for trays, covers, lids, and doors, the attribute name on the device communicates what’s being opened:
await cytation.tray.open() # tray slides out
await star.cover.open() # cover lifts
await vspin.door.open() # centrifuge door opens
Same ABC, same capability, different attribute name.
Backend ABC: open, close, is_open. Locking as a mixin on the backend, same pattern as HasContinuousShaking for shakers and HasJoints for arms, co-located in the same file, scoped to the capability:
class CanAccess(CapabilityBackend, ABC):
async def open(self, backend_params=None) -> None: ...
async def close(self, backend_params=None) -> None: ...
async def is_open(self) -> bool: ...
class HasLocking(ABC):
"""Mixin for Accessing backends that support locking."""
async def lock(self) -> None: ...
async def unlock(self) -> None: ...
class STARCoverBackend(CanAccess, HasLocking): # cover with locking
...
class VSpinDoorBackend(CanAccess, HasLocking): # centrifuge door with locking
...
class BioTekAccessingBackend(CanAccess): # tray, no locking
...
class Accessing(Capability):
async def open(self, backend_params=None):
await self.backend.open(backend_params=backend_params)
async def lock(self):
if not isinstance(self.backend, HasLocking):
raise NotImplementedError(
f"{type(self.backend).__name__} does not support locking."
)
await self.backend.lock()
async def _on_stop(self):
"""Safe shutdown: unlock if locked, then close."""
if self._setup_finished:
if isinstance(self.backend, HasLocking):
try:
if await self.backend.is_locked():
await self.backend.unlock()
except Exception:
pass
try:
await self.backend.close()
except Exception:
pass
await super()._on_stop()
Mixin on the backend, not the device. The device stays clean:
class Cytation1(Resource, Device):
async def setup(self):
self.tray = Accessing(backend=BioTekAccessingBackend(self.driver))
self.microscopy = Microscopy(backend=CytationMicroscopyBackend(self.driver))
self._capabilities = [self.tray, self.microscopy]
I can work on a Cytation PR for this if the direction makes sense.