From 825e2af7d477a6ae9399cdb43c11e819416a664b Mon Sep 17 00:00:00 2001 From: Laura Cox <31892318+Laura-Danielle@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:51:30 +0100 Subject: [PATCH] feat(api): RobotContext: Add gripper commands (#16752) --- .../protocols/gripper_controller.py | 3 + .../protocol_api/core/engine/robot.py | 8 ++ api/src/opentrons/protocol_api/core/robot.py | 8 ++ .../opentrons/protocol_api/robot_context.py | 8 +- .../commands/command_unions.py | 10 +++ .../commands/robot/__init__.py | 26 ++++++ .../commands/robot/close_gripper_jaw.py | 79 +++++++++++++++++ .../commands/robot/open_gripper_jaw.py | 77 ++++++++++++++++ .../commands/robot/test_close_gripper_jaw.py | 28 ++++++ .../commands/robot/test_open_gripper_jaw.py | 28 ++++++ shared-data/command/schemas/11.json | 88 ++++++++++++++++++- 11 files changed, 359 insertions(+), 4 deletions(-) create mode 100644 api/src/opentrons/protocol_engine/commands/robot/close_gripper_jaw.py create mode 100644 api/src/opentrons/protocol_engine/commands/robot/open_gripper_jaw.py create mode 100644 api/tests/opentrons/protocol_engine/commands/robot/test_close_gripper_jaw.py create mode 100644 api/tests/opentrons/protocol_engine/commands/robot/test_open_gripper_jaw.py diff --git a/api/src/opentrons/hardware_control/protocols/gripper_controller.py b/api/src/opentrons/hardware_control/protocols/gripper_controller.py index fc81325193c..1b81f4ab460 100644 --- a/api/src/opentrons/hardware_control/protocols/gripper_controller.py +++ b/api/src/opentrons/hardware_control/protocols/gripper_controller.py @@ -14,6 +14,9 @@ async def grip( ) -> None: ... + async def home_gripper_jaw(self) -> None: + ... + async def ungrip(self, force_newtons: Optional[float] = None) -> None: """Release gripped object. diff --git a/api/src/opentrons/protocol_api/core/engine/robot.py b/api/src/opentrons/protocol_api/core/engine/robot.py index df80917e091..0418afcbb95 100644 --- a/api/src/opentrons/protocol_api/core/engine/robot.py +++ b/api/src/opentrons/protocol_api/core/engine/robot.py @@ -129,3 +129,11 @@ def move_axes_relative(self, axis_map: AxisMapType, speed: Optional[float]) -> N self._engine_client.execute_command( cmd.robot.MoveAxesRelativeParams(axis_map=axis_engine_map, speed=speed) ) + + def release_grip(self) -> None: + self._engine_client.execute_command(cmd.robot.openGripperJawParams()) + + def close_gripper(self, force: Optional[float] = None) -> None: + self._engine_client.execute_command( + cmd.robot.closeGripperJawParams(force=force) + ) diff --git a/api/src/opentrons/protocol_api/core/robot.py b/api/src/opentrons/protocol_api/core/robot.py index 95def3e17f3..f65ddbbd7bb 100644 --- a/api/src/opentrons/protocol_api/core/robot.py +++ b/api/src/opentrons/protocol_api/core/robot.py @@ -41,3 +41,11 @@ def move_axes_to( @abstractmethod def move_axes_relative(self, axis_map: AxisMapType, speed: Optional[float]) -> None: ... + + @abstractmethod + def release_grip(self) -> None: + ... + + @abstractmethod + def close_gripper(self, force: Optional[float] = None) -> None: + ... diff --git a/api/src/opentrons/protocol_api/robot_context.py b/api/src/opentrons/protocol_api/robot_context.py index 5b0e578f9bb..df14b8bb7c5 100644 --- a/api/src/opentrons/protocol_api/robot_context.py +++ b/api/src/opentrons/protocol_api/robot_context.py @@ -144,11 +144,13 @@ def move_axes_relative( ) self._core.move_axes_relative(axis_map, speed) - def close_gripper_jaw(self, force: float) -> None: - raise NotImplementedError() + def close_gripper_jaw(self, force: Optional[float] = None) -> None: + """Command the gripper closed with some force.""" + self._core.close_gripper(force) def open_gripper_jaw(self) -> None: - raise NotImplementedError() + """Command the gripper open.""" + self._core.release_grip() def axis_coordinates_for( self, diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index c33f55e2e01..9c548fa8045 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -416,6 +416,8 @@ robot.MoveTo, robot.MoveAxesRelative, robot.MoveAxesTo, + robot.openGripperJaw, + robot.closeGripperJaw, ], Field(discriminator="commandType"), ] @@ -499,6 +501,8 @@ robot.MoveAxesRelativeParams, robot.MoveAxesToParams, robot.MoveToParams, + robot.openGripperJawParams, + robot.closeGripperJawParams, ] CommandType = Union[ @@ -580,6 +584,8 @@ robot.MoveAxesRelativeCommandType, robot.MoveAxesToCommandType, robot.MoveToCommandType, + robot.openGripperJawCommandType, + robot.closeGripperJawCommandType, ] CommandCreate = Annotated[ @@ -662,6 +668,8 @@ robot.MoveAxesRelativeCreate, robot.MoveAxesToCreate, robot.MoveToCreate, + robot.openGripperJawCreate, + robot.closeGripperJawCreate, ], Field(discriminator="commandType"), ] @@ -745,6 +753,8 @@ robot.MoveAxesRelativeResult, robot.MoveAxesToResult, robot.MoveToResult, + robot.openGripperJawResult, + robot.closeGripperJawResult, ] diff --git a/api/src/opentrons/protocol_engine/commands/robot/__init__.py b/api/src/opentrons/protocol_engine/commands/robot/__init__.py index 5d5fc691e5f..048fecd09fe 100644 --- a/api/src/opentrons/protocol_engine/commands/robot/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/robot/__init__.py @@ -21,6 +21,20 @@ MoveAxesRelativeResult, MoveAxesRelativeCommandType, ) +from .open_gripper_jaw import ( + openGripperJaw, + openGripperJawCreate, + openGripperJawParams, + openGripperJawResult, + openGripperJawCommandType, +) +from .close_gripper_jaw import ( + closeGripperJaw, + closeGripperJawCreate, + closeGripperJawParams, + closeGripperJawResult, + closeGripperJawCommandType, +) __all__ = [ # robot/moveTo @@ -41,4 +55,16 @@ "MoveAxesRelativeParams", "MoveAxesRelativeResult", "MoveAxesRelativeCommandType", + # robot/openGripperJaw + "openGripperJaw", + "openGripperJawCreate", + "openGripperJawParams", + "openGripperJawResult", + "openGripperJawCommandType", + # robot/closeGripperJaw + "closeGripperJaw", + "closeGripperJawCreate", + "closeGripperJawParams", + "closeGripperJawResult", + "closeGripperJawCommandType", ] diff --git a/api/src/opentrons/protocol_engine/commands/robot/close_gripper_jaw.py b/api/src/opentrons/protocol_engine/commands/robot/close_gripper_jaw.py new file mode 100644 index 00000000000..965c6d2ec72 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/robot/close_gripper_jaw.py @@ -0,0 +1,79 @@ +"""Command models for opening a gripper jaw.""" +from __future__ import annotations +from typing import Literal, Type, Optional +from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.resources import ensure_ot3_hardware + +from pydantic import BaseModel, Field + +from ..command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) +from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence + + +closeGripperJawCommandType = Literal["robot/closeGripperJaw"] + + +class closeGripperJawParams(BaseModel): + """Payload required to close a gripper.""" + + force: Optional[float] = Field( + default=None, + description="The force the gripper should use to hold the jaws, falls to default if none is provided.", + ) + + +class closeGripperJawResult(BaseModel): + """Result data from the execution of a closeGripperJaw command.""" + + pass + + +class closeGripperJawImplementation( + AbstractCommandImpl[closeGripperJawParams, SuccessData[closeGripperJawResult]] +): + """closeGripperJaw command implementation.""" + + def __init__( + self, + hardware_api: HardwareControlAPI, + **kwargs: object, + ) -> None: + self._hardware_api = hardware_api + + async def execute( + self, params: closeGripperJawParams + ) -> SuccessData[closeGripperJawResult]: + """Release the gripper.""" + ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) + await ot3_hardware_api.grip(force_newtons=params.force) + return SuccessData( + public=closeGripperJawResult(), + ) + + +class closeGripperJaw( + BaseCommand[closeGripperJawParams, closeGripperJawResult, ErrorOccurrence] +): + """closeGripperJaw command model.""" + + commandType: closeGripperJawCommandType = "robot/closeGripperJaw" + params: closeGripperJawParams + result: Optional[closeGripperJawResult] + + _ImplementationCls: Type[ + closeGripperJawImplementation + ] = closeGripperJawImplementation + + +class closeGripperJawCreate(BaseCommandCreate[closeGripperJawParams]): + """closeGripperJaw command request model.""" + + commandType: closeGripperJawCommandType = "robot/closeGripperJaw" + params: closeGripperJawParams + + _CommandCls: Type[closeGripperJaw] = closeGripperJaw diff --git a/api/src/opentrons/protocol_engine/commands/robot/open_gripper_jaw.py b/api/src/opentrons/protocol_engine/commands/robot/open_gripper_jaw.py new file mode 100644 index 00000000000..22aa1debd42 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/robot/open_gripper_jaw.py @@ -0,0 +1,77 @@ +"""Command models for opening a gripper jaw.""" +from __future__ import annotations +from typing import Literal, Type, Optional +from opentrons.hardware_control import HardwareControlAPI +from opentrons.protocol_engine.resources import ensure_ot3_hardware + +from pydantic import BaseModel + +from ..command import ( + AbstractCommandImpl, + BaseCommand, + BaseCommandCreate, + SuccessData, +) +from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence + + +openGripperJawCommandType = Literal["robot/openGripperJaw"] + + +class openGripperJawParams(BaseModel): + """Payload required to release a gripper.""" + + pass + + +class openGripperJawResult(BaseModel): + """Result data from the execution of a openGripperJaw command.""" + + pass + + +class openGripperJawImplementation( + AbstractCommandImpl[openGripperJawParams, SuccessData[openGripperJawResult]] +): + """openGripperJaw command implementation.""" + + def __init__( + self, + hardware_api: HardwareControlAPI, + **kwargs: object, + ) -> None: + self._hardware_api = hardware_api + + async def execute( + self, params: openGripperJawParams + ) -> SuccessData[openGripperJawResult]: + """Release the gripper.""" + ot3_hardware_api = ensure_ot3_hardware(self._hardware_api) + + await ot3_hardware_api.home_gripper_jaw() + return SuccessData( + public=openGripperJawResult(), + ) + + +class openGripperJaw( + BaseCommand[openGripperJawParams, openGripperJawResult, ErrorOccurrence] +): + """openGripperJaw command model.""" + + commandType: openGripperJawCommandType = "robot/openGripperJaw" + params: openGripperJawParams + result: Optional[openGripperJawResult] + + _ImplementationCls: Type[ + openGripperJawImplementation + ] = openGripperJawImplementation + + +class openGripperJawCreate(BaseCommandCreate[openGripperJawParams]): + """openGripperJaw command request model.""" + + commandType: openGripperJawCommandType = "robot/openGripperJaw" + params: openGripperJawParams + + _CommandCls: Type[openGripperJaw] = openGripperJaw diff --git a/api/tests/opentrons/protocol_engine/commands/robot/test_close_gripper_jaw.py b/api/tests/opentrons/protocol_engine/commands/robot/test_close_gripper_jaw.py new file mode 100644 index 00000000000..c5ccd4bf48d --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/robot/test_close_gripper_jaw.py @@ -0,0 +1,28 @@ +"""Test robot.open-gripper-jaw commands.""" +from decoy import Decoy + +from opentrons.hardware_control import OT3HardwareControlAPI + +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.robot.close_gripper_jaw import ( + closeGripperJawParams, + closeGripperJawResult, + closeGripperJawImplementation, +) + + +async def test_close_gripper_jaw_implementation( + decoy: Decoy, + ot3_hardware_api: OT3HardwareControlAPI, +) -> None: + """Test the `robot.closeGripperJaw` implementation.""" + subject = closeGripperJawImplementation( + hardware_api=ot3_hardware_api, + ) + + params = closeGripperJawParams(force=10) + + result = await subject.execute(params=params) + + assert result == SuccessData(public=closeGripperJawResult()) + decoy.verify(await ot3_hardware_api.grip(force_newtons=10)) diff --git a/api/tests/opentrons/protocol_engine/commands/robot/test_open_gripper_jaw.py b/api/tests/opentrons/protocol_engine/commands/robot/test_open_gripper_jaw.py new file mode 100644 index 00000000000..6ded7932963 --- /dev/null +++ b/api/tests/opentrons/protocol_engine/commands/robot/test_open_gripper_jaw.py @@ -0,0 +1,28 @@ +"""Test robot.open-gripper-jaw commands.""" +from decoy import Decoy + +from opentrons.hardware_control import OT3HardwareControlAPI + +from opentrons.protocol_engine.commands.command import SuccessData +from opentrons.protocol_engine.commands.robot.open_gripper_jaw import ( + openGripperJawParams, + openGripperJawResult, + openGripperJawImplementation, +) + + +async def test_open_gripper_jaw_implementation( + decoy: Decoy, + ot3_hardware_api: OT3HardwareControlAPI, +) -> None: + """Test the `robot.openGripperJaw` implementation.""" + subject = openGripperJawImplementation( + hardware_api=ot3_hardware_api, + ) + + params = openGripperJawParams() + + result = await subject.execute(params=params) + + assert result == SuccessData(public=openGripperJawResult()) + decoy.verify(await ot3_hardware_api.home_gripper_jaw()) diff --git a/shared-data/command/schemas/11.json b/shared-data/command/schemas/11.json index 432e8a08231..38a39ea7902 100644 --- a/shared-data/command/schemas/11.json +++ b/shared-data/command/schemas/11.json @@ -82,7 +82,9 @@ "unsafe/placeLabware": "#/definitions/UnsafePlaceLabwareCreate", "robot/moveAxesRelative": "#/definitions/MoveAxesRelativeCreate", "robot/moveAxesTo": "#/definitions/MoveAxesToCreate", - "robot/moveTo": "#/definitions/MoveToCreate" + "robot/moveTo": "#/definitions/MoveToCreate", + "robot/openGripperJaw": "#/definitions/openGripperJawCreate", + "robot/closeGripperJaw": "#/definitions/closeGripperJawCreate" } }, "oneOf": [ @@ -319,6 +321,12 @@ }, { "$ref": "#/definitions/MoveToCreate" + }, + { + "$ref": "#/definitions/openGripperJawCreate" + }, + { + "$ref": "#/definitions/closeGripperJawCreate" } ], "definitions": { @@ -6111,6 +6119,84 @@ } }, "required": ["params"] + }, + "openGripperJawParams": { + "title": "openGripperJawParams", + "description": "Payload required to release a gripper.", + "type": "object", + "properties": {} + }, + "openGripperJawCreate": { + "title": "openGripperJawCreate", + "description": "openGripperJaw command request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "robot/openGripperJaw", + "enum": ["robot/openGripperJaw"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/openGripperJawParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] + }, + "closeGripperJawParams": { + "title": "closeGripperJawParams", + "description": "Payload required to close a gripper.", + "type": "object", + "properties": { + "force": { + "title": "Force", + "description": "The force the gripper should use to hold the jaws, falls to default if none is provided.", + "type": "number" + } + } + }, + "closeGripperJawCreate": { + "title": "closeGripperJawCreate", + "description": "closeGripperJaw command request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "robot/closeGripperJaw", + "enum": ["robot/closeGripperJaw"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/closeGripperJawParams" + }, + "intent": { + "description": "The reason the command was added. If not specified or `protocol`, the command will be treated as part of the protocol run itself, and added to the end of the existing command queue.\n\nIf `setup`, the command will be treated as part of run setup. A setup command may only be enqueued if the run has not started.\n\nUse setup commands for activities like pre-run calibration checks and module setup, like pre-heating.", + "allOf": [ + { + "$ref": "#/definitions/CommandIntent" + } + ] + }, + "key": { + "title": "Key", + "description": "A key value, unique in this run, that can be used to track the same logical command across multiple runs of the same protocol. If a value is not provided, one will be generated.", + "type": "string" + } + }, + "required": ["params"] } }, "$id": "opentronsCommandSchemaV11",