Accessing as a general capability for trays, covers, and doors

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.

I really like this idea of an “accessibility” pattern. This would work nicely with Resource.check_can_drop_resource_here

the capability so far is for machines that clearly move a plate in and out of a machine, like the cytation and spectramax etc.

I ignored just “open/close” like the imageXpress pico where a door opens but the plate does not move. Thinking about it more, I can be convinced we should also include that in this.

The purpose is to standardize open close and also to have state tracking so that arms can easily see if you can drop a resource to a given location at a specific time.

the star cover is actually not openable using code, it’s just a door. (the vantage door can be opened programmatically) but both of them are just doors for the machine , which is very different from a plate that moves in and out

the vspin door is an interesting one, the door does move up and down the plate actually does not move with it. Like on the cytation the plate moving out actually mechanically forces the door open so it’s the same action. Loading the plate actually requires a separate machine called Access2. I chose to not model this using this capability since it is technically a separate machine that has its own serial connection. But maybe we can think of a way to use this capability there if you think it is right. Undecided.

1 Like

maybe the capability should be called InNOut?

1 Like

Good point on check_can_drop_resource_here. Every access mechanism can report whether it’s open with the Accessing capability, and arms need that to know if they can drop a plate. That makes is_open() a nice universal method on the capability.

In that case the is_open() is the only ABC method. HasOpenClose and HasLocking will be the mixins.