Opentrons: feat: Safe-to-move signal for custom modules

Created on 9 Apr 2021  路  7Comments  路  Source: Opentrons/opentrons

Overview

I am developing a custom module for the OT-2 with its own Z-axis. I want a way to programmatically stop the OT-2 from moving to avoid collisions with this custom module. Just like how there's a run function in Python protocols, I was thinking there could be another function that gets run whenever the OT-2 wants to begin a movement. Only if this function returns True does the OT-2 actually move. There could also be a function to request that the custom module be put back in a safe position. The reason I am asking for this is because my OT-2 homed itself while my module was in its down position, which crashed it against the tip trash bucket, breaking some pieces on my module and stalling the motors.

I thought about different ways to implement this, and I think this callback function is a nice way to do it. An alternative is to set some attribute on protocol or something, but this could be set True and then the module disconnects for example.

I know this is a specialized feature. I don't expect anyone to get to it anytime soon. I would understand if it doesn't get implemented at all. In that case, If someone could point me in the direction of implementing this myself, I would very much appreciate it. That said, I do think that this would be a valuable feature to have for an open-source, expandable, hackable robot.

Implementation details

There should be a function, call it check_safe_to_move for now, that can optionally be defined in Python protocols, like the run function. If this function exists, it is called before every movement command. Only if the function returns true (or isn't defined) is the robot allowed to move.

Acceptance criteria

  • [ ] If a certain function is defined in a protocol, its return value is used to check if it is safe for the robot to move
  • [ ] (Optional) A second function defined in a protocol is used to request that all custom modules be moved to a safe state

I apologize if something like this already exists. I did not find anything similar in your documentation.

cpx feature-request hmg papi v2 workaround

Most helpful comment

Thanks for the request! I think this is interesting, especially as we think about how we might one day add first-class custom hardware support to the OT-2.

In the mean time, I think you could do a fairly serviceable version of this by wrapping your pipettes in a proxy object that can call your own is_safe_to_move function.

I've written this example below (and tested using a completely bogus is_safe_to_move implementation) with type annotations for clarity, but those type annotations are not necessary for it to work properly!

from typing import Any, Callable, Sequence
from opentrons.protocol_api import ProtocolContext, InstrumentContext
from opentrons.types import Mount

metadata = {
    'protocolName': 'Testosaur',
    'author': 'Opentrons <[email protected]>',
    'description': 'A variant on "Dinosaur" for testing',
    'source': 'Opentrons Repository',
    'apiLevel': '2.0',
}


class SafePipette:
    GAURDED_METHODS: Sequence[str] = (
        'aspirate',
        'dispense',
        'mix',
        'blow_out',
        'touch_tip',
        'air_gap',
        'return_tip',
        'pick_up_tip',
        'drop_tip',
        'home',
        'distribute',
        'consolidate',
        'transfer',
        'move_to',
    )

    def __init__(
        self,
        pipette_context: InstrumentContext,
        protocol_context: ProtocolContext,
        is_safe_to_move: Callable[[], bool],
    ) -> None:
        self._pipette_context = pipette_context
        self._protocol_context = protocol_context
        self._is_safe_to_move = is_safe_to_move

    def __getattr__(self, name: str) -> Any:
        safe_to_move = True

        if name in SafePipette.GAURDED_METHODS and not self._protocol_context.is_simulating():
            safe_to_move = self._is_safe_to_move()

        if not safe_to_move:
            self._protocol_context.pause(
                msg=(
                    "Pipette is not safe to move, "
                    "please remove obstruction before resuming"
                )
            )

        return getattr(self._pipette_context, name)


def run(ctx: ProtocolContext) -> None:
    count = 0

    def is_safe_to_move():
        """Fake implementation for demonstration purposes."""
        nonlocal count

        if count == 0:
            count += 1
            return False
        else:
            return True

    tip_rack = ctx.load_labware('opentrons_96_tiprack_300ul', 1)
    well_plate = ctx.load_labware('corning_96_wellplate_360ul_flat', 2)
    unsafe_right = ctx.load_instrument('p300_single', Mount.RIGHT, [tip_rack])

    right = SafePipette(
        pipette_context=unsafe_right,
        protocol_context=ctx,
        is_safe_to_move=is_safe_to_move,
    )

    right.pick_up_tip()
    right.aspirate(10, well_plate.wells()[0].bottom())
    right.dispense(10, well_plate.wells()[1].bottom())
    right.return_tip()

All 7 comments

This doesn't really answer your question, but here's how we currently handle this for the Thermocycler Module:

https://github.com/Opentrons/opentrons/blob/273be7a9b90eb2a488858efc3fe60992e24fd124/api/src/opentrons/protocol_api/instrument_context.py#L1110-L1112

If the Thermocycler lid is ~open~ closed, trying to move raises an exception.

Since we special-case ThermocyclerContext, there's no way to extend this to other modules without modifying our internal code.

Thanks for the request! I think this is interesting, especially as we think about how we might one day add first-class custom hardware support to the OT-2.

In the mean time, I think you could do a fairly serviceable version of this by wrapping your pipettes in a proxy object that can call your own is_safe_to_move function.

I've written this example below (and tested using a completely bogus is_safe_to_move implementation) with type annotations for clarity, but those type annotations are not necessary for it to work properly!

from typing import Any, Callable, Sequence
from opentrons.protocol_api import ProtocolContext, InstrumentContext
from opentrons.types import Mount

metadata = {
    'protocolName': 'Testosaur',
    'author': 'Opentrons <[email protected]>',
    'description': 'A variant on "Dinosaur" for testing',
    'source': 'Opentrons Repository',
    'apiLevel': '2.0',
}


class SafePipette:
    GAURDED_METHODS: Sequence[str] = (
        'aspirate',
        'dispense',
        'mix',
        'blow_out',
        'touch_tip',
        'air_gap',
        'return_tip',
        'pick_up_tip',
        'drop_tip',
        'home',
        'distribute',
        'consolidate',
        'transfer',
        'move_to',
    )

    def __init__(
        self,
        pipette_context: InstrumentContext,
        protocol_context: ProtocolContext,
        is_safe_to_move: Callable[[], bool],
    ) -> None:
        self._pipette_context = pipette_context
        self._protocol_context = protocol_context
        self._is_safe_to_move = is_safe_to_move

    def __getattr__(self, name: str) -> Any:
        safe_to_move = True

        if name in SafePipette.GAURDED_METHODS and not self._protocol_context.is_simulating():
            safe_to_move = self._is_safe_to_move()

        if not safe_to_move:
            self._protocol_context.pause(
                msg=(
                    "Pipette is not safe to move, "
                    "please remove obstruction before resuming"
                )
            )

        return getattr(self._pipette_context, name)


def run(ctx: ProtocolContext) -> None:
    count = 0

    def is_safe_to_move():
        """Fake implementation for demonstration purposes."""
        nonlocal count

        if count == 0:
            count += 1
            return False
        else:
            return True

    tip_rack = ctx.load_labware('opentrons_96_tiprack_300ul', 1)
    well_plate = ctx.load_labware('corning_96_wellplate_360ul_flat', 2)
    unsafe_right = ctx.load_instrument('p300_single', Mount.RIGHT, [tip_rack])

    right = SafePipette(
        pipette_context=unsafe_right,
        protocol_context=ctx,
        is_safe_to_move=is_safe_to_move,
    )

    right.pick_up_tip()
    right.aspirate(10, well_plate.wells()[0].bottom())
    right.dispense(10, well_plate.wells()[1].bottom())
    right.return_tip()

This is awesome! Thank you so much for such a detailed response so quickly. I will try to implement it this week. Should I let you know how it goes so this can serve as a documented working workaround?

Yeah would love to hear how this goes!

It looks like opentrons_execute homes the head even before running my run function. Is there a way to avoid this?

Edit: Actually it homes even before executing the protocol file.

That probably can't be avoided if you're running opentrons_execute.

You could try rewriting the Python script like:

# See https://docs.opentrons.com/v2/new_protocol_api.html#opentrons.execute.get_protocol_api
protocol = opentrons.execute.get_protocol_api(version='2.9')

protocol.load_instrument(...)
# etc.

Executing it like:

python my_script.py

I think you'll still need to insert a manual protocol.home() before doing anything that moves the robot. But you at least have a little more control over when that happens.

Thank you @SyntaxColoring. This is working for me. I did not know about that. Really good to know. If X and Y axes are homed with protocol.home(), maybe I have to wrap my protocol object as well, similar to the block given by @mcous?

Was this page helpful?
0 / 5 - 0 ratings