From 369bb401b6354a8a66d8dca611e2df6d9e6b6be7 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Fri, 18 Oct 2024 11:57:22 -0400 Subject: [PATCH 01/20] save --- .../opentrons/motion_planning/waypoints.py | 32 ++++ .../commands/command_unions.py | 5 + .../commands/unsafe/__init__.py | 15 ++ .../commands/unsafe/unsafe_place_labware.py | 174 ++++++++++++++++++ .../modules/hooks/useDropPlateReaderLid.ts | 47 +++++ .../robot_server/robot/control/router.py | 45 ++++- .../robot_server/runs/router/base_router.py | 15 +- robot-server/robot_server/runs/run_models.py | 12 ++ 8 files changed, 343 insertions(+), 2 deletions(-) create mode 100644 api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py create mode 100644 app/src/resources/modules/hooks/useDropPlateReaderLid.ts diff --git a/api/src/opentrons/motion_planning/waypoints.py b/api/src/opentrons/motion_planning/waypoints.py index b9c62114215..bcc56ad7eda 100644 --- a/api/src/opentrons/motion_planning/waypoints.py +++ b/api/src/opentrons/motion_planning/waypoints.py @@ -181,3 +181,35 @@ def get_gripper_labware_movement_waypoints( ) ) return waypoints_with_jaw_status + + +def get_gripper_labware_placement_waypoints( + to_labware_center: Point, + gripper_home_z: float, + drop_offset: Optional[Point], +) -> List[GripperMovementWaypointsWithJawStatus]: + """Get waypoints for placing labware using a gripper.""" + drop_offset = drop_offset or Point() + + drop_location = to_labware_center + Point( + drop_offset.x, drop_offset.y, drop_offset.z + ) + + post_drop_home_pos = Point(drop_location.x, drop_location.y, gripper_home_z) + + return [ + GripperMovementWaypointsWithJawStatus( + position=Point(drop_location.x, drop_location.y, gripper_home_z), + jaw_open=False, + dropping=False, + ), + GripperMovementWaypointsWithJawStatus( + position=drop_location, jaw_open=False, dropping=False + ), + # Gripper ungrips here + GripperMovementWaypointsWithJawStatus( + position=post_drop_home_pos, + jaw_open=True, + dropping=True, + ), + ] diff --git a/api/src/opentrons/protocol_engine/commands/command_unions.py b/api/src/opentrons/protocol_engine/commands/command_unions.py index 80df6710f8b..b239059b8d0 100644 --- a/api/src/opentrons/protocol_engine/commands/command_unions.py +++ b/api/src/opentrons/protocol_engine/commands/command_unions.py @@ -393,6 +393,7 @@ unsafe.UpdatePositionEstimators, unsafe.UnsafeEngageAxes, unsafe.UnsafeUngripLabware, + unsafe.UnsafePlaceLabware, ], Field(discriminator="commandType"), ] @@ -470,6 +471,7 @@ unsafe.UpdatePositionEstimatorsParams, unsafe.UnsafeEngageAxesParams, unsafe.UnsafeUngripLabwareParams, + unsafe.UnsafePlaceLabwareParams, ] CommandType = Union[ @@ -545,6 +547,7 @@ unsafe.UpdatePositionEstimatorsCommandType, unsafe.UnsafeEngageAxesCommandType, unsafe.UnsafeUngripLabwareCommandType, + unsafe.UnsafePlaceLabwareCommandType, ] CommandCreate = Annotated[ @@ -621,6 +624,7 @@ unsafe.UpdatePositionEstimatorsCreate, unsafe.UnsafeEngageAxesCreate, unsafe.UnsafeUngripLabwareCreate, + unsafe.UnsafePlaceLabwareCreate, ], Field(discriminator="commandType"), ] @@ -698,6 +702,7 @@ unsafe.UpdatePositionEstimatorsResult, unsafe.UnsafeEngageAxesResult, unsafe.UnsafeUngripLabwareResult, + unsafe.UnsafePlaceLabwareResult, ] # todo(mm, 2024-06-12): Ideally, command return types would have specific diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py b/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py index 72698a3b0f2..eb138d89914 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/__init__.py @@ -40,6 +40,15 @@ ) +from .unsafe_place_labware import ( + UnsafePlaceLabwareCommandType, + UnsafePlaceLabwareParams, + UnsafePlaceLabwareResult, + UnsafePlaceLabware, + UnsafePlaceLabwareCreate, +) + + __all__ = [ # Unsafe blow-out-in-place command models "UnsafeBlowOutInPlaceCommandType", @@ -71,4 +80,10 @@ "UnsafeUngripLabwareResult", "UnsafeUngripLabware", "UnsafeUngripLabwareCreate", + # Unsafe place labware + "UnsafePlaceLabwareCommandType", + "UnsafePlaceLabwareParams", + "UnsafePlaceLabwareResult", + "UnsafePlaceLabware", + "UnsafePlaceLabwareCreate", ] diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py new file mode 100644 index 00000000000..875c1bb0799 --- /dev/null +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -0,0 +1,174 @@ +"""Place labware with gripper, result, and implementaiton.""" + +from __future__ import annotations +from pydantic import BaseModel, Field +from typing import TYPE_CHECKING, Optional, Type +from typing_extensions import Literal +from logging import getLogger + +from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +from ...errors.error_occurrence import ErrorOccurrence +from ...errors.exceptions import CannotPerformGripperAction, GripperNotAttachedError +from ...resources import ensure_ot3_hardware +from ...types import DeckSlotLocation, LabwareLocation + +from opentrons.types import Point + +from opentrons.hardware_control import HardwareControlAPI +from opentrons.hardware_control.types import Axis, OT3Mount +from opentrons.motion_planning.waypoints import get_gripper_labware_placement_waypoints + + +if TYPE_CHECKING: + from ...state.state import StateView, StateStore + +log = getLogger(__name__) + +UnsafePlaceLabwareCommandType = Literal["unsafe/placeLabware"] + + +class UnsafePlaceLabwareParams(BaseModel): + """Payload required for an PlaceLabware command.""" + + labwareId: str = Field(..., description="The id of the labware to place.") + newLocation: LabwareLocation = Field(..., description="Where to place the labware.") + + +class UnsafePlaceLabwareResult(BaseModel): + """Result data from the execution of an PlaceLabware command.""" + + +class UnsafePlaceLabwareImplementation( + AbstractCommandImpl[ + UnsafePlaceLabwareParams, + SuccessData[UnsafePlaceLabwareResult, None], + ] +): + """Move labware command implementation.""" + + def __init__( + self, + state_view: StateView, + state_store: StateStore, + state_: StateView, + hardware_api: HardwareControlAPI, + **kwargs: object, + ) -> None: + self._hardware_api = hardware_api + self._state_view = state_view + self._state_store = state_store + + async def execute( + self, params: UnsafePlaceLabwareParams + ) -> SuccessData[UnsafePlaceLabwareResult, None]: + """Place Labware.""" + + # 1. Make sure we are on a Flex + # 2. Make sure we have a gripper + # 3. Make sure the gripper has something in its jaws + # 4. Make sure there isnt anything in the destination slot + # 5. Get the labware definition for geometric shape, gripper offset, force, etc + # 6. Create movement waypoints for gripper, starting with home on the gripper z + # 7. Execute waypoints, raise if error (stall, collision, drop, etc) + return SuccessData(public=UnsafePlaceLabwareResult(), private=None) + log.warning(1) + ot3api = ensure_ot3_hardware(self._hardware_api) + log.warning(2) + if not ot3api.has_gripper(): + raise GripperNotAttachedError("No gripper found to perform labware place.") + + if ot3api.gripper_jaw_can_home(): + raise CannotPerformGripperAction( + "Cannot place labware when gripper is not gripping." + ) + + log.warning(3) + # Allow propagation of LabwareNotLoadedError. + labware_id = params.labwareId + current_labware = self._state_view.labware.get(labware_id=params.labwareId) + current_labware_definition = self._state_view.labware.get_definition( + labware_id=params.labwareId + ) + + log.warning(4) + if isinstance(params.newLocation, DeckSlotLocation): + self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( + params.newLocation.slotName.id + ) + + log.warning(5) + available_new_location = self._state_view.geometry.ensure_location_not_occupied( + location=params.newLocation + ) + + log.warning(6) + new_location = self._state_view.geometry.ensure_valid_gripper_location( + available_new_location, + ) + + log.warning(7) + # TODO: Only home gripper Z + await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]) + gripper_homed_position = await ot3api.gantry_position(mount=OT3Mount.GRIPPER) + + log.warning(8) + drop_offset = Point() # TODO: FIx this + to_labware_center = self._state_store.geometry.get_labware_grip_point( + labware_id=labware_id, location=new_location + + ) + + log.warning(9) + movement_waypoints = get_gripper_labware_placement_waypoints( + to_labware_center=to_labware_center, + gripper_home_z=gripper_homed_position.z, + drop_offset=drop_offset, + ) + + log.warning(10) + # start movement + for waypoint_data in movement_waypoints: + + log.warning(f"WP: {waypoint_data}") + await ot3api.move_to( + mount=OT3Mount.GRIPPER, abs_position=waypoint_data.position + ) + + if waypoint_data.jaw_open: + if waypoint_data.dropping: + # This `disengage_axes` step is important in order to engage + # the electronic brake on the Z axis of the gripper. The brake + # has a stronger holding force on the axis than the hold current, + # and prevents the axis from spuriously dropping when e.g. the notch + # on the side of a falling tiprack catches the jaw. + await ot3api.disengage_axes([Axis.Z_G]) + await ot3api.ungrip() + if waypoint_data.dropping: + # We lost the position estimation after disengaging the axis, so + # it is necessary to home it next + await ot3api.home_z(OT3Mount.GRIPPER) + + return SuccessData(public=UnsafePlaceLabwareResult(), private=None) + + +class UnsafePlaceLabware( + BaseCommand[UnsafePlaceLabwareParams, UnsafePlaceLabwareResult, ErrorOccurrence] +): + """UnsafePlaceLabware command model.""" + + commandType: UnsafePlaceLabwareCommandType = "unsafe/placeLabware" + params: UnsafePlaceLabwareParams + result: Optional[UnsafePlaceLabwareResult] + + _ImplementationCls: Type[ + UnsafePlaceLabwareImplementation + ] = UnsafePlaceLabwareImplementation + + +class UnsafePlaceLabwareCreate(BaseCommandCreate[UnsafePlaceLabwareParams]): + """UnsafeEngageAxes command request model.""" + + commandType: UnsafePlaceLabwareCommandType = "unsafe/placeLabware" + params: UnsafePlaceLabwareParams + + _CommandCls: Type[UnsafePlaceLabware] = UnsafePlaceLabware diff --git a/app/src/resources/modules/hooks/useDropPlateReaderLid.ts b/app/src/resources/modules/hooks/useDropPlateReaderLid.ts new file mode 100644 index 00000000000..88282eadedd --- /dev/null +++ b/app/src/resources/modules/hooks/useDropPlateReaderLid.ts @@ -0,0 +1,47 @@ +import { useRobotControlCommands } from '/app/resources/maintenance_runs' + +import type { CreateCommand } from '@opentrons/shared-data' +import type { + UseRobotControlCommandsProps, + UseRobotControlCommandsResult, +} from '/app/resources/maintenance_runs' + +interface UseHomePipettesResult { + isHoming: UseRobotControlCommandsResult['isExecuting'] + homePipettes: UseRobotControlCommandsResult['executeCommands'] +} + +export type UseDropPlateReaderLidProps = Pick< + UseRobotControlCommandsProps, + 'pipetteInfo' | 'onSettled' +> + +export function useDropPlateReaderLid( + props: UseDropPlateReaderLidProps +): UseHomePipettesResult { + const { executeCommands, isExecuting } = useRobotControlCommands({ + ...props, + commands: [LOAD_PLATE_READER, DROP_PLATE_READER_LID], + continuePastCommandFailure: true, + }) + + return { + isHoming: isExecuting, + homePipettes: executeCommands, + } +} + +const LOAD_PLATE_READER: CreateCommand = { + commandType: 'loadModule' as const, + params: {"model": "absorbanceReaderV1", "location": {"slotName": "C3"}}, +} + +const DROP_PLATE_READER_LID: CreateCommand = { + commandType: 'moveLabware' as const, + params: + { + "labwareId": "absorbanceReaderV1LidC3", + "newLocation": {"slotName": "C3"}, + "strategy": "usingGripper", + }, +} diff --git a/robot-server/robot_server/robot/control/router.py b/robot-server/robot_server/robot/control/router.py index 35910748115..995e81e4c06 100644 --- a/robot-server/robot_server/robot/control/router.py +++ b/robot-server/robot_server/robot/control/router.py @@ -1,17 +1,32 @@ """Router for /robot/control endpoints.""" +from datetime import datetime from fastapi import APIRouter, status, Depends -from typing import Annotated, TYPE_CHECKING +from typing import Annotated, TYPE_CHECKING, Callable, Optional from opentrons_shared_data.robot.types import RobotType from opentrons_shared_data.robot.types import RobotTypeEnum +from robot_server.deck_configuration.fastapi_dependencies import ( + get_deck_configuration_store, +) +from robot_server.deck_configuration.store import DeckConfigurationStore from robot_server.hardware import get_robot_type from robot_server.errors.error_responses import ErrorBody from robot_server.errors.robot_errors import NotSupportedOnOT2 +from robot_server.maintenance_runs.dependencies import get_maintenance_run_data_manager +from robot_server.maintenance_runs.maintenance_run_data_manager import ( + MaintenanceRunDataManager, +) +from robot_server.maintenance_runs.maintenance_run_models import MaintenanceRunCreate +from robot_server.service.dependencies import get_current_time, get_unique_id from robot_server.service.json_api import ( PydanticResponse, SimpleBody, ) +from robot_server.service.json_api.request import RequestModel +from robot_server.service.notifications.publisher_notifier import ( + get_pe_notify_publishers, +) from .models import EstopStatusModel, DoorStatusModel, DoorState from .estop_handler import EstopHandler @@ -67,9 +82,37 @@ async def get_estop_status( ) async def put_acknowledge_estop_disengage( estop_handler: Annotated[EstopHandler, Depends(get_estop_handler)], + run_id: Annotated[str, Depends(get_unique_id)], + created_at: Annotated[datetime, Depends(get_current_time)], + run_data_manager: Annotated[ + MaintenanceRunDataManager, Depends(get_maintenance_run_data_manager) + ], + deck_configuration_store: Annotated[ + DeckConfigurationStore, Depends(get_deck_configuration_store) + ], + notify_publishers: Annotated[Callable[[], None], Depends(get_pe_notify_publishers)], + request_body: Optional[RequestModel[MaintenanceRunCreate]] = None, ) -> PydanticResponse[SimpleBody[EstopStatusModel]]: """Transition from the `logically_engaged` status if applicable.""" estop_handler.acknowledge_and_clear() + # here, move the plate reader back into its deck slot + + # 1. check if the gripper has a plate reader lid? + # 2. create a maintenence run + # 3. load plate reader + lid + # 4. move lid to dock + + offsets = request_body.data.labwareOffsets if request_body is not None else [] + deck_configuration = await deck_configuration_store.get_deck_configuration() + + run_data = await run_data_manager.create( + run_id=run_id, + created_at=created_at, + labware_offsets=offsets, + deck_configuration=deck_configuration, + notify_publishers=notify_publishers, + ) + return await _get_estop_status_response(estop_handler) diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index b9bd8cd24b2..018433eb1c8 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -12,6 +12,7 @@ from pydantic import BaseModel, Field from opentrons_shared_data.errors import ErrorCodes +from robot_server.service.legacy.models.modules import HeaterShakerModuleLiveData from opentrons.protocol_engine.types import CSVRuntimeParamPaths from opentrons.protocol_engine import ( errors as pe_errors, @@ -48,6 +49,7 @@ from robot_server.protocols.router import ProtocolNotFound from ..run_models import ( + PlateReaderState, RunNotFoundError, ActiveNozzleLayout, RunCurrentState, @@ -597,9 +599,20 @@ async def get_current_state( else None ) + # TODO: add the actual plate reader data + plate_reader_states = { + "test": PlateReaderState( + lidHeldByGripper=False, + plateReaderLidLocation="D3", + ) + } + return await PydanticResponse.create( content=Body.construct( - data=RunCurrentState.construct(activeNozzleLayouts=nozzle_layouts), + data=RunCurrentState.construct( + activeNozzleLayouts=nozzle_layouts, + plateReaderState=plate_reader_states + ), links=links, ), status_code=status.HTTP_200_OK, diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index 962c3ab51e7..6ba9a94ebb5 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -307,10 +307,22 @@ class ActiveNozzleLayout(BaseModel): ) +class PlateReaderState(BaseModel): + """Details about the plate reader.""" + + plateReaderLidLocation: str = Field( + ..., description="The location the Plate Reade lid should be in." + ) + lidHeldByGripper: bool = Field( + ..., description="Whether the gripper is holding the Plate Reader lid." + ) + + class RunCurrentState(BaseModel): """Current details about a run.""" activeNozzleLayouts: Dict[str, ActiveNozzleLayout] = Field(..., description="") + plateReaderState: Dict[str, PlateReaderState] = Field(..., description="") class CommandLinkNoMeta(BaseModel): From d42b4ba0078c7f13878dcb28e96ec27f1e84608b Mon Sep 17 00:00:00 2001 From: vegano1 Date: Fri, 18 Oct 2024 13:18:54 -0400 Subject: [PATCH 02/20] save --- .../commands/unsafe/unsafe_place_labware.py | 1 - .../modules/hooks/useDropPlateReaderLid.ts | 11 ++- .../robot_server/runs/router/base_router.py | 11 ++- shared-data/command/schemas/10.json | 71 ++++++++++++++++++- 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index 875c1bb0799..c2188bd50c6 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -115,7 +115,6 @@ async def execute( drop_offset = Point() # TODO: FIx this to_labware_center = self._state_store.geometry.get_labware_grip_point( labware_id=labware_id, location=new_location - ) log.warning(9) diff --git a/app/src/resources/modules/hooks/useDropPlateReaderLid.ts b/app/src/resources/modules/hooks/useDropPlateReaderLid.ts index 88282eadedd..b0234647c84 100644 --- a/app/src/resources/modules/hooks/useDropPlateReaderLid.ts +++ b/app/src/resources/modules/hooks/useDropPlateReaderLid.ts @@ -33,15 +33,14 @@ export function useDropPlateReaderLid( const LOAD_PLATE_READER: CreateCommand = { commandType: 'loadModule' as const, - params: {"model": "absorbanceReaderV1", "location": {"slotName": "C3"}}, + params: { model: 'absorbanceReaderV1', location: { slotName: 'C3' } }, } const DROP_PLATE_READER_LID: CreateCommand = { commandType: 'moveLabware' as const, - params: - { - "labwareId": "absorbanceReaderV1LidC3", - "newLocation": {"slotName": "C3"}, - "strategy": "usingGripper", + params: { + labwareId: 'absorbanceReaderV1LidC3', + newLocation: { slotName: 'C3' }, + strategy: 'usingGripper', }, } diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index 018433eb1c8..6b4b99ab6a4 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -601,17 +601,16 @@ async def get_current_state( # TODO: add the actual plate reader data plate_reader_states = { - "test": PlateReaderState( - lidHeldByGripper=False, - plateReaderLidLocation="D3", - ) + "test": PlateReaderState( + lidHeldByGripper=False, + plateReaderLidLocation="D3", + ) } return await PydanticResponse.create( content=Body.construct( data=RunCurrentState.construct( - activeNozzleLayouts=nozzle_layouts, - plateReaderState=plate_reader_states + activeNozzleLayouts=nozzle_layouts, plateReaderState=plate_reader_states ), links=links, ), diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index 6508269ac62..8a6ed178827 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -76,7 +76,8 @@ "unsafe/dropTipInPlace": "#/definitions/UnsafeDropTipInPlaceCreate", "unsafe/updatePositionEstimators": "#/definitions/UpdatePositionEstimatorsCreate", "unsafe/engageAxes": "#/definitions/UnsafeEngageAxesCreate", - "unsafe/ungripLabware": "#/definitions/UnsafeUngripLabwareCreate" + "unsafe/ungripLabware": "#/definitions/UnsafeUngripLabwareCreate", + "unsafe/placeLabware": "#/definitions/UnsafePlaceLabwareCreate" } }, "oneOf": [ @@ -295,6 +296,9 @@ }, { "$ref": "#/definitions/UnsafeUngripLabwareCreate" + }, + { + "$ref": "#/definitions/UnsafePlaceLabwareCreate" } ], "definitions": { @@ -4734,6 +4738,71 @@ } }, "required": ["params"] + }, + "UnsafePlaceLabwareParams": { + "title": "UnsafePlaceLabwareParams", + "description": "Payload required for an PlaceLabware command.", + "type": "object", + "properties": { + "labwareId": { + "title": "Labwareid", + "description": "The id of the labware to place.", + "type": "string" + }, + "newLocation": { + "title": "Newlocation", + "description": "Where to place the labware.", + "anyOf": [ + { + "$ref": "#/definitions/DeckSlotLocation" + }, + { + "$ref": "#/definitions/ModuleLocation" + }, + { + "$ref": "#/definitions/OnLabwareLocation" + }, + { + "enum": ["offDeck"], + "type": "string" + }, + { + "$ref": "#/definitions/AddressableAreaLocation" + } + ] + } + }, + "required": ["labwareId", "newLocation"] + }, + "UnsafePlaceLabwareCreate": { + "title": "UnsafePlaceLabwareCreate", + "description": "UnsafeEngageAxes command request model.", + "type": "object", + "properties": { + "commandType": { + "title": "Commandtype", + "default": "unsafe/placeLabware", + "enum": ["unsafe/placeLabware"], + "type": "string" + }, + "params": { + "$ref": "#/definitions/UnsafePlaceLabwareParams" + }, + "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": "opentronsCommandSchemaV10", From 893da7a429da52cce122efe74b1e4ff3598572be Mon Sep 17 00:00:00 2001 From: vegano1 Date: Fri, 18 Oct 2024 22:03:41 -0400 Subject: [PATCH 03/20] working place labware command --- .../modules/absorbance_reader.py | 11 +- api/src/opentrons/motion_planning/__init__.py | 2 + .../commands/unsafe/unsafe_place_labware.py | 272 ++++++++++++++---- .../opentrons/protocol_engine/state/state.py | 7 + .../protocol_runner/run_orchestrator.py | 5 + .../router/commands_router.py | 3 + .../robot_server/robot/control/router.py | 45 +-- 7 files changed, 237 insertions(+), 108 deletions(-) diff --git a/api/src/opentrons/hardware_control/modules/absorbance_reader.py b/api/src/opentrons/hardware_control/modules/absorbance_reader.py index da7c4746086..ab6ce1bb22b 100644 --- a/api/src/opentrons/hardware_control/modules/absorbance_reader.py +++ b/api/src/opentrons/hardware_control/modules/absorbance_reader.py @@ -272,12 +272,8 @@ def usb_port(self) -> USBPort: return self._usb_port async def deactivate(self, must_be_running: bool = True) -> None: - """Deactivate the module. - - Contains an override to the `wait_for_is_running` step in cases where the - module must be deactivated regardless of context.""" - await self._poller.stop() - await self._driver.disconnect() + """Deactivate the module.""" + pass async def wait_for_is_running(self) -> None: if not self.is_simulated: @@ -336,7 +332,8 @@ async def cleanup(self) -> None: Clean up, i.e. stop pollers, disconnect serial, etc in preparation for object destruction. """ - await self.deactivate() + await self._poller.stop() + await self._driver.disconnect() async def set_sample_wavelength( self, diff --git a/api/src/opentrons/motion_planning/__init__.py b/api/src/opentrons/motion_planning/__init__.py index 570d4250ebe..2b304ecb74d 100644 --- a/api/src/opentrons/motion_planning/__init__.py +++ b/api/src/opentrons/motion_planning/__init__.py @@ -6,6 +6,7 @@ MINIMUM_Z_MARGIN, get_waypoints, get_gripper_labware_movement_waypoints, + get_gripper_labware_placement_waypoints, ) from .types import Waypoint, MoveType @@ -27,4 +28,5 @@ "ArcOutOfBoundsError", "get_waypoints", "get_gripper_labware_movement_waypoints", + "get_gripper_labware_placement_waypoints", ] diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index c2188bd50c6..e2838a76fc0 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -1,41 +1,38 @@ -"""Place labware with gripper, result, and implementaiton.""" +"""Testing""" from __future__ import annotations from pydantic import BaseModel, Field -from typing import TYPE_CHECKING, Optional, Type +from typing import TYPE_CHECKING, Optional, Type, cast from typing_extensions import Literal -from logging import getLogger +from opentrons.hardware_control.types import Axis, OT3Mount +from opentrons.motion_planning.waypoints import get_gripper_labware_placement_waypoints +from opentrons.protocol_engine.errors.exceptions import CannotPerformGripperAction, GripperNotAttachedError +from opentrons.types import Point + +from ...types import DeckSlotLocation, LabwareLocation, ModuleLocation, OnDeckLabwareLocation from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence -from ...errors.exceptions import CannotPerformGripperAction, GripperNotAttachedError from ...resources import ensure_ot3_hardware -from ...types import DeckSlotLocation, LabwareLocation - -from opentrons.types import Point from opentrons.hardware_control import HardwareControlAPI -from opentrons.hardware_control.types import Axis, OT3Mount -from opentrons.motion_planning.waypoints import get_gripper_labware_placement_waypoints - if TYPE_CHECKING: - from ...state.state import StateView, StateStore + from ...state.state import StateView -log = getLogger(__name__) UnsafePlaceLabwareCommandType = Literal["unsafe/placeLabware"] class UnsafePlaceLabwareParams(BaseModel): - """Payload required for an PlaceLabware command.""" + """Payload required for an UnsafePlaceLabware command.""" labwareId: str = Field(..., description="The id of the labware to place.") newLocation: LabwareLocation = Field(..., description="Where to place the labware.") class UnsafePlaceLabwareResult(BaseModel): - """Result data from the execution of an PlaceLabware command.""" + """Result data from the execution of an UnsafePlaceLabware command.""" class UnsafePlaceLabwareImplementation( @@ -44,36 +41,22 @@ class UnsafePlaceLabwareImplementation( SuccessData[UnsafePlaceLabwareResult, None], ] ): - """Move labware command implementation.""" + """The place labware command implementation.""" def __init__( self, - state_view: StateView, - state_store: StateStore, - state_: StateView, hardware_api: HardwareControlAPI, + state_view: StateView, **kwargs: object, ) -> None: self._hardware_api = hardware_api self._state_view = state_view - self._state_store = state_store async def execute( self, params: UnsafePlaceLabwareParams ) -> SuccessData[UnsafePlaceLabwareResult, None]: - """Place Labware.""" - - # 1. Make sure we are on a Flex - # 2. Make sure we have a gripper - # 3. Make sure the gripper has something in its jaws - # 4. Make sure there isnt anything in the destination slot - # 5. Get the labware definition for geometric shape, gripper offset, force, etc - # 6. Create movement waypoints for gripper, starting with home on the gripper z - # 7. Execute waypoints, raise if error (stall, collision, drop, etc) - return SuccessData(public=UnsafePlaceLabwareResult(), private=None) - log.warning(1) + """Enable exes.""" ot3api = ensure_ot3_hardware(self._hardware_api) - log.warning(2) if not ot3api.has_gripper(): raise GripperNotAttachedError("No gripper found to perform labware place.") @@ -82,7 +65,6 @@ async def execute( "Cannot place labware when gripper is not gripping." ) - log.warning(3) # Allow propagation of LabwareNotLoadedError. labware_id = params.labwareId current_labware = self._state_view.labware.get(labware_id=params.labwareId) @@ -90,49 +72,47 @@ async def execute( labware_id=params.labwareId ) - log.warning(4) + # TODO: do we need these offsets + # final_offsets = ( + # self._state_view.geometry.get_final_labware_movement_offset_vectors( + # from_location=ModuleLocation(), + # to_location=new_location, + # additional_offset_vector=Point(), + # current_labware=current_labware, + # ) + # ) + final_offsets = self._state_view.labware.get_labware_gripper_offsets(labware_id, None) + drop_offset = cast(Point, final_offsets.dropOffset) if final_offsets else Point() + if isinstance(params.newLocation, DeckSlotLocation): self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( params.newLocation.slotName.id ) - log.warning(5) - available_new_location = self._state_view.geometry.ensure_location_not_occupied( - location=params.newLocation - ) - - log.warning(6) new_location = self._state_view.geometry.ensure_valid_gripper_location( - available_new_location, + params.newLocation, ) - log.warning(7) - # TODO: Only home gripper Z - await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]) - gripper_homed_position = await ot3api.gantry_position(mount=OT3Mount.GRIPPER) + # TODO: when we stop the gantry position goes bad, so the robot needs to + # home x, y. + await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G, Axis.X, Axis.Y]) + gripper_homed_position = await ot3api.gantry_position( + mount=OT3Mount.GRIPPER, + refresh=True, + ) - log.warning(8) - drop_offset = Point() # TODO: FIx this - to_labware_center = self._state_store.geometry.get_labware_grip_point( + to_labware_center = self._state_view.geometry.get_labware_grip_point( labware_id=labware_id, location=new_location ) - log.warning(9) movement_waypoints = get_gripper_labware_placement_waypoints( to_labware_center=to_labware_center, gripper_home_z=gripper_homed_position.z, drop_offset=drop_offset, ) - log.warning(10) # start movement for waypoint_data in movement_waypoints: - - log.warning(f"WP: {waypoint_data}") - await ot3api.move_to( - mount=OT3Mount.GRIPPER, abs_position=waypoint_data.position - ) - if waypoint_data.jaw_open: if waypoint_data.dropping: # This `disengage_axes` step is important in order to engage @@ -146,7 +126,9 @@ async def execute( # We lost the position estimation after disengaging the axis, so # it is necessary to home it next await ot3api.home_z(OT3Mount.GRIPPER) - + await ot3api.move_to( + mount=OT3Mount.GRIPPER, abs_position=waypoint_data.position + ) return SuccessData(public=UnsafePlaceLabwareResult(), private=None) @@ -165,9 +147,185 @@ class UnsafePlaceLabware( class UnsafePlaceLabwareCreate(BaseCommandCreate[UnsafePlaceLabwareParams]): - """UnsafeEngageAxes command request model.""" + """UnsafePlaceLabware command request model.""" commandType: UnsafePlaceLabwareCommandType = "unsafe/placeLabware" params: UnsafePlaceLabwareParams _CommandCls: Type[UnsafePlaceLabware] = UnsafePlaceLabware + + +# """Place labware with gripper, result, and implementaiton.""" +# +# from __future__ import annotations +# import asyncio +# from pydantic import BaseModel, Field +# from typing import TYPE_CHECKING, Optional, Type +# from typing_extensions import Literal +# from logging import getLogger +# +# from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData +# from ...errors.error_occurrence import ErrorOccurrence +# from ...errors.exceptions import CannotPerformGripperAction, GripperNotAttachedError +# from ...resources import ensure_ot3_hardware +# from ...types import DeckSlotLocation, LabwareLocation +# +# from opentrons.types import Point +# +# from opentrons.hardware_control import HardwareControlAPI +# from opentrons.hardware_control.types import Axis, OT3Mount +# from opentrons.motion_planning.waypoints import get_gripper_labware_placement_waypoints +# +# +# if TYPE_CHECKING: +# from ...state.state import StateView, StateStore +# +# log = getLogger(__name__) +# +# UnsafePlaceLabwareCommandType = Literal["unsafe/placeLabware"] +# +# +# class UnsafePlaceLabwareParams(BaseModel): +# """Payload required for an PlaceLabware command.""" +# +# labwareId: str = Field(..., description="The id of the labware to place.") +# newLocation: LabwareLocation = Field(..., description="Where to place the labware.") +# +# +# class UnsafePlaceLabwareResult(BaseModel): +# """Result data from the execution of an PlaceLabware command.""" +# +# +# class UnsafePlaceLabwareImplementation( +# AbstractCommandImpl[ +# UnsafePlaceLabwareParams, +# SuccessData[UnsafePlaceLabwareResult, None], +# ] +# ): +# """Move labware command implementation.""" +# +# def __init__( +# self, +# state_view: StateView, +# state_store: StateStore, +# hardware_api: HardwareControlAPI, +# **kwargs: object, +# ) -> None: +# self._hardware_api = hardware_api +# self._state_view = state_view +# self._state_store = state_store +# +# async def execute( +# self, params: UnsafePlaceLabwareParams +# ) -> SuccessData[UnsafePlaceLabwareResult, None]: +# """Place Labware.""" +# +# # 1. Make sure we are on a Flex +# # 2. Make sure we have a gripper +# # 3. Make sure the gripper has something in its jaws +# # 4. Make sure there isnt anything in the destination slot +# # 5. Get the labware definition for geometric shape, gripper offset, force, etc +# # 6. Create movement waypoints for gripper, starting with home on the gripper z +# # 7. Execute waypoints, raise if error (stall, collision, drop, etc) +# print("SOMETHING") +# raise RuntimeError("FUCK OFF") +# return SuccessData(public=UnsafePlaceLabwareResult(), private=None) +# ot3api = ensure_ot3_hardware(self._hardware_api) +# log.warning(2) +# if not ot3api.has_gripper(): +# raise GripperNotAttachedError("No gripper found to perform labware place.") +# +# if ot3api.gripper_jaw_can_home(): +# raise CannotPerformGripperAction( +# "Cannot place labware when gripper is not gripping." +# ) +# +# log.warning(3) +# # Allow propagation of LabwareNotLoadedError. +# labware_id = params.labwareId +# current_labware = self._state_view.labware.get(labware_id=params.labwareId) +# current_labware_definition = self._state_view.labware.get_definition( +# labware_id=params.labwareId +# ) +# +# log.warning(4) +# if isinstance(params.newLocation, DeckSlotLocation): +# self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( +# params.newLocation.slotName.id +# ) +# +# log.warning(5) +# available_new_location = self._state_view.geometry.ensure_location_not_occupied( +# location=params.newLocation +# ) +# +# log.warning(6) +# new_location = self._state_view.geometry.ensure_valid_gripper_location( +# available_new_location, +# ) +# +# log.warning(7) +# # TODO: Only home gripper Z +# # await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]) +# gripper_homed_position = await ot3api.gantry_position(mount=OT3Mount.GRIPPER) +# +# log.warning(8) +# drop_offset = Point() # TODO: FIx this +# to_labware_center = self._state_store.geometry.get_labware_grip_point( +# labware_id=labware_id, location=new_location +# ) +# +# log.warning(9) +# movement_waypoints = get_gripper_labware_placement_waypoints( +# to_labware_center=to_labware_center, +# gripper_home_z=gripper_homed_position.z, +# drop_offset=drop_offset, +# ) +# +# log.warning(10) +# # start movement +# for waypoint_data in movement_waypoints: +# +# log.warning(f"WP: {waypoint_data}") +# await ot3api.move_to( +# mount=OT3Mount.GRIPPER, abs_position=waypoint_data.position +# ) +# +# if waypoint_data.jaw_open: +# if waypoint_data.dropping: +# # This `disengage_axes` step is important in order to engage +# # the electronic brake on the Z axis of the gripper. The brake +# # has a stronger holding force on the axis than the hold current, +# # and prevents the axis from spuriously dropping when e.g. the notch +# # on the side of a falling tiprack catches the jaw. +# await ot3api.disengage_axes([Axis.Z_G]) +# await ot3api.ungrip() +# if waypoint_data.dropping: +# # We lost the position estimation after disengaging the axis, so +# # it is necessary to home it next +# await ot3api.home_z(OT3Mount.GRIPPER) +# +# return SuccessData(public=UnsafePlaceLabwareResult(), private=None) +# +# +# class UnsafePlaceLabware( +# BaseCommand[UnsafePlaceLabwareParams, UnsafePlaceLabwareResult, ErrorOccurrence] +# ): +# """UnsafePlaceLabware command model.""" +# +# commandType: UnsafePlaceLabwareCommandType = "unsafe/placeLabware" +# params: UnsafePlaceLabwareParams +# result: Optional[UnsafePlaceLabwareResult] +# +# _ImplementationCls: Type[ +# UnsafePlaceLabwareImplementation +# ] = UnsafePlaceLabwareImplementation +# +# +# class UnsafePlaceLabwareCreate(BaseCommandCreate[UnsafePlaceLabwareParams]): +# """UnsafeEngageAxes command request model.""" +# +# commandType: UnsafePlaceLabwareCommandType = "unsafe/placeLabware" +# params: UnsafePlaceLabwareParams +# +# _CommandCls: Type[UnsafePlaceLabware] = UnsafePlaceLabware diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 7fc23a8ee2f..2be6949f20b 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -4,6 +4,7 @@ from dataclasses import dataclass from typing import Callable, Dict, List, Optional, Sequence, TypeVar from typing_extensions import ParamSpec +from logging import getLogger from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.robot.types import RobotDefinition @@ -34,6 +35,8 @@ from ..types import DeckConfigurationType +log = getLogger(__name__) + _ParamsT = ParamSpec("_ParamsT") _ReturnT = TypeVar("_ReturnT") @@ -317,9 +320,13 @@ async def _wait_for( current_value = condition() while bool(current_value) != truthiness_to_wait_for: + log.warning("IM FUCKING STUCK!!") await self._change_notifier.wait() + log.warning("CHECK CONDITION") current_value = condition() + log.warning(f"CONDITION: {current_value}") + log.warning("NOT STUCK! RETURN") return current_value def _get_next_state(self) -> State: diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 697e4a14e3a..9a906a65a24 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -3,6 +3,7 @@ import enum from typing import Optional, Union, List, Dict, AsyncGenerator +from logging import getLogger from anyio import move_on_after @@ -43,6 +44,8 @@ from ..protocols.parse import PythonParseMode +log = getLogger(__name__) + class NoProtocolRunAvailable(RuntimeError): """An error raised if there is no protocol run available.""" @@ -339,9 +342,11 @@ async def add_command_and_wait_for_interval( added_command = self._protocol_engine.add_command( request=command, failed_command_id=failed_command_id ) + log.warning(f"PE: added commmand: {added_command}") if wait_until_complete: timeout_sec = None if timeout is None else timeout / 1000.0 with move_on_after(timeout_sec): + log.warning(f"PE: executing command") await self._protocol_engine.wait_for_command(added_command.id) return added_command diff --git a/robot-server/robot_server/maintenance_runs/router/commands_router.py b/robot-server/robot_server/maintenance_runs/router/commands_router.py index afc5e03779b..c4ba4c3c85f 100644 --- a/robot-server/robot_server/maintenance_runs/router/commands_router.py +++ b/robot-server/robot_server/maintenance_runs/router/commands_router.py @@ -2,6 +2,7 @@ import textwrap from typing import Annotated, Optional, Union from typing_extensions import Final, Literal +from logging import getLogger from fastapi import APIRouter, Depends, Query, status @@ -36,6 +37,7 @@ ) from .base_router import RunNotFound +log = getLogger(__name__) _DEFAULT_COMMAND_LIST_LENGTH: Final = 20 @@ -156,6 +158,7 @@ async def create_run_command( # behavior is to pass through `command_intent` without overriding it command_intent = pe_commands.CommandIntent.SETUP command_create = request_body.data.copy(update={"intent": command_intent}) + log.warning(f"Create Command: {command_create}") command = await run_orchestrator_store.add_command_and_wait_for_interval( request=command_create, wait_until_complete=waitUntilComplete, timeout=timeout ) diff --git a/robot-server/robot_server/robot/control/router.py b/robot-server/robot_server/robot/control/router.py index 995e81e4c06..35910748115 100644 --- a/robot-server/robot_server/robot/control/router.py +++ b/robot-server/robot_server/robot/control/router.py @@ -1,32 +1,17 @@ """Router for /robot/control endpoints.""" -from datetime import datetime from fastapi import APIRouter, status, Depends -from typing import Annotated, TYPE_CHECKING, Callable, Optional +from typing import Annotated, TYPE_CHECKING from opentrons_shared_data.robot.types import RobotType from opentrons_shared_data.robot.types import RobotTypeEnum -from robot_server.deck_configuration.fastapi_dependencies import ( - get_deck_configuration_store, -) -from robot_server.deck_configuration.store import DeckConfigurationStore from robot_server.hardware import get_robot_type from robot_server.errors.error_responses import ErrorBody from robot_server.errors.robot_errors import NotSupportedOnOT2 -from robot_server.maintenance_runs.dependencies import get_maintenance_run_data_manager -from robot_server.maintenance_runs.maintenance_run_data_manager import ( - MaintenanceRunDataManager, -) -from robot_server.maintenance_runs.maintenance_run_models import MaintenanceRunCreate -from robot_server.service.dependencies import get_current_time, get_unique_id from robot_server.service.json_api import ( PydanticResponse, SimpleBody, ) -from robot_server.service.json_api.request import RequestModel -from robot_server.service.notifications.publisher_notifier import ( - get_pe_notify_publishers, -) from .models import EstopStatusModel, DoorStatusModel, DoorState from .estop_handler import EstopHandler @@ -82,37 +67,9 @@ async def get_estop_status( ) async def put_acknowledge_estop_disengage( estop_handler: Annotated[EstopHandler, Depends(get_estop_handler)], - run_id: Annotated[str, Depends(get_unique_id)], - created_at: Annotated[datetime, Depends(get_current_time)], - run_data_manager: Annotated[ - MaintenanceRunDataManager, Depends(get_maintenance_run_data_manager) - ], - deck_configuration_store: Annotated[ - DeckConfigurationStore, Depends(get_deck_configuration_store) - ], - notify_publishers: Annotated[Callable[[], None], Depends(get_pe_notify_publishers)], - request_body: Optional[RequestModel[MaintenanceRunCreate]] = None, ) -> PydanticResponse[SimpleBody[EstopStatusModel]]: """Transition from the `logically_engaged` status if applicable.""" estop_handler.acknowledge_and_clear() - # here, move the plate reader back into its deck slot - - # 1. check if the gripper has a plate reader lid? - # 2. create a maintenence run - # 3. load plate reader + lid - # 4. move lid to dock - - offsets = request_body.data.labwareOffsets if request_body is not None else [] - deck_configuration = await deck_configuration_store.get_deck_configuration() - - run_data = await run_data_manager.create( - run_id=run_id, - created_at=created_at, - labware_offsets=offsets, - deck_configuration=deck_configuration, - notify_publishers=notify_publishers, - ) - return await _get_estop_status_response(estop_handler) From 2fcce8ed11fc17308ffdebe6f275859e1bbc48f2 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Sat, 19 Oct 2024 17:04:20 -0400 Subject: [PATCH 04/20] feat(api): add unsafe/placeLabware command to place labware the gripper is holding somewhere on the deck. --- .../commands/unsafe/unsafe_place_labware.py | 218 ++---------------- .../opentrons/protocol_engine/state/state.py | 7 - .../protocol_runner/run_orchestrator.py | 5 - .../router/commands_router.py | 3 - 4 files changed, 20 insertions(+), 213 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index e2838a76fc0..c48532e3ecd 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -1,4 +1,4 @@ -"""Testing""" +"""Place labware payload, result, and implementaiton.""" from __future__ import annotations from pydantic import BaseModel, Field @@ -7,10 +7,13 @@ from opentrons.hardware_control.types import Axis, OT3Mount from opentrons.motion_planning.waypoints import get_gripper_labware_placement_waypoints -from opentrons.protocol_engine.errors.exceptions import CannotPerformGripperAction, GripperNotAttachedError +from opentrons.protocol_engine.errors.exceptions import ( + CannotPerformGripperAction, + GripperNotAttachedError, +) from opentrons.types import Point -from ...types import DeckSlotLocation, LabwareLocation, ModuleLocation, OnDeckLabwareLocation +from ...types import DeckSlotLocation, LabwareLocation from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence from ...resources import ensure_ot3_hardware @@ -41,7 +44,7 @@ class UnsafePlaceLabwareImplementation( SuccessData[UnsafePlaceLabwareResult, None], ] ): - """The place labware command implementation.""" + """The UnsafePlaceLabware command implementation.""" def __init__( self, @@ -55,7 +58,7 @@ def __init__( async def execute( self, params: UnsafePlaceLabwareParams ) -> SuccessData[UnsafePlaceLabwareResult, None]: - """Enable exes.""" + """Place Labware.""" ot3api = ensure_ot3_hardware(self._hardware_api) if not ot3api.has_gripper(): raise GripperNotAttachedError("No gripper found to perform labware place.") @@ -67,22 +70,16 @@ async def execute( # Allow propagation of LabwareNotLoadedError. labware_id = params.labwareId - current_labware = self._state_view.labware.get(labware_id=params.labwareId) - current_labware_definition = self._state_view.labware.get_definition( - labware_id=params.labwareId - ) + self._state_view.labware.get(labware_id=labware_id) - # TODO: do we need these offsets - # final_offsets = ( - # self._state_view.geometry.get_final_labware_movement_offset_vectors( - # from_location=ModuleLocation(), - # to_location=new_location, - # additional_offset_vector=Point(), - # current_labware=current_labware, - # ) - # ) - final_offsets = self._state_view.labware.get_labware_gripper_offsets(labware_id, None) - drop_offset = cast(Point, final_offsets.dropOffset) if final_offsets else Point() + # TODO: do we need these offsets instead? + # self._state_view.geometry.get_final_labware_movement_offset_vectors + final_offsets = self._state_view.labware.get_labware_gripper_offsets( + labware_id, None + ) + drop_offset = ( + cast(Point, final_offsets.dropOffset) if final_offsets else Point() + ) if isinstance(params.newLocation, DeckSlotLocation): self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( @@ -93,8 +90,9 @@ async def execute( params.newLocation, ) - # TODO: when we stop the gantry position goes bad, so the robot needs to - # home x, y. + # NOTE: when we e-stop, the gantry position goes bad, so the robot needs to + # home x, y. This can be a problem if a pipette is holding tips. + # so maybe home everything? and then drop the tips? await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G, Axis.X, Axis.Y]) gripper_homed_position = await ot3api.gantry_position( mount=OT3Mount.GRIPPER, @@ -153,179 +151,3 @@ class UnsafePlaceLabwareCreate(BaseCommandCreate[UnsafePlaceLabwareParams]): params: UnsafePlaceLabwareParams _CommandCls: Type[UnsafePlaceLabware] = UnsafePlaceLabware - - -# """Place labware with gripper, result, and implementaiton.""" -# -# from __future__ import annotations -# import asyncio -# from pydantic import BaseModel, Field -# from typing import TYPE_CHECKING, Optional, Type -# from typing_extensions import Literal -# from logging import getLogger -# -# from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData -# from ...errors.error_occurrence import ErrorOccurrence -# from ...errors.exceptions import CannotPerformGripperAction, GripperNotAttachedError -# from ...resources import ensure_ot3_hardware -# from ...types import DeckSlotLocation, LabwareLocation -# -# from opentrons.types import Point -# -# from opentrons.hardware_control import HardwareControlAPI -# from opentrons.hardware_control.types import Axis, OT3Mount -# from opentrons.motion_planning.waypoints import get_gripper_labware_placement_waypoints -# -# -# if TYPE_CHECKING: -# from ...state.state import StateView, StateStore -# -# log = getLogger(__name__) -# -# UnsafePlaceLabwareCommandType = Literal["unsafe/placeLabware"] -# -# -# class UnsafePlaceLabwareParams(BaseModel): -# """Payload required for an PlaceLabware command.""" -# -# labwareId: str = Field(..., description="The id of the labware to place.") -# newLocation: LabwareLocation = Field(..., description="Where to place the labware.") -# -# -# class UnsafePlaceLabwareResult(BaseModel): -# """Result data from the execution of an PlaceLabware command.""" -# -# -# class UnsafePlaceLabwareImplementation( -# AbstractCommandImpl[ -# UnsafePlaceLabwareParams, -# SuccessData[UnsafePlaceLabwareResult, None], -# ] -# ): -# """Move labware command implementation.""" -# -# def __init__( -# self, -# state_view: StateView, -# state_store: StateStore, -# hardware_api: HardwareControlAPI, -# **kwargs: object, -# ) -> None: -# self._hardware_api = hardware_api -# self._state_view = state_view -# self._state_store = state_store -# -# async def execute( -# self, params: UnsafePlaceLabwareParams -# ) -> SuccessData[UnsafePlaceLabwareResult, None]: -# """Place Labware.""" -# -# # 1. Make sure we are on a Flex -# # 2. Make sure we have a gripper -# # 3. Make sure the gripper has something in its jaws -# # 4. Make sure there isnt anything in the destination slot -# # 5. Get the labware definition for geometric shape, gripper offset, force, etc -# # 6. Create movement waypoints for gripper, starting with home on the gripper z -# # 7. Execute waypoints, raise if error (stall, collision, drop, etc) -# print("SOMETHING") -# raise RuntimeError("FUCK OFF") -# return SuccessData(public=UnsafePlaceLabwareResult(), private=None) -# ot3api = ensure_ot3_hardware(self._hardware_api) -# log.warning(2) -# if not ot3api.has_gripper(): -# raise GripperNotAttachedError("No gripper found to perform labware place.") -# -# if ot3api.gripper_jaw_can_home(): -# raise CannotPerformGripperAction( -# "Cannot place labware when gripper is not gripping." -# ) -# -# log.warning(3) -# # Allow propagation of LabwareNotLoadedError. -# labware_id = params.labwareId -# current_labware = self._state_view.labware.get(labware_id=params.labwareId) -# current_labware_definition = self._state_view.labware.get_definition( -# labware_id=params.labwareId -# ) -# -# log.warning(4) -# if isinstance(params.newLocation, DeckSlotLocation): -# self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( -# params.newLocation.slotName.id -# ) -# -# log.warning(5) -# available_new_location = self._state_view.geometry.ensure_location_not_occupied( -# location=params.newLocation -# ) -# -# log.warning(6) -# new_location = self._state_view.geometry.ensure_valid_gripper_location( -# available_new_location, -# ) -# -# log.warning(7) -# # TODO: Only home gripper Z -# # await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]) -# gripper_homed_position = await ot3api.gantry_position(mount=OT3Mount.GRIPPER) -# -# log.warning(8) -# drop_offset = Point() # TODO: FIx this -# to_labware_center = self._state_store.geometry.get_labware_grip_point( -# labware_id=labware_id, location=new_location -# ) -# -# log.warning(9) -# movement_waypoints = get_gripper_labware_placement_waypoints( -# to_labware_center=to_labware_center, -# gripper_home_z=gripper_homed_position.z, -# drop_offset=drop_offset, -# ) -# -# log.warning(10) -# # start movement -# for waypoint_data in movement_waypoints: -# -# log.warning(f"WP: {waypoint_data}") -# await ot3api.move_to( -# mount=OT3Mount.GRIPPER, abs_position=waypoint_data.position -# ) -# -# if waypoint_data.jaw_open: -# if waypoint_data.dropping: -# # This `disengage_axes` step is important in order to engage -# # the electronic brake on the Z axis of the gripper. The brake -# # has a stronger holding force on the axis than the hold current, -# # and prevents the axis from spuriously dropping when e.g. the notch -# # on the side of a falling tiprack catches the jaw. -# await ot3api.disengage_axes([Axis.Z_G]) -# await ot3api.ungrip() -# if waypoint_data.dropping: -# # We lost the position estimation after disengaging the axis, so -# # it is necessary to home it next -# await ot3api.home_z(OT3Mount.GRIPPER) -# -# return SuccessData(public=UnsafePlaceLabwareResult(), private=None) -# -# -# class UnsafePlaceLabware( -# BaseCommand[UnsafePlaceLabwareParams, UnsafePlaceLabwareResult, ErrorOccurrence] -# ): -# """UnsafePlaceLabware command model.""" -# -# commandType: UnsafePlaceLabwareCommandType = "unsafe/placeLabware" -# params: UnsafePlaceLabwareParams -# result: Optional[UnsafePlaceLabwareResult] -# -# _ImplementationCls: Type[ -# UnsafePlaceLabwareImplementation -# ] = UnsafePlaceLabwareImplementation -# -# -# class UnsafePlaceLabwareCreate(BaseCommandCreate[UnsafePlaceLabwareParams]): -# """UnsafeEngageAxes command request model.""" -# -# commandType: UnsafePlaceLabwareCommandType = "unsafe/placeLabware" -# params: UnsafePlaceLabwareParams -# -# _CommandCls: Type[UnsafePlaceLabware] = UnsafePlaceLabware diff --git a/api/src/opentrons/protocol_engine/state/state.py b/api/src/opentrons/protocol_engine/state/state.py index 2be6949f20b..7fc23a8ee2f 100644 --- a/api/src/opentrons/protocol_engine/state/state.py +++ b/api/src/opentrons/protocol_engine/state/state.py @@ -4,7 +4,6 @@ from dataclasses import dataclass from typing import Callable, Dict, List, Optional, Sequence, TypeVar from typing_extensions import ParamSpec -from logging import getLogger from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.robot.types import RobotDefinition @@ -35,8 +34,6 @@ from ..types import DeckConfigurationType -log = getLogger(__name__) - _ParamsT = ParamSpec("_ParamsT") _ReturnT = TypeVar("_ReturnT") @@ -320,13 +317,9 @@ async def _wait_for( current_value = condition() while bool(current_value) != truthiness_to_wait_for: - log.warning("IM FUCKING STUCK!!") await self._change_notifier.wait() - log.warning("CHECK CONDITION") current_value = condition() - log.warning(f"CONDITION: {current_value}") - log.warning("NOT STUCK! RETURN") return current_value def _get_next_state(self) -> State: diff --git a/api/src/opentrons/protocol_runner/run_orchestrator.py b/api/src/opentrons/protocol_runner/run_orchestrator.py index 9a906a65a24..697e4a14e3a 100644 --- a/api/src/opentrons/protocol_runner/run_orchestrator.py +++ b/api/src/opentrons/protocol_runner/run_orchestrator.py @@ -3,7 +3,6 @@ import enum from typing import Optional, Union, List, Dict, AsyncGenerator -from logging import getLogger from anyio import move_on_after @@ -44,8 +43,6 @@ from ..protocols.parse import PythonParseMode -log = getLogger(__name__) - class NoProtocolRunAvailable(RuntimeError): """An error raised if there is no protocol run available.""" @@ -342,11 +339,9 @@ async def add_command_and_wait_for_interval( added_command = self._protocol_engine.add_command( request=command, failed_command_id=failed_command_id ) - log.warning(f"PE: added commmand: {added_command}") if wait_until_complete: timeout_sec = None if timeout is None else timeout / 1000.0 with move_on_after(timeout_sec): - log.warning(f"PE: executing command") await self._protocol_engine.wait_for_command(added_command.id) return added_command diff --git a/robot-server/robot_server/maintenance_runs/router/commands_router.py b/robot-server/robot_server/maintenance_runs/router/commands_router.py index c4ba4c3c85f..afc5e03779b 100644 --- a/robot-server/robot_server/maintenance_runs/router/commands_router.py +++ b/robot-server/robot_server/maintenance_runs/router/commands_router.py @@ -2,7 +2,6 @@ import textwrap from typing import Annotated, Optional, Union from typing_extensions import Final, Literal -from logging import getLogger from fastapi import APIRouter, Depends, Query, status @@ -37,7 +36,6 @@ ) from .base_router import RunNotFound -log = getLogger(__name__) _DEFAULT_COMMAND_LIST_LENGTH: Final = 20 @@ -158,7 +156,6 @@ async def create_run_command( # behavior is to pass through `command_intent` without overriding it command_intent = pe_commands.CommandIntent.SETUP command_create = request_body.data.copy(update={"intent": command_intent}) - log.warning(f"Create Command: {command_create}") command = await run_orchestrator_store.add_command_and_wait_for_interval( request=command_create, wait_until_complete=waitUntilComplete, timeout=timeout ) From 0e201e05be014b20af57b521bc5ec40aa91ae1c7 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Sat, 19 Oct 2024 22:11:10 -0400 Subject: [PATCH 05/20] feat(robot-server): add placeLabwareState and estopEngaged to runs/currentState endpoint. --- .../commands/unsafe/unsafe_place_labware.py | 12 ++-- .../robot_server/runs/router/base_router.py | 60 ++++++++++++++++--- robot-server/robot_server/runs/run_models.py | 15 +++-- shared-data/command/schemas/10.json | 10 ++-- 4 files changed, 71 insertions(+), 26 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index c48532e3ecd..e2fc9bb287c 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -31,7 +31,7 @@ class UnsafePlaceLabwareParams(BaseModel): """Payload required for an UnsafePlaceLabware command.""" labwareId: str = Field(..., description="The id of the labware to place.") - newLocation: LabwareLocation = Field(..., description="Where to place the labware.") + location: LabwareLocation = Field(..., description="Where to place the labware.") class UnsafePlaceLabwareResult(BaseModel): @@ -81,13 +81,13 @@ async def execute( cast(Point, final_offsets.dropOffset) if final_offsets else Point() ) - if isinstance(params.newLocation, DeckSlotLocation): + if isinstance(params.location, DeckSlotLocation): self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( - params.newLocation.slotName.id + params.location.slotName.id ) - new_location = self._state_view.geometry.ensure_valid_gripper_location( - params.newLocation, + location = self._state_view.geometry.ensure_valid_gripper_location( + params.location, ) # NOTE: when we e-stop, the gantry position goes bad, so the robot needs to @@ -100,7 +100,7 @@ async def execute( ) to_labware_center = self._state_view.geometry.get_labware_grip_point( - labware_id=labware_id, location=new_location + labware_id=labware_id, location=location ) movement_waypoints = get_gripper_labware_placement_waypoints( diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index 6b4b99ab6a4..9d0b35c69c0 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -12,7 +12,11 @@ from pydantic import BaseModel, Field from opentrons_shared_data.errors import ErrorCodes -from robot_server.service.legacy.models.modules import HeaterShakerModuleLiveData +from opentrons_shared_data.robot.types import RobotTypeEnum +from opentrons.hardware_control import HardwareControlAPI +from opentrons.hardware_control.modules.absorbance_reader import AbsorbanceReader +from opentrons.hardware_control.types import EstopState +from opentrons.protocol_engine.commands.move_labware import MoveLabware from opentrons.protocol_engine.types import CSVRuntimeParamPaths from opentrons.protocol_engine import ( errors as pe_errors, @@ -28,6 +32,7 @@ from robot_server.protocols.protocol_models import ProtocolKind from robot_server.service.dependencies import get_current_time, get_unique_id from robot_server.robot.control.dependencies import require_estop_in_good_state +from robot_server.hardware import get_hardware, get_robot_type_enum from robot_server.service.json_api import ( RequestModel, @@ -49,7 +54,7 @@ from robot_server.protocols.router import ProtocolNotFound from ..run_models import ( - PlateReaderState, + PlaceLabwareState, RunNotFoundError, ActiveNozzleLayout, RunCurrentState, @@ -566,6 +571,8 @@ async def get_run_commands_error( async def get_current_state( runId: str, run_data_manager: Annotated[RunDataManager, Depends(get_run_data_manager)], + hardware: Annotated[HardwareControlAPI, Depends(get_hardware)], + robot_type: Annotated[RobotTypeEnum, Depends(get_robot_type_enum)], ) -> PydanticResponse[Body[RunCurrentState, CurrentStateLinks]]: """Get current state associated with a run if the run is current. @@ -574,6 +581,7 @@ async def get_current_state( run_data_manager: Run data retrieval interface. """ try: + run = run_data_manager.get(run_id=runId) active_nozzle_maps = run_data_manager.get_nozzle_maps(run_id=runId) nozzle_layouts = { @@ -599,18 +607,52 @@ async def get_current_state( else None ) - # TODO: add the actual plate reader data - plate_reader_states = { - "test": PlateReaderState( - lidHeldByGripper=False, - plateReaderLidLocation="D3", + estop_engaged = False + place_labware = None + if robot_type == RobotTypeEnum.FLEX: + estop_engaged = hardware.get_estop_state() in [ + EstopState.PHYSICALLY_ENGAGED, + EstopState.LOGICALLY_ENGAGED, + ] + + command = ( + run_data_manager.get_command(runId, current_command.command_id) + if current_command + else None ) - } + if isinstance(command, MoveLabware): + place_down = False + labware_id = command.params.labwareId + location = command.params.newLocation + if estop_engaged: + for mod in run.modules: + # Check if the plate reader lid was being moved and needs to be placed down. + if ( + not isinstance(mod, AbsorbanceReader) + and mod.location != location + ): + continue + for hw_mod in hardware.attached_modules: + if ( + hw_mod.serial_number == mod.serialNumber + and hw_mod.live_data.get("lidStatus") != "on" + ): + place_down = True + break + if place_down: + break + place_labware = PlaceLabwareState( + location=location, + labwareId=labware_id, + shouldPlaceDown=place_down, + ) return await PydanticResponse.create( content=Body.construct( data=RunCurrentState.construct( - activeNozzleLayouts=nozzle_layouts, plateReaderState=plate_reader_states + estopEngaged=estop_engaged, + activeNozzleLayouts=nozzle_layouts, + placeLabwareState=place_labware, ), links=links, ), diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index 6ba9a94ebb5..c90f02cb7b4 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -21,6 +21,7 @@ CommandNote, ) from opentrons.protocol_engine.types import ( + LabwareLocation, RunTimeParameter, PrimitiveRunTimeParamValuesType, CSVRunTimeParamFilesType, @@ -307,22 +308,24 @@ class ActiveNozzleLayout(BaseModel): ) -class PlateReaderState(BaseModel): - """Details about the plate reader.""" +class PlaceLabwareState(BaseModel): + """Details the labware being placed by the gripper.""" - plateReaderLidLocation: str = Field( + labwareId: str = Field(..., description="The ID of the labware to place.") + location: LabwareLocation = Field( ..., description="The location the Plate Reade lid should be in." ) - lidHeldByGripper: bool = Field( - ..., description="Whether the gripper is holding the Plate Reader lid." + shouldPlaceDown: bool = Field( + ..., description="Whether the gripper should place down the labware." ) class RunCurrentState(BaseModel): """Current details about a run.""" + estopEngaged: bool = Field(..., description="") activeNozzleLayouts: Dict[str, ActiveNozzleLayout] = Field(..., description="") - plateReaderState: Dict[str, PlateReaderState] = Field(..., description="") + placeLabwareState: Optional[PlaceLabwareState] = Field(None, description="") class CommandLinkNoMeta(BaseModel): diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index 8a6ed178827..a5411a43a86 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -4741,7 +4741,7 @@ }, "UnsafePlaceLabwareParams": { "title": "UnsafePlaceLabwareParams", - "description": "Payload required for an PlaceLabware command.", + "description": "Payload required for an UnsafePlaceLabware command.", "type": "object", "properties": { "labwareId": { @@ -4749,8 +4749,8 @@ "description": "The id of the labware to place.", "type": "string" }, - "newLocation": { - "title": "Newlocation", + "location": { + "title": "Location", "description": "Where to place the labware.", "anyOf": [ { @@ -4772,11 +4772,11 @@ ] } }, - "required": ["labwareId", "newLocation"] + "required": ["labwareId", "location"] }, "UnsafePlaceLabwareCreate": { "title": "UnsafePlaceLabwareCreate", - "description": "UnsafeEngageAxes command request model.", + "description": "UnsafePlaceLabware command request model.", "type": "object", "properties": { "commandType": { From 3ae5e2f18d01efb7ea3e7c8adc40ca980103b20d Mon Sep 17 00:00:00 2001 From: vegano1 Date: Mon, 21 Oct 2024 15:54:45 -0400 Subject: [PATCH 06/20] clean up --- .../commands/unsafe/unsafe_place_labware.py | 18 +++++-- .../robot_server/runs/router/base_router.py | 48 ++++++++++--------- 2 files changed, 40 insertions(+), 26 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index e2fc9bb287c..44d34450472 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -13,7 +13,7 @@ ) from opentrons.types import Point -from ...types import DeckSlotLocation, LabwareLocation +from ...types import DeckSlotLocation, LabwareLocation, ModuleModel from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence from ...resources import ensure_ot3_hardware @@ -90,9 +90,19 @@ async def execute( params.location, ) - # NOTE: when we e-stop, the gantry position goes bad, so the robot needs to - # home x, y. This can be a problem if a pipette is holding tips. - # so maybe home everything? and then drop the tips? + # If this is an Aborbance Reader, and the lid is already on, just ungrip and home the gripper. + if isinstance(location, DeckSlotLocation): + module = self._state_view.modules.get_by_slot(location.slotName) + if module and module.model == ModuleModel.ABSORBANCE_READER_V1: + for hw_mod in ot3api.attached_modules: + lid_status = hw_mod.live_data["data"].get('lidStatus') + if hw_mod.serial_number == module.serialNumber and lid_status == 'on': + await ot3api.ungrip() + await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]) + return SuccessData(public=UnsafePlaceLabwareResult(), private=None) + + # NOTE: When the estop is pressed, the gantry loses postion, + # so the robot needs to home x, y to sync. await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G, Axis.X, Axis.Y]) gripper_homed_position = await ot3api.gantry_position( mount=OT3Mount.GRIPPER, diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index 9d0b35c69c0..5df721d7b46 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -16,6 +16,7 @@ from opentrons.hardware_control import HardwareControlAPI from opentrons.hardware_control.modules.absorbance_reader import AbsorbanceReader from opentrons.hardware_control.types import EstopState +from opentrons.protocol_engine.commands.absorbance_reader import CloseLid, OpenLid from opentrons.protocol_engine.commands.move_labware import MoveLabware from opentrons.protocol_engine.types import CSVRuntimeParamPaths from opentrons.protocol_engine import ( @@ -620,32 +621,35 @@ async def get_current_state( if current_command else None ) + + # Labware state when estop is engaged if isinstance(command, MoveLabware): - place_down = False - labware_id = command.params.labwareId - location = command.params.newLocation - if estop_engaged: - for mod in run.modules: - # Check if the plate reader lid was being moved and needs to be placed down. + place_labware = PlaceLabwareState( + location=command.params.newLocation, + labwareId=command.params.labwareId, + shouldPlaceDown=False, + ) + # Handle absorbance reader lid + elif isinstance(command, (OpenLid, CloseLid)): + for mod in run.modules: + if not isinstance(mod, AbsorbanceReader) and mod.id != command.params.moduleId: + continue + for hw_mod in hardware.attached_modules: + lid_status = hw_mod.live_data['data'].get('lidStatus') if ( - not isinstance(mod, AbsorbanceReader) - and mod.location != location + mod.location is not None + and hw_mod.serial_number == mod.serialNumber ): - continue - for hw_mod in hardware.attached_modules: - if ( - hw_mod.serial_number == mod.serialNumber - and hw_mod.live_data.get("lidStatus") != "on" - ): - place_down = True - break - if place_down: + location = mod.location + labware_id = f"{mod.model}Lid{location.slotName}" + place_labware = PlaceLabwareState( + location=location, + labwareId=labware_id, + shouldPlaceDown=estop_engaged, + ) break - place_labware = PlaceLabwareState( - location=location, - labwareId=labware_id, - shouldPlaceDown=place_down, - ) + if place_labware: + break return await PydanticResponse.create( content=Body.construct( From f0b4766056db12b965c5c35fe87a720811a645fa Mon Sep 17 00:00:00 2001 From: vegano1 Date: Mon, 21 Oct 2024 15:56:26 -0400 Subject: [PATCH 07/20] format --- .../commands/unsafe/unsafe_place_labware.py | 13 ++++++++----- .../robot_server/runs/router/base_router.py | 7 +++++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index 44d34450472..5ad874757b1 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -72,8 +72,6 @@ async def execute( labware_id = params.labwareId self._state_view.labware.get(labware_id=labware_id) - # TODO: do we need these offsets instead? - # self._state_view.geometry.get_final_labware_movement_offset_vectors final_offsets = self._state_view.labware.get_labware_gripper_offsets( labware_id, None ) @@ -95,11 +93,16 @@ async def execute( module = self._state_view.modules.get_by_slot(location.slotName) if module and module.model == ModuleModel.ABSORBANCE_READER_V1: for hw_mod in ot3api.attached_modules: - lid_status = hw_mod.live_data["data"].get('lidStatus') - if hw_mod.serial_number == module.serialNumber and lid_status == 'on': + lid_status = hw_mod.live_data["data"].get("lidStatus") + if ( + hw_mod.serial_number == module.serialNumber + and lid_status == "on" + ): await ot3api.ungrip() await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]) - return SuccessData(public=UnsafePlaceLabwareResult(), private=None) + return SuccessData( + public=UnsafePlaceLabwareResult(), private=None + ) # NOTE: When the estop is pressed, the gantry loses postion, # so the robot needs to home x, y to sync. diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index 5df721d7b46..20b766c56ae 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -632,10 +632,13 @@ async def get_current_state( # Handle absorbance reader lid elif isinstance(command, (OpenLid, CloseLid)): for mod in run.modules: - if not isinstance(mod, AbsorbanceReader) and mod.id != command.params.moduleId: + if ( + not isinstance(mod, AbsorbanceReader) + and mod.id != command.params.moduleId + ): continue for hw_mod in hardware.attached_modules: - lid_status = hw_mod.live_data['data'].get('lidStatus') + lid_status = hw_mod.live_data["data"].get("lidStatus") if ( mod.location is not None and hw_mod.serial_number == mod.serialNumber From a35161b02d0ac7d59770384c1457759a2567adac Mon Sep 17 00:00:00 2001 From: vegano1 Date: Tue, 22 Oct 2024 10:51:34 -0400 Subject: [PATCH 08/20] api, always move the plate reader lid to its dock --- .../commands/unsafe/unsafe_place_labware.py | 40 ++++++++++++------- 1 file changed, 26 insertions(+), 14 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index 5ad874757b1..d78b3398357 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -17,11 +17,13 @@ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence from ...resources import ensure_ot3_hardware +from ...state.update_types import StateUpdate from opentrons.hardware_control import HardwareControlAPI if TYPE_CHECKING: from ...state.state import StateView + from ...execution.equipment import EquipmentHandler UnsafePlaceLabwareCommandType = Literal["unsafe/placeLabware"] @@ -50,10 +52,12 @@ def __init__( self, hardware_api: HardwareControlAPI, state_view: StateView, + equipment: EquipmentHandler, **kwargs: object, ) -> None: self._hardware_api = hardware_api self._state_view = state_view + self._equipment = equipment async def execute( self, params: UnsafePlaceLabwareParams @@ -70,7 +74,8 @@ async def execute( # Allow propagation of LabwareNotLoadedError. labware_id = params.labwareId - self._state_view.labware.get(labware_id=labware_id) + current_labware = self._state_view.labware.get(labware_id=labware_id) + definition_uri = current_labware.definitionUri final_offsets = self._state_view.labware.get_labware_gripper_offsets( labware_id, None @@ -88,21 +93,18 @@ async def execute( params.location, ) - # If this is an Aborbance Reader, and the lid is already on, just ungrip and home the gripper. + # This is an absorbance reader, move the lid to its dock (staging area). if isinstance(location, DeckSlotLocation): module = self._state_view.modules.get_by_slot(location.slotName) if module and module.model == ModuleModel.ABSORBANCE_READER_V1: - for hw_mod in ot3api.attached_modules: - lid_status = hw_mod.live_data["data"].get("lidStatus") - if ( - hw_mod.serial_number == module.serialNumber - and lid_status == "on" - ): - await ot3api.ungrip() - await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]) - return SuccessData( - public=UnsafePlaceLabwareResult(), private=None - ) + location = self._state_view.modules.absorbance_reader_dock_location( + module.id + ) + + new_offset_id = self._equipment.find_applicable_labware_offset_id( + labware_definition_uri=definition_uri, + labware_location=location, + ) # NOTE: When the estop is pressed, the gantry loses postion, # so the robot needs to home x, y to sync. @@ -140,7 +142,17 @@ async def execute( await ot3api.move_to( mount=OT3Mount.GRIPPER, abs_position=waypoint_data.position ) - return SuccessData(public=UnsafePlaceLabwareResult(), private=None) + + state_update = StateUpdate() + + state_update.set_labware_location( + labware_id=labware_id, + new_location=location, + new_offset_id=new_offset_id, + ) + return SuccessData( + public=UnsafePlaceLabwareResult(), private=None, state_update=state_update + ) class UnsafePlaceLabware( From c7e8264b2c908cda140ce91d99c492ec66a4d55e Mon Sep 17 00:00:00 2001 From: vegano1 Date: Tue, 22 Oct 2024 11:13:41 -0400 Subject: [PATCH 09/20] feat(app, shared-data): add usePlacePlateReaderLid to EstopPressedModal to handle plate reader lid. --- app/src/App/OnDeviceDisplayApp.tsx | 2 +- .../EmergencyStop/EstopPressedModal.tsx | 27 ++++++++-- app/src/resources/modules/hooks/index.ts | 1 + .../modules/hooks/useDropPlateReaderLid.ts | 46 ----------------- .../modules/hooks/usePlacePlateReaderLid.ts | 49 +++++++++++++++++++ shared-data/command/types/unsafe.ts | 18 ++++++- 6 files changed, 90 insertions(+), 53 deletions(-) delete mode 100644 app/src/resources/modules/hooks/useDropPlateReaderLid.ts create mode 100644 app/src/resources/modules/hooks/usePlacePlateReaderLid.ts diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 42335754432..8dc3a362f80 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -178,7 +178,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => { // TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals return ( - + diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index 8dc996c3374..e1a255abdcb 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -23,6 +23,8 @@ import { } from '@opentrons/components' import { useAcknowledgeEstopDisengageMutation } from '@opentrons/react-api-client' +import { usePlacePlateReaderLid } from '/app/resources/modules' + import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' @@ -43,6 +45,7 @@ interface EstopPressedModalProps { isDismissedModal?: boolean setIsDismissedModal?: (isDismissedModal: boolean) => void isWaitingForLogicalDisengage: boolean + isWaitingForPlateReaderLidPlacement: boolean setShouldSeeLogicalDisengage: () => void } @@ -52,6 +55,7 @@ export function EstopPressedModal({ isDismissedModal, setIsDismissedModal, isWaitingForLogicalDisengage, + isWaitingForPlateReaderLidPlacement, setShouldSeeLogicalDisengage, }: EstopPressedModalProps): JSX.Element { const isOnDevice = useSelector(getIsOnDevice) @@ -61,6 +65,7 @@ export function EstopPressedModal({ isEngaged={isEngaged} closeModal={closeModal} isWaitingForLogicalDisengage={isWaitingForLogicalDisengage} + isWaitingForPlateReaderLidPlacement={isWaitingForPlateReaderLidPlacement} setShouldSeeLogicalDisengage={setShouldSeeLogicalDisengage} /> ) : ( @@ -71,6 +76,7 @@ export function EstopPressedModal({ closeModal={closeModal} setIsDismissedModal={setIsDismissedModal} isWaitingForLogicalDisengage={isWaitingForLogicalDisengage} + isWaitingForPlateReaderLidPlacement={isWaitingForPlateReaderLidPlacement} setShouldSeeLogicalDisengage={setShouldSeeLogicalDisengage} /> ) : null} @@ -84,11 +90,16 @@ function TouchscreenModal({ isEngaged, closeModal, isWaitingForLogicalDisengage, + isWaitingForPlateReaderLidPlacement, setShouldSeeLogicalDisengage, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation(['device_settings', 'branded']) const [isResuming, setIsResuming] = React.useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() + + const { placeReaderLid, isPlacing } = usePlacePlateReaderLid({ + pipetteInfo: null + }) const modalHeader: OddModalHeaderBaseProps = { title: t('estop_pressed'), iconName: 'ot-alert', @@ -102,6 +113,7 @@ function TouchscreenModal({ setIsResuming(true) acknowledgeEstopDisengage(null) setShouldSeeLogicalDisengage() + placeReaderLid() closeModal() } return ( @@ -131,15 +143,15 @@ function TouchscreenModal({ data-testid="Estop_pressed_button" width="100%" iconName={ - isResuming || isWaitingForLogicalDisengage + isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement ? 'ot-spinner' : undefined } iconPlacement={ - isResuming || isWaitingForLogicalDisengage ? 'startIcon' : undefined + isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement ? 'startIcon' : undefined } buttonText={t('resume_robot_operations')} - disabled={isEngaged || isResuming || isWaitingForLogicalDisengage} + disabled={isEngaged || isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement} onClick={handleClick} /> @@ -152,11 +164,15 @@ function DesktopModal({ closeModal, setIsDismissedModal, isWaitingForLogicalDisengage, + isWaitingForPlateReaderLidPlacement, setShouldSeeLogicalDisengage, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation('device_settings') const [isResuming, setIsResuming] = React.useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() + const { placeReaderLid, isPlacing } = usePlacePlateReaderLid({ + pipetteInfo: null + }) const handleCloseModal = (): void => { if (setIsDismissedModal != null) { @@ -182,6 +198,7 @@ function DesktopModal({ { onSuccess: () => { setShouldSeeLogicalDisengage() + placeReaderLid() closeModal() }, onError: (error: any) => { @@ -204,14 +221,14 @@ function DesktopModal({ - {isResuming || isWaitingForLogicalDisengage ? ( + {isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement ? ( ) : null} {t('resume_robot_operations')} diff --git a/app/src/resources/modules/hooks/index.ts b/app/src/resources/modules/hooks/index.ts index c38e5f46140..c26e43c8bfc 100644 --- a/app/src/resources/modules/hooks/index.ts +++ b/app/src/resources/modules/hooks/index.ts @@ -1 +1,2 @@ export * from './useAttachedModules' +export * from './usePlacePlateReaderLid' diff --git a/app/src/resources/modules/hooks/useDropPlateReaderLid.ts b/app/src/resources/modules/hooks/useDropPlateReaderLid.ts deleted file mode 100644 index b0234647c84..00000000000 --- a/app/src/resources/modules/hooks/useDropPlateReaderLid.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { useRobotControlCommands } from '/app/resources/maintenance_runs' - -import type { CreateCommand } from '@opentrons/shared-data' -import type { - UseRobotControlCommandsProps, - UseRobotControlCommandsResult, -} from '/app/resources/maintenance_runs' - -interface UseHomePipettesResult { - isHoming: UseRobotControlCommandsResult['isExecuting'] - homePipettes: UseRobotControlCommandsResult['executeCommands'] -} - -export type UseDropPlateReaderLidProps = Pick< - UseRobotControlCommandsProps, - 'pipetteInfo' | 'onSettled' -> - -export function useDropPlateReaderLid( - props: UseDropPlateReaderLidProps -): UseHomePipettesResult { - const { executeCommands, isExecuting } = useRobotControlCommands({ - ...props, - commands: [LOAD_PLATE_READER, DROP_PLATE_READER_LID], - continuePastCommandFailure: true, - }) - - return { - isHoming: isExecuting, - homePipettes: executeCommands, - } -} - -const LOAD_PLATE_READER: CreateCommand = { - commandType: 'loadModule' as const, - params: { model: 'absorbanceReaderV1', location: { slotName: 'C3' } }, -} - -const DROP_PLATE_READER_LID: CreateCommand = { - commandType: 'moveLabware' as const, - params: { - labwareId: 'absorbanceReaderV1LidC3', - newLocation: { slotName: 'C3' }, - strategy: 'usingGripper', - }, -} diff --git a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts new file mode 100644 index 00000000000..5fcacd48cc2 --- /dev/null +++ b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts @@ -0,0 +1,49 @@ +import { useRobotControlCommands } from '/app/resources/maintenance_runs' + +import { LabwareLocation, type CreateCommand } from '@opentrons/shared-data' +import type { + UseRobotControlCommandsProps, + UseRobotControlCommandsResult, +} from '/app/resources/maintenance_runs' + +interface UsePlacePlateReaderLidResult { + isPlacing: UseRobotControlCommandsResult['isExecuting'] + placeReaderLid: UseRobotControlCommandsResult['executeCommands'] +} + +export type UsePlacePlateReaderLidProps = Pick< + UseRobotControlCommandsProps, + 'pipetteInfo' | 'onSettled' +> + +// TODO: Need to conditionally run this function based on `runs/currentState` value +export function usePlacePlateReaderLid( + props: UsePlacePlateReaderLidProps +): UsePlacePlateReaderLidResult { + const labwareId: string = 'absorbanceReaderV1LidC3' + const location: LabwareLocation = {slotName: 'C3'} + + const LOAD_PLATE_READER: CreateCommand = { + commandType: 'loadModule' as const, + params: { model: 'absorbanceReaderV1', location: location }, + } + + const PLACE_READER_LID: CreateCommand = { + commandType: 'unsafe/placeLabware' as const, + params: { + labwareId: labwareId, + location: location, + }, + } + + const { executeCommands, isExecuting } = useRobotControlCommands({ + ...props, + commands: [LOAD_PLATE_READER, PLACE_READER_LID], + continuePastCommandFailure: true, + }) + + return { + isPlacing: isExecuting, + placeReaderLid: executeCommands, + } +} diff --git a/shared-data/command/types/unsafe.ts b/shared-data/command/types/unsafe.ts index d24a6f8e054..6b20b18c49b 100644 --- a/shared-data/command/types/unsafe.ts +++ b/shared-data/command/types/unsafe.ts @@ -1,4 +1,4 @@ -import type { CommonCommandRunTimeInfo, CommonCommandCreateInfo } from '.' +import type { CommonCommandRunTimeInfo, CommonCommandCreateInfo, LabwareLocation } from '.' import type { MotorAxes } from '../../js/types' export type UnsafeRunTimeCommand = @@ -7,6 +7,7 @@ export type UnsafeRunTimeCommand = | UnsafeUpdatePositionEstimatorsRunTimeCommand | UnsafeEngageAxesRunTimeCommand | UnsafeUngripLabwareRunTimeCommand + | UnsafePlaceLabwareRunTimeCommand export type UnsafeCreateCommand = | UnsafeBlowoutInPlaceCreateCommand @@ -14,6 +15,7 @@ export type UnsafeCreateCommand = | UnsafeUpdatePositionEstimatorsCreateCommand | UnsafeEngageAxesCreateCommand | UnsafeUngripLabwareCreateCommand + | UnsafePlaceLabwareCreateCommand export interface UnsafeBlowoutInPlaceParams { pipetteId: string @@ -85,3 +87,17 @@ export interface UnsafeUngripLabwareRunTimeCommand UnsafeUngripLabwareCreateCommand { result?: any } +export interface UnsafePlaceLabwareParams { + labwareId: string, + location: LabwareLocation, +} +export interface UnsafePlaceLabwareCreateCommand + extends CommonCommandCreateInfo { + commandType: 'unsafe/placeLabware' + params: UnsafePlaceLabwareParams +} +export interface UnsafePlaceLabwareRunTimeCommand + extends CommonCommandRunTimeInfo, + UnsafePlaceLabwareCreateCommand { + result?: any +} From 2a7a4b427527a9160495c84b81d19f90843bbae3 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Tue, 22 Oct 2024 14:48:55 -0400 Subject: [PATCH 10/20] save --- api-client/src/runs/types.ts | 9 ++++++++ .../EmergencyStop/EstopPressedModal.tsx | 2 +- .../organisms/EmergencyStop/EstopTakeover.tsx | 5 ++++ .../modules/hooks/usePlacePlateReaderLid.ts | 23 +++++++++++++++---- 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/api-client/src/runs/types.ts b/api-client/src/runs/types.ts index 0415367f1e6..c42752b0eb9 100644 --- a/api-client/src/runs/types.ts +++ b/api-client/src/runs/types.ts @@ -8,6 +8,7 @@ import type { RunTimeCommand, RunTimeParameter, NozzleLayoutConfig, + LabwareLocation, } from '@opentrons/shared-data' import type { ResourceLink, ErrorDetails } from '../types' export * from './commands/types' @@ -116,7 +117,9 @@ export interface Runs { } export interface RunCurrentStateData { + estopEngaged: boolean activeNozzleLayouts: Record // keyed by pipetteId + placeLabwareState?: PlaceLabwareState } export const RUN_ACTION_TYPE_PLAY: 'play' = 'play' @@ -201,3 +204,9 @@ export interface NozzleLayoutValues { activeNozzles: string[] config: NozzleLayoutConfig } + +export interface PlaceLabwareState { + labwareId: string + location: LabwareLocation + shouldPlaceDown: boolean +} diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index e1a255abdcb..0266c1f6c07 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -143,7 +143,7 @@ function TouchscreenModal({ data-testid="Estop_pressed_button" width="100%" iconName={ - isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement + isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement ? 'ot-spinner' : undefined } diff --git a/app/src/organisms/EmergencyStop/EstopTakeover.tsx b/app/src/organisms/EmergencyStop/EstopTakeover.tsx index 5967edae75a..9460eddff54 100644 --- a/app/src/organisms/EmergencyStop/EstopTakeover.tsx +++ b/app/src/organisms/EmergencyStop/EstopTakeover.tsx @@ -27,6 +27,10 @@ export function EstopTakeover({ robotName }: EstopTakeoverProps): JSX.Element { isWaitingForLogicalDisengage, setIsWaitingForLogicalDisengage, ] = useState(false) + const [ + isWaitingForPlateReaderLidPlacement, + setisWaitingForPlateReaderLidPlacement, + ] = useState(false) const { data: estopStatus } = useEstopQuery({ refetchInterval: estopEngaged ? ESTOP_CURRENTLY_ENGAGED_REFETCH_INTERVAL_MS @@ -65,6 +69,7 @@ export function EstopTakeover({ robotName }: EstopTakeoverProps): JSX.Element { isDismissedModal={isEmergencyStopModalDismissed} setIsDismissedModal={setIsEmergencyStopModalDismissed} isWaitingForLogicalDisengage={isWaitingForLogicalDisengage} + isWaitingForPlateReaderLidPlacement={isWaitingForPlateReaderLidPlacement} setShouldSeeLogicalDisengage={() => { setIsWaitingForLogicalDisengage(true) }} diff --git a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts index 5fcacd48cc2..01b73099246 100644 --- a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts +++ b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts @@ -5,6 +5,8 @@ import type { UseRobotControlCommandsProps, UseRobotControlCommandsResult, } from '/app/resources/maintenance_runs' +import { useRunCurrentState } from '@opentrons/react-api-client' +import { useCurrentRunId } from '../../runs' interface UsePlacePlateReaderLidResult { isPlacing: UseRobotControlCommandsResult['isExecuting'] @@ -16,12 +18,15 @@ export type UsePlacePlateReaderLidProps = Pick< 'pipetteInfo' | 'onSettled' > -// TODO: Need to conditionally run this function based on `runs/currentState` value export function usePlacePlateReaderLid( props: UsePlacePlateReaderLidProps ): UsePlacePlateReaderLidResult { - const labwareId: string = 'absorbanceReaderV1LidC3' - const location: LabwareLocation = {slotName: 'C3'} + const runId = useCurrentRunId() + const { data: runCurrentState } = useRunCurrentState(runId) + const estopEngaged = runCurrentState?.data.estopEngaged + const placeLabware = runCurrentState?.data.placeLabwareState?.shouldPlaceDown + const labwareId = runCurrentState?.data.placeLabwareState?.labwareId + const location = runCurrentState?.data.placeLabwareState?.location const LOAD_PLATE_READER: CreateCommand = { commandType: 'loadModule' as const, @@ -42,8 +47,18 @@ export function usePlacePlateReaderLid( continuePastCommandFailure: true, }) + const decideFunction = (): void => { + console.log("DECIDE!") + if (estopEngaged != null && placeLabware) { + console.log("PLACE") + executeCommands() + } else { + console.log("DONT PLACE") + } + } + return { isPlacing: isExecuting, - placeReaderLid: executeCommands, + placeReaderLid: decideFunction, } } From 2cb3765dddc0238ae08abeefce336cb39daf4c23 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Tue, 22 Oct 2024 18:57:49 -0400 Subject: [PATCH 11/20] conditionally call unsafe/placeLabware command based on the runs/currentState --- app/src/App/OnDeviceDisplayApp.tsx | 2 +- .../EmergencyStop/EstopPressedModal.tsx | 48 ++++++-- .../organisms/EmergencyStop/EstopTakeover.tsx | 4 +- .../modules/hooks/usePlacePlateReaderLid.ts | 114 +++++++++++------- robot-server/tests/runs/router/conftest.py | 7 ++ .../tests/runs/router/test_base_router.py | 5 + shared-data/command/types/unsafe.ts | 10 +- 7 files changed, 132 insertions(+), 58 deletions(-) diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 8dc3a362f80..42335754432 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -178,7 +178,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => { // TODO (sb:6/12/23) Create a notification manager to set up preference and order of takeover modals return ( - + diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index 0266c1f6c07..dbf618b06f4 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -23,9 +23,8 @@ import { } from '@opentrons/components' import { useAcknowledgeEstopDisengageMutation } from '@opentrons/react-api-client' -import { usePlacePlateReaderLid } from '/app/resources/modules' - +import { usePlacePlateReaderLid } from '/app/resources/modules' import { getTopPortalEl } from '/app/App/portal' import { SmallButton } from '/app/atoms/buttons' import { OddModal } from '/app/molecules/OddModal' @@ -65,7 +64,9 @@ export function EstopPressedModal({ isEngaged={isEngaged} closeModal={closeModal} isWaitingForLogicalDisengage={isWaitingForLogicalDisengage} - isWaitingForPlateReaderLidPlacement={isWaitingForPlateReaderLidPlacement} + isWaitingForPlateReaderLidPlacement={ + isWaitingForPlateReaderLidPlacement + } setShouldSeeLogicalDisengage={setShouldSeeLogicalDisengage} /> ) : ( @@ -76,7 +77,9 @@ export function EstopPressedModal({ closeModal={closeModal} setIsDismissedModal={setIsDismissedModal} isWaitingForLogicalDisengage={isWaitingForLogicalDisengage} - isWaitingForPlateReaderLidPlacement={isWaitingForPlateReaderLidPlacement} + isWaitingForPlateReaderLidPlacement={ + isWaitingForPlateReaderLidPlacement + } setShouldSeeLogicalDisengage={setShouldSeeLogicalDisengage} /> ) : null} @@ -98,7 +101,7 @@ function TouchscreenModal({ const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const { placeReaderLid, isPlacing } = usePlacePlateReaderLid({ - pipetteInfo: null + pipetteInfo: null, }) const modalHeader: OddModalHeaderBaseProps = { title: t('estop_pressed'), @@ -143,15 +146,29 @@ function TouchscreenModal({ data-testid="Estop_pressed_button" width="100%" iconName={ - isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement + isResuming || + isPlacing || + isWaitingForLogicalDisengage || + isWaitingForPlateReaderLidPlacement ? 'ot-spinner' : undefined } iconPlacement={ - isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement ? 'startIcon' : undefined + isResuming || + isPlacing || + isWaitingForLogicalDisengage || + isWaitingForPlateReaderLidPlacement + ? 'startIcon' + : undefined } buttonText={t('resume_robot_operations')} - disabled={isEngaged || isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement} + disabled={ + isEngaged || + isResuming || + isPlacing || + isWaitingForLogicalDisengage || + isWaitingForPlateReaderLidPlacement + } onClick={handleClick} /> @@ -171,7 +188,7 @@ function DesktopModal({ const [isResuming, setIsResuming] = React.useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() const { placeReaderLid, isPlacing } = usePlacePlateReaderLid({ - pipetteInfo: null + pipetteInfo: null, }) const handleCloseModal = (): void => { @@ -221,14 +238,23 @@ function DesktopModal({ - {isResuming || isPlacing || isWaitingForLogicalDisengage || isWaitingForPlateReaderLidPlacement ? ( + {isResuming || + isPlacing || + isWaitingForLogicalDisengage || + isWaitingForPlateReaderLidPlacement ? ( ) : null} {t('resume_robot_operations')} diff --git a/app/src/organisms/EmergencyStop/EstopTakeover.tsx b/app/src/organisms/EmergencyStop/EstopTakeover.tsx index 9460eddff54..716511df7c0 100644 --- a/app/src/organisms/EmergencyStop/EstopTakeover.tsx +++ b/app/src/organisms/EmergencyStop/EstopTakeover.tsx @@ -69,7 +69,9 @@ export function EstopTakeover({ robotName }: EstopTakeoverProps): JSX.Element { isDismissedModal={isEmergencyStopModalDismissed} setIsDismissedModal={setIsEmergencyStopModalDismissed} isWaitingForLogicalDisengage={isWaitingForLogicalDisengage} - isWaitingForPlateReaderLidPlacement={isWaitingForPlateReaderLidPlacement} + isWaitingForPlateReaderLidPlacement={ + isWaitingForPlateReaderLidPlacement + } setShouldSeeLogicalDisengage={() => { setIsWaitingForLogicalDisengage(true) }} diff --git a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts index 01b73099246..54203c0d244 100644 --- a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts +++ b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts @@ -1,64 +1,94 @@ -import { useRobotControlCommands } from '/app/resources/maintenance_runs' +import { useState } from 'react' -import { LabwareLocation, type CreateCommand } from '@opentrons/shared-data' -import type { - UseRobotControlCommandsProps, - UseRobotControlCommandsResult, +import { LabwareLocation, type CreateCommand, ModuleLocation } from '@opentrons/shared-data' +import { + useChainMaintenanceCommands, } from '/app/resources/maintenance_runs' -import { useRunCurrentState } from '@opentrons/react-api-client' -import { useCurrentRunId } from '../../runs' +import { useDeleteMaintenanceRunMutation, useRunCurrentState } from '@opentrons/react-api-client' +import { useCreateTargetedMaintenanceRunMutation, useCurrentRunId } from '../../runs' +import { MaintenanceRun } from '@opentrons/api-client' interface UsePlacePlateReaderLidResult { - isPlacing: UseRobotControlCommandsResult['isExecuting'] - placeReaderLid: UseRobotControlCommandsResult['executeCommands'] + placeReaderLid: () => Promise + isPlacing: boolean } -export type UsePlacePlateReaderLidProps = Pick< - UseRobotControlCommandsProps, - 'pipetteInfo' | 'onSettled' -> +export interface UsePlacePlateReaderLidProps { + +} export function usePlacePlateReaderLid( props: UsePlacePlateReaderLidProps ): UsePlacePlateReaderLidResult { + const [isPlacing, setIsPlacing] = useState(false) + const { chainRunCommands } = useChainMaintenanceCommands() + const { + mutateAsync: deleteMaintenanceRun, + } = useDeleteMaintenanceRunMutation() + const runId = useCurrentRunId() const { data: runCurrentState } = useRunCurrentState(runId) - const estopEngaged = runCurrentState?.data.estopEngaged - const placeLabware = runCurrentState?.data.placeLabwareState?.shouldPlaceDown - const labwareId = runCurrentState?.data.placeLabwareState?.labwareId - const location = runCurrentState?.data.placeLabwareState?.location + const placeLabware = runCurrentState?.data.placeLabwareState ?? null - const LOAD_PLATE_READER: CreateCommand = { - commandType: 'loadModule' as const, - params: { model: 'absorbanceReaderV1', location: location }, - } + const { + createTargetedMaintenanceRun, + } = useCreateTargetedMaintenanceRunMutation({ + onSuccess: response => { + const runId = response.data.id as string - const PLACE_READER_LID: CreateCommand = { - commandType: 'unsafe/placeLabware' as const, - params: { - labwareId: labwareId, - location: location, - }, - } + const loadModuleIfSupplied = (): Promise => { + if (placeLabware !== null && placeLabware.shouldPlaceDown) { + const location = placeLabware.location + const labwareId = placeLabware.labwareId + const moduleLocation: ModuleLocation = location as ModuleLocation + const loadModuleCommand = buildLoadModuleCommand(moduleLocation) + const placeLabwareCommand = buildPlaceLabwareCommand(labwareId, location) + console.log({location, labwareId}) + console.log({loadModuleCommand, placeLabwareCommand}) + return chainRunCommands(runId, [loadModuleCommand, placeLabwareCommand], false) + .then(() => Promise.resolve()) + .catch((error: Error) => { + console.error(error.message) + }) + } + return Promise.resolve() + } - const { executeCommands, isExecuting } = useRobotControlCommands({ - ...props, - commands: [LOAD_PLATE_READER, PLACE_READER_LID], - continuePastCommandFailure: true, + loadModuleIfSupplied() + .catch((error: Error) => { + console.error(error.message) + }) + .finally(() => + deleteMaintenanceRun(runId).catch((error: Error) => { + console.error('Failed to delete maintenance run:', error.message) + })) + setIsPlacing(false) + } }) - const decideFunction = (): void => { - console.log("DECIDE!") - if (estopEngaged != null && placeLabware) { - console.log("PLACE") - executeCommands() - } else { - console.log("DONT PLACE") - } + const placeReaderLid = (): Promise => { + setIsPlacing(true) + return createTargetedMaintenanceRun({}) } + return { placeReaderLid, isPlacing } +} + +const buildLoadModuleCommand = ( + location: ModuleLocation +): CreateCommand => { return { - isPlacing: isExecuting, - placeReaderLid: decideFunction, + commandType: 'loadModule' as const, + params: { model: 'absorbanceReaderV1', location: location }, + } +} + +const buildPlaceLabwareCommand = ( + labwaerId: string, + location: LabwareLocation +): CreateCommand => { + return { + commandType: 'unsafe/placeLabware' as const, + params: { labwareId, location }, } } diff --git a/robot-server/tests/runs/router/conftest.py b/robot-server/tests/runs/router/conftest.py index 700be63f4ef..7fce02963b2 100644 --- a/robot-server/tests/runs/router/conftest.py +++ b/robot-server/tests/runs/router/conftest.py @@ -1,4 +1,5 @@ """Common test fixtures for runs route tests.""" +from opentrons.hardware_control import HardwareControlAPI, OT3HardwareControlAPI import pytest from decoy import Decoy @@ -63,3 +64,9 @@ def mock_maintenance_run_orchestrator_store( def mock_deck_configuration_store(decoy: Decoy) -> DeckConfigurationStore: """Get a mock DeckConfigurationStore.""" return decoy.mock(cls=DeckConfigurationStore) + + +@pytest.fixture +def mock_hardware_api(decoy: Decoy) -> HardwareControlAPI: + """Get a mock HardwareControlAPI.""" + return decoy.mock(cls=OT3HardwareControlAPI) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 8a10af1940d..e77caf78863 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -1,6 +1,8 @@ """Tests for base /runs routes.""" from typing import Dict +from opentrons.hardware_control import HardwareControlAPI +from opentrons_shared_data.robot.types import RobotTypeEnum import pytest from datetime import datetime from decoy import Decoy @@ -843,6 +845,7 @@ async def test_get_run_commands_errors_defualt_cursor( async def test_get_current_state_success( decoy: Decoy, mock_run_data_manager: RunDataManager, + mock_hardware_api: HardwareControlAPI, mock_nozzle_maps: Dict[str, NozzleMap], ) -> None: """It should return the active nozzle layout for a specific pipette.""" @@ -863,6 +866,8 @@ async def test_get_current_state_success( result = await get_current_state( runId=run_id, run_data_manager=mock_run_data_manager, + hardware=mock_hardware_api, + robot_type=RobotTypeEnum.FLEX, ) assert result.status_code == 200 diff --git a/shared-data/command/types/unsafe.ts b/shared-data/command/types/unsafe.ts index 6b20b18c49b..3a9b91c2d31 100644 --- a/shared-data/command/types/unsafe.ts +++ b/shared-data/command/types/unsafe.ts @@ -1,4 +1,8 @@ -import type { CommonCommandRunTimeInfo, CommonCommandCreateInfo, LabwareLocation } from '.' +import type { + CommonCommandRunTimeInfo, + CommonCommandCreateInfo, + LabwareLocation, +} from '.' import type { MotorAxes } from '../../js/types' export type UnsafeRunTimeCommand = @@ -88,8 +92,8 @@ export interface UnsafeUngripLabwareRunTimeCommand result?: any } export interface UnsafePlaceLabwareParams { - labwareId: string, - location: LabwareLocation, + labwareId: string + location: LabwareLocation } export interface UnsafePlaceLabwareCreateCommand extends CommonCommandCreateInfo { From 837fe9a4586aa3defed5f792a346aef6ff038f73 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Wed, 23 Oct 2024 13:37:50 -0400 Subject: [PATCH 12/20] simplify usePlacePlateReaderLid hook --- .../EmergencyStop/EstopPressedModal.tsx | 6 +- .../modules/hooks/usePlacePlateReaderLid.ts | 100 +++++++----------- 2 files changed, 44 insertions(+), 62 deletions(-) diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index dbf618b06f4..191ba1c5e05 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -100,8 +100,8 @@ function TouchscreenModal({ const [isResuming, setIsResuming] = React.useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() - const { placeReaderLid, isPlacing } = usePlacePlateReaderLid({ - pipetteInfo: null, + const { handlePlaceReaderLid, isPlacing } = usePlacePlateReaderLid({ + onSettled: undefined, }) const modalHeader: OddModalHeaderBaseProps = { title: t('estop_pressed'), @@ -116,7 +116,7 @@ function TouchscreenModal({ setIsResuming(true) acknowledgeEstopDisengage(null) setShouldSeeLogicalDisengage() - placeReaderLid() + handlePlaceReaderLid() closeModal() } return ( diff --git a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts index 54203c0d244..721f4fe4a97 100644 --- a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts +++ b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts @@ -1,82 +1,64 @@ -import { useState } from 'react' - -import { LabwareLocation, type CreateCommand, ModuleLocation } from '@opentrons/shared-data' import { - useChainMaintenanceCommands, -} from '/app/resources/maintenance_runs' -import { useDeleteMaintenanceRunMutation, useRunCurrentState } from '@opentrons/react-api-client' -import { useCreateTargetedMaintenanceRunMutation, useCurrentRunId } from '../../runs' -import { MaintenanceRun } from '@opentrons/api-client' + CreateCommand, + LabwareLocation, + ModuleLocation, +} from '@opentrons/shared-data' +import { UseRobotControlCommandsProps } from '/app/resources/maintenance_runs' +import { useRunCurrentState } from '@opentrons/react-api-client' +import { useCurrentRunId } from '../../runs' +import { useRobotControlCommands } from '/app/resources/maintenance_runs' interface UsePlacePlateReaderLidResult { - placeReaderLid: () => Promise + handlePlaceReaderLid: () => Promise isPlacing: boolean } -export interface UsePlacePlateReaderLidProps { - -} +type UsePlacePlateReaderLidProps = Pick< + UseRobotControlCommandsProps, + 'onSettled' +> export function usePlacePlateReaderLid( props: UsePlacePlateReaderLidProps ): UsePlacePlateReaderLidResult { - const [isPlacing, setIsPlacing] = useState(false) - const { chainRunCommands } = useChainMaintenanceCommands() - const { - mutateAsync: deleteMaintenanceRun, - } = useDeleteMaintenanceRunMutation() - const runId = useCurrentRunId() const { data: runCurrentState } = useRunCurrentState(runId) - const placeLabware = runCurrentState?.data.placeLabwareState ?? null - const { - createTargetedMaintenanceRun, - } = useCreateTargetedMaintenanceRunMutation({ - onSuccess: response => { - const runId = response.data.id as string + const placeLabware = runCurrentState?.data.placeLabwareState ?? null + const isValidPlateReaderMove = + placeLabware !== null && placeLabware.shouldPlaceDown - const loadModuleIfSupplied = (): Promise => { - if (placeLabware !== null && placeLabware.shouldPlaceDown) { - const location = placeLabware.location - const labwareId = placeLabware.labwareId - const moduleLocation: ModuleLocation = location as ModuleLocation - const loadModuleCommand = buildLoadModuleCommand(moduleLocation) - const placeLabwareCommand = buildPlaceLabwareCommand(labwareId, location) - console.log({location, labwareId}) - console.log({loadModuleCommand, placeLabwareCommand}) - return chainRunCommands(runId, [loadModuleCommand, placeLabwareCommand], false) - .then(() => Promise.resolve()) - .catch((error: Error) => { - console.error(error.message) - }) - } - return Promise.resolve() - } + // TODO eventually load module support for useRobotControlCommands + let commandsToExecute: CreateCommand[] = [] + if (isValidPlateReaderMove) { + const location = placeLabware.location + const loadModuleCommand = buildLoadModuleCommand(location as ModuleLocation) + const placeLabwareCommand = buildPlaceLabwareCommand( + placeLabware.labwareId as string, + location + ) + commandsToExecute = [loadModuleCommand, placeLabwareCommand] + } - loadModuleIfSupplied() - .catch((error: Error) => { - console.error(error.message) - }) - .finally(() => - deleteMaintenanceRun(runId).catch((error: Error) => { - console.error('Failed to delete maintenance run:', error.message) - })) - setIsPlacing(false) - } + const { executeCommands, isExecuting } = useRobotControlCommands({ + ...props, + pipetteInfo: null, + commands: commandsToExecute, + continuePastCommandFailure: true, }) - const placeReaderLid = (): Promise => { - setIsPlacing(true) - return createTargetedMaintenanceRun({}) + const handlePlaceReaderLid = (): Promise => { + if (isValidPlateReaderMove) { + return executeCommands().then(() => Promise.resolve()) + } else { + return Promise.resolve() + } } - return { placeReaderLid, isPlacing } + return { handlePlaceReaderLid, isPlacing: isExecuting } } -const buildLoadModuleCommand = ( - location: ModuleLocation -): CreateCommand => { +const buildLoadModuleCommand = (location: ModuleLocation): CreateCommand => { return { commandType: 'loadModule' as const, params: { model: 'absorbanceReaderV1', location: location }, @@ -84,7 +66,7 @@ const buildLoadModuleCommand = ( } const buildPlaceLabwareCommand = ( - labwaerId: string, + labwareId: string, location: LabwareLocation ): CreateCommand => { return { From 6861d3d12b6cfbc87cdbccb49b3d05ea9ecf8629 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Wed, 23 Oct 2024 14:18:55 -0400 Subject: [PATCH 13/20] fix bad edge merge --- robot-server/robot_server/runs/router/base_router.py | 1 + .../robot_server/service/notifications/publisher_notifier.py | 2 ++ 2 files changed, 3 insertions(+) diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index 7d01f4679ca..df8264a0190 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -601,6 +601,7 @@ async def get_current_state( for pipetteId, nozzle_map in active_nozzle_maps.items() } + current_command = run_data_manager.get_current_command(run_id=runId) last_completed_command = run_data_manager.get_last_completed_command( run_id=runId ) diff --git a/robot-server/robot_server/service/notifications/publisher_notifier.py b/robot-server/robot_server/service/notifications/publisher_notifier.py index 89a53e27b64..4701aa83718 100644 --- a/robot-server/robot_server/service/notifications/publisher_notifier.py +++ b/robot-server/robot_server/service/notifications/publisher_notifier.py @@ -51,6 +51,8 @@ async def _wait_for_event(self) -> None: LOG.exception( f'PublisherNotifier: exception in callback {getattr(callback, "__name__", "")}' ) + except asyncio.exceptions.CancelledError: + LOG.warning("PublisherNotifuer task cancelled.") except BaseException: LOG.exception("PublisherNotifer notify task failed") From 6c4de3ef9a2463cff693bd65c899bfe514b4c753 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Wed, 23 Oct 2024 15:00:49 -0400 Subject: [PATCH 14/20] move EstopTakeover component below maintenanceRunTakeover so we dont show the remote-controlled takeover modal when we are automatically moving the plate reader lid --- app/src/App/OnDeviceDisplayApp.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index 42335754432..d72eabf3bc9 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -187,9 +187,9 @@ export const OnDeviceDisplayApp = (): JSX.Element => { ) : ( <> - + From de39cbebd44808d37df5abd300700f4b81bee875 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Thu, 24 Oct 2024 10:18:01 -0400 Subject: [PATCH 15/20] refactor estopPressed --- app/src/App/OnDeviceDisplayApp.tsx | 2 +- .../EmergencyStop/EstopPressedModal.tsx | 134 ++++++------------ .../organisms/EmergencyStop/EstopTakeover.tsx | 95 +++++-------- .../modules/hooks/usePlacePlateReaderLid.ts | 9 +- 4 files changed, 86 insertions(+), 154 deletions(-) diff --git a/app/src/App/OnDeviceDisplayApp.tsx b/app/src/App/OnDeviceDisplayApp.tsx index d72eabf3bc9..5ab0598587a 100644 --- a/app/src/App/OnDeviceDisplayApp.tsx +++ b/app/src/App/OnDeviceDisplayApp.tsx @@ -189,7 +189,7 @@ export const OnDeviceDisplayApp = (): JSX.Element => { <> - + diff --git a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx index 191ba1c5e05..7c78de6b8e2 100644 --- a/app/src/organisms/EmergencyStop/EstopPressedModal.tsx +++ b/app/src/organisms/EmergencyStop/EstopPressedModal.tsx @@ -41,21 +41,15 @@ import type { ModalProps } from '@opentrons/components' interface EstopPressedModalProps { isEngaged: boolean closeModal: () => void - isDismissedModal?: boolean - setIsDismissedModal?: (isDismissedModal: boolean) => void - isWaitingForLogicalDisengage: boolean - isWaitingForPlateReaderLidPlacement: boolean - setShouldSeeLogicalDisengage: () => void + isWaitingForResumeOperation: boolean + setIsWaitingForResumeOperation: () => void } export function EstopPressedModal({ isEngaged, closeModal, - isDismissedModal, - setIsDismissedModal, - isWaitingForLogicalDisengage, - isWaitingForPlateReaderLidPlacement, - setShouldSeeLogicalDisengage, + isWaitingForResumeOperation, + setIsWaitingForResumeOperation, }: EstopPressedModalProps): JSX.Element { const isOnDevice = useSelector(getIsOnDevice) return createPortal( @@ -63,26 +57,17 @@ export function EstopPressedModal({ ) : ( <> - {isDismissedModal === false ? ( - - ) : null} + ), getTopPortalEl() @@ -92,16 +77,18 @@ export function EstopPressedModal({ function TouchscreenModal({ isEngaged, closeModal, - isWaitingForLogicalDisengage, - isWaitingForPlateReaderLidPlacement, - setShouldSeeLogicalDisengage, + isWaitingForResumeOperation, + setIsWaitingForResumeOperation, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation(['device_settings', 'branded']) const [isResuming, setIsResuming] = React.useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() - const { handlePlaceReaderLid, isPlacing } = usePlacePlateReaderLid({ - onSettled: undefined, + const { + handlePlaceReaderLid, + isValidPlateReaderMove, + } = usePlacePlateReaderLid({ + onSettled: closeModal, }) const modalHeader: OddModalHeaderBaseProps = { title: t('estop_pressed'), @@ -114,10 +101,12 @@ function TouchscreenModal({ } const handleClick = (): void => { setIsResuming(true) + setIsWaitingForResumeOperation() acknowledgeEstopDisengage(null) - setShouldSeeLogicalDisengage() handlePlaceReaderLid() - closeModal() + if (!isValidPlateReaderMove) { + closeModal() + } } return ( @@ -146,29 +135,13 @@ function TouchscreenModal({ data-testid="Estop_pressed_button" width="100%" iconName={ - isResuming || - isPlacing || - isWaitingForLogicalDisengage || - isWaitingForPlateReaderLidPlacement - ? 'ot-spinner' - : undefined + isResuming || isWaitingForResumeOperation ? 'ot-spinner' : undefined } iconPlacement={ - isResuming || - isPlacing || - isWaitingForLogicalDisengage || - isWaitingForPlateReaderLidPlacement - ? 'startIcon' - : undefined + isResuming || isWaitingForResumeOperation ? 'startIcon' : undefined } buttonText={t('resume_robot_operations')} - disabled={ - isEngaged || - isResuming || - isPlacing || - isWaitingForLogicalDisengage || - isWaitingForPlateReaderLidPlacement - } + disabled={isEngaged || isResuming || isWaitingForResumeOperation} onClick={handleClick} /> @@ -179,29 +152,23 @@ function TouchscreenModal({ function DesktopModal({ isEngaged, closeModal, - setIsDismissedModal, - isWaitingForLogicalDisengage, - isWaitingForPlateReaderLidPlacement, - setShouldSeeLogicalDisengage, + isWaitingForResumeOperation, + setIsWaitingForResumeOperation, }: EstopPressedModalProps): JSX.Element { const { t } = useTranslation('device_settings') const [isResuming, setIsResuming] = React.useState(false) const { acknowledgeEstopDisengage } = useAcknowledgeEstopDisengageMutation() - const { placeReaderLid, isPlacing } = usePlacePlateReaderLid({ - pipetteInfo: null, + const { + handlePlaceReaderLid, + isValidPlateReaderMove, + } = usePlacePlateReaderLid({ + onSettled: closeModal, }) - const handleCloseModal = (): void => { - if (setIsDismissedModal != null) { - setIsDismissedModal(true) - } - closeModal() - } - const modalProps: ModalProps = { type: 'error', title: t('estop_pressed'), - onClose: handleCloseModal, + onClose: closeModal, closeOnOutsideClick: false, childrenPadding: SPACING.spacing24, width: '47rem', @@ -210,20 +177,12 @@ function DesktopModal({ const handleClick: React.MouseEventHandler = (e): void => { e.preventDefault() setIsResuming(true) - acknowledgeEstopDisengage( - {}, - { - onSuccess: () => { - setShouldSeeLogicalDisengage() - placeReaderLid() - closeModal() - }, - onError: (error: any) => { - setIsResuming(false) - console.error(error) - }, - } - ) + setIsWaitingForResumeOperation() + acknowledgeEstopDisengage(null) + handlePlaceReaderLid() + if (!isValidPlateReaderMove) { + closeModal() + } } return ( @@ -238,23 +197,14 @@ function DesktopModal({ - {isResuming || - isPlacing || - isWaitingForLogicalDisengage || - isWaitingForPlateReaderLidPlacement ? ( + {isResuming || isWaitingForResumeOperation ? ( ) : null} {t('resume_robot_operations')} diff --git a/app/src/organisms/EmergencyStop/EstopTakeover.tsx b/app/src/organisms/EmergencyStop/EstopTakeover.tsx index 716511df7c0..dbc0d38b024 100644 --- a/app/src/organisms/EmergencyStop/EstopTakeover.tsx +++ b/app/src/organisms/EmergencyStop/EstopTakeover.tsx @@ -4,15 +4,10 @@ import { useEstopQuery } from '@opentrons/react-api-client' import { EstopPressedModal } from './EstopPressedModal' import { EstopMissingModal } from './EstopMissingModal' -import { useEstopContext } from './hooks' import { useIsUnboxingFlowOngoing } from '/app/redux-resources/config' import { getLocalRobot } from '/app/redux/discovery' -import { - PHYSICALLY_ENGAGED, - LOGICALLY_ENGAGED, - NOT_PRESENT, - DISENGAGED, -} from './constants' +import { PHYSICALLY_ENGAGED, NOT_PRESENT, DISENGAGED } from './constants' +import { EstopState } from '@opentrons/api-client' const ESTOP_CURRENTLY_DISENGAGED_REFETCH_INTERVAL_MS = 10000 const ESTOP_CURRENTLY_ENGAGED_REFETCH_INTERVAL_MS = 1000 @@ -22,78 +17,60 @@ interface EstopTakeoverProps { } export function EstopTakeover({ robotName }: EstopTakeoverProps): JSX.Element { - const [estopEngaged, setEstopEngaged] = useState(false) + const [isDismissedModal, setIsDismissedModal] = useState(false) const [ - isWaitingForLogicalDisengage, - setIsWaitingForLogicalDisengage, + isWaitingForResumeOperation, + setIsWatingForResumeOperation, ] = useState(false) - const [ - isWaitingForPlateReaderLidPlacement, - setisWaitingForPlateReaderLidPlacement, - ] = useState(false) - const { data: estopStatus } = useEstopQuery({ - refetchInterval: estopEngaged + + const [estopState, setEstopState] = useState(DISENGAGED) + const [showEmergencyStopModal, setShowEmergencyStopModal] = useState( + false + ) + + // TODO: (ba, 2024-10-24): Use notifications instead of polling + useEstopQuery({ + refetchInterval: showEmergencyStopModal ? ESTOP_CURRENTLY_ENGAGED_REFETCH_INTERVAL_MS : ESTOP_CURRENTLY_DISENGAGED_REFETCH_INTERVAL_MS, onSuccess: response => { - setEstopEngaged( - [PHYSICALLY_ENGAGED || LOGICALLY_ENGAGED].includes( - response?.data.status - ) + setEstopState(response?.data.status) + setShowEmergencyStopModal( + response.data.status !== DISENGAGED || isWaitingForResumeOperation ) - setIsWaitingForLogicalDisengage(false) }, }) - const { - isEmergencyStopModalDismissed, - setIsEmergencyStopModalDismissed, - } = useEstopContext() const isUnboxingFlowOngoing = useIsUnboxingFlowOngoing() const closeModal = (): void => { - if (estopStatus?.data.status === DISENGAGED) { - setIsEmergencyStopModalDismissed(false) - } + setIsWatingForResumeOperation(false) } const localRobot = useSelector(getLocalRobot) const localRobotName = localRobot?.name ?? 'no name' const TargetEstopModal = (): JSX.Element | null => { - switch (estopStatus?.data.status) { - case PHYSICALLY_ENGAGED: - case LOGICALLY_ENGAGED: - return ( - { - setIsWaitingForLogicalDisengage(true) - }} - /> - ) - case NOT_PRESENT: - return ( - - ) - default: - return null - } + return estopState === NOT_PRESENT ? ( + + ) : estopState !== DISENGAGED || isWaitingForResumeOperation ? ( + { + setIsWatingForResumeOperation(true) + }} + /> + ) : null } return ( <> - {estopStatus?.data.status !== DISENGAGED && !isUnboxingFlowOngoing ? ( + {showEmergencyStopModal && !isUnboxingFlowOngoing ? ( ) : null} diff --git a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts index 721f4fe4a97..532ce54b612 100644 --- a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts +++ b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts @@ -10,7 +10,8 @@ import { useRobotControlCommands } from '/app/resources/maintenance_runs' interface UsePlacePlateReaderLidResult { handlePlaceReaderLid: () => Promise - isPlacing: boolean + isExecuting: boolean + isValidPlateReaderMove: boolean } type UsePlacePlateReaderLidProps = Pick< @@ -55,7 +56,11 @@ export function usePlacePlateReaderLid( } } - return { handlePlaceReaderLid, isPlacing: isExecuting } + return { + handlePlaceReaderLid, + isExecuting: isExecuting, + isValidPlateReaderMove, + } } const buildLoadModuleCommand = (location: ModuleLocation): CreateCommand => { From 3dee541e2de97e098ebb57938466b6cf66de1de0 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Thu, 24 Oct 2024 13:02:34 -0400 Subject: [PATCH 16/20] clean up --- .../commands/unsafe/unsafe_place_labware.py | 48 +++++++++++-------- .../modules/hooks/usePlacePlateReaderLid.ts | 4 +- .../robot_server/runs/router/base_router.py | 15 +++--- robot-server/robot_server/runs/run_models.py | 4 +- .../tests/runs/router/test_base_router.py | 33 ++++++++----- shared-data/command/schemas/10.json | 4 -- shared-data/command/types/setup.ts | 7 +++ shared-data/command/types/unsafe.ts | 4 +- 8 files changed, 71 insertions(+), 48 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py index d78b3398357..4b2044ff8e9 100644 --- a/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +++ b/api/src/opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py @@ -13,13 +13,13 @@ ) from opentrons.types import Point -from ...types import DeckSlotLocation, LabwareLocation, ModuleModel +from ...types import DeckSlotLocation, ModuleModel, OnDeckLabwareLocation from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ...errors.error_occurrence import ErrorOccurrence from ...resources import ensure_ot3_hardware from ...state.update_types import StateUpdate -from opentrons.hardware_control import HardwareControlAPI +from opentrons.hardware_control import HardwareControlAPI, OT3HardwareControlAPI if TYPE_CHECKING: from ...state.state import StateView @@ -33,7 +33,9 @@ class UnsafePlaceLabwareParams(BaseModel): """Payload required for an UnsafePlaceLabware command.""" labwareId: str = Field(..., description="The id of the labware to place.") - location: LabwareLocation = Field(..., description="Where to place the labware.") + location: OnDeckLabwareLocation = Field( + ..., description="Where to place the labware." + ) class UnsafePlaceLabwareResult(BaseModel): @@ -74,15 +76,11 @@ async def execute( # Allow propagation of LabwareNotLoadedError. labware_id = params.labwareId - current_labware = self._state_view.labware.get(labware_id=labware_id) - definition_uri = current_labware.definitionUri - + definition_uri = self._state_view.labware.get(labware_id).definitionUri final_offsets = self._state_view.labware.get_labware_gripper_offsets( labware_id, None ) - drop_offset = ( - cast(Point, final_offsets.dropOffset) if final_offsets else Point() - ) + drop_offset = cast(Point, final_offsets.dropOffset) if final_offsets else None if isinstance(params.location, DeckSlotLocation): self._state_view.addressable_areas.raise_if_area_not_in_deck_configuration( @@ -109,6 +107,27 @@ async def execute( # NOTE: When the estop is pressed, the gantry loses postion, # so the robot needs to home x, y to sync. await ot3api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G, Axis.X, Axis.Y]) + state_update = StateUpdate() + + # Place the labware down + await self._start_movement(ot3api, labware_id, location, drop_offset) + + state_update.set_labware_location( + labware_id=labware_id, + new_location=location, + new_offset_id=new_offset_id, + ) + return SuccessData( + public=UnsafePlaceLabwareResult(), private=None, state_update=state_update + ) + + async def _start_movement( + self, + ot3api: OT3HardwareControlAPI, + labware_id: str, + location: OnDeckLabwareLocation, + drop_offset: Optional[Point], + ) -> None: gripper_homed_position = await ot3api.gantry_position( mount=OT3Mount.GRIPPER, refresh=True, @@ -143,17 +162,6 @@ async def execute( mount=OT3Mount.GRIPPER, abs_position=waypoint_data.position ) - state_update = StateUpdate() - - state_update.set_labware_location( - labware_id=labware_id, - new_location=location, - new_offset_id=new_offset_id, - ) - return SuccessData( - public=UnsafePlaceLabwareResult(), private=None, state_update=state_update - ) - class UnsafePlaceLabware( BaseCommand[UnsafePlaceLabwareParams, UnsafePlaceLabwareResult, ErrorOccurrence] diff --git a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts index 532ce54b612..9312a868153 100644 --- a/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts +++ b/app/src/resources/modules/hooks/usePlacePlateReaderLid.ts @@ -1,6 +1,6 @@ import { CreateCommand, - LabwareLocation, + OnDeckLabwareLocation, ModuleLocation, } from '@opentrons/shared-data' import { UseRobotControlCommandsProps } from '/app/resources/maintenance_runs' @@ -72,7 +72,7 @@ const buildLoadModuleCommand = (location: ModuleLocation): CreateCommand => { const buildPlaceLabwareCommand = ( labwareId: string, - location: LabwareLocation + location: OnDeckLabwareLocation ): CreateCommand => { return { commandType: 'unsafe/placeLabware' as const, diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index df8264a0190..5d86aa83ba1 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -18,7 +18,7 @@ from opentrons.hardware_control.types import EstopState from opentrons.protocol_engine.commands.absorbance_reader import CloseLid, OpenLid from opentrons.protocol_engine.commands.move_labware import MoveLabware -from opentrons.protocol_engine.types import CSVRuntimeParamPaths +from opentrons.protocol_engine.types import CSVRuntimeParamPaths, DeckSlotLocation from opentrons.protocol_engine import ( errors as pe_errors, ) @@ -633,11 +633,13 @@ async def get_current_state( # Labware state when estop is engaged if isinstance(command, MoveLabware): - place_labware = PlaceLabwareState( - location=command.params.newLocation, - labwareId=command.params.labwareId, - shouldPlaceDown=False, - ) + location = command.params.newLocation + if isinstance(location, DeckSlotLocation): + place_labware = PlaceLabwareState( + location=location, + labwareId=command.params.labwareId, + shouldPlaceDown=False, + ) # Handle absorbance reader lid elif isinstance(command, (OpenLid, CloseLid)): for mod in run.modules: @@ -647,7 +649,6 @@ async def get_current_state( ): continue for hw_mod in hardware.attached_modules: - lid_status = hw_mod.live_data["data"].get("lidStatus") if ( mod.location is not None and hw_mod.serial_number == mod.serialNumber diff --git a/robot-server/robot_server/runs/run_models.py b/robot-server/robot_server/runs/run_models.py index c5cffbc17e2..34c43692aaa 100644 --- a/robot-server/robot_server/runs/run_models.py +++ b/robot-server/robot_server/runs/run_models.py @@ -21,7 +21,7 @@ CommandNote, ) from opentrons.protocol_engine.types import ( - LabwareLocation, + OnDeckLabwareLocation, RunTimeParameter, PrimitiveRunTimeParamValuesType, CSVRunTimeParamFilesType, @@ -320,7 +320,7 @@ class PlaceLabwareState(BaseModel): """Details the labware being placed by the gripper.""" labwareId: str = Field(..., description="The ID of the labware to place.") - location: LabwareLocation = Field( + location: OnDeckLabwareLocation = Field( ..., description="The location the Plate Reade lid should be in." ) shouldPlaceDown: bool = Field( diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 11250c76a91..e9ec31d9b18 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -58,6 +58,7 @@ from robot_server.runs.run_models import RunNotFoundError from robot_server.runs.router.base_router import ( AllRunsLinks, + PlaceLabwareState, create_run, get_run_data_from_url, get_run, @@ -873,22 +874,28 @@ async def test_get_current_state_success( mock_hardware_api: HardwareControlAPI, mock_nozzle_maps: Dict[str, NozzleMap], ) -> None: - """It should return the active nozzle layout for a specific pipette.""" + """It should return different state from the current run. + + - the active nozzle layout for a specific pipette. + - place plate reader state for absorbance reader. + """ run_id = "test-run-id" decoy.when(mock_run_data_manager.get_nozzle_maps(run_id=run_id)).then_return( mock_nozzle_maps ) - decoy.when( - mock_run_data_manager.get_last_completed_command(run_id=run_id) - ).then_return( - CommandPointer( - command_id="last-command-id", - command_key="last-command-key", + command_pointer = CommandPointer( + command_id="command-id", + command_key="command-key", created_at=datetime(year=2024, month=4, day=4), index=101, ) - ) + decoy.when( + mock_run_data_manager.get_last_completed_command(run_id=run_id) + ).then_return(command_pointer) + decoy.when( + mock_run_data_manager.get_current_command(run_id=run_id) + ).then_return(command_pointer) result = await get_current_state( runId=run_id, @@ -899,18 +906,19 @@ async def test_get_current_state_success( assert result.status_code == 200 assert result.content.data == RunCurrentState.construct( + estopEngaged=False, activeNozzleLayouts={ "mock-pipette-id": ActiveNozzleLayout( startingNozzle="A1", activeNozzles=["A1"], config=NozzleLayoutConfig.FULL, ) - } + }, ) assert result.content.links == CurrentStateLinks( lastCompleted=CommandLinkNoMeta( - href="/runs/test-run-id/commands/last-command-id", - id="last-command-id", + href="/runs/test-run-id/commands/command-id", + id="command-id", ) ) @@ -918,6 +926,7 @@ async def test_get_current_state_success( async def test_get_current_state_run_not_current( decoy: Decoy, mock_run_data_manager: RunDataManager, + mock_hardware_api: HardwareControlAPI, ) -> None: """It should raise RunStopped when the run is not current.""" run_id = "non-current-run-id" @@ -930,6 +939,8 @@ async def test_get_current_state_run_not_current( await get_current_state( runId=run_id, run_data_manager=mock_run_data_manager, + hardware=mock_hardware_api, + robot_type=RobotTypeEnum.FLEX, ) assert exc_info.value.status_code == 409 diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index f1330b074d3..93bc2387d63 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -4767,10 +4767,6 @@ { "$ref": "#/definitions/OnLabwareLocation" }, - { - "enum": ["offDeck"], - "type": "string" - }, { "$ref": "#/definitions/AddressableAreaLocation" } diff --git a/shared-data/command/types/setup.ts b/shared-data/command/types/setup.ts index 0be40e6de13..493d55c349c 100644 --- a/shared-data/command/types/setup.ts +++ b/shared-data/command/types/setup.ts @@ -106,6 +106,13 @@ export type LabwareLocation = | { labwareId: string } | { addressableAreaName: AddressableAreaName } +export type OnDeckLabwareLocation = + | { slotName: string } + | { moduleId: string } + | { labwareId: string } + | { addressableAreaName: AddressableAreaName } + + export type NonStackedLocation = | 'offDeck' | { slotName: string } diff --git a/shared-data/command/types/unsafe.ts b/shared-data/command/types/unsafe.ts index 3a9b91c2d31..3875aaa3036 100644 --- a/shared-data/command/types/unsafe.ts +++ b/shared-data/command/types/unsafe.ts @@ -1,7 +1,7 @@ import type { CommonCommandRunTimeInfo, CommonCommandCreateInfo, - LabwareLocation, + OnDeckLabwareLocation, } from '.' import type { MotorAxes } from '../../js/types' @@ -93,7 +93,7 @@ export interface UnsafeUngripLabwareRunTimeCommand } export interface UnsafePlaceLabwareParams { labwareId: string - location: LabwareLocation + location: OnDeckLabwareLocation } export interface UnsafePlaceLabwareCreateCommand extends CommonCommandCreateInfo { From ddc24cb84aae7c3179254024ac368fe12ee922a8 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Thu, 24 Oct 2024 13:14:52 -0400 Subject: [PATCH 17/20] default the filename arg of absorbance read command to None --- .../protocol_api/core/engine/module_core.py | 2 +- .../protocol_api/core/engine/protocol.py | 35 +++++++++++-------- api/src/opentrons/protocol_api/core/module.py | 2 +- .../opentrons/protocol_api/module_contexts.py | 4 ++- .../robot_server/runs/router/base_router.py | 4 ++- .../tests/runs/router/test_base_router.py | 17 +++++---- 6 files changed, 36 insertions(+), 28 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/module_core.py b/api/src/opentrons/protocol_api/core/engine/module_core.py index 1d800dee7ea..9f2785b7432 100644 --- a/api/src/opentrons/protocol_api/core/engine/module_core.py +++ b/api/src/opentrons/protocol_api/core/engine/module_core.py @@ -586,7 +586,7 @@ def initialize( ) self._initialized_value = wavelengths - def read(self, filename: Optional[str]) -> Dict[int, Dict[str, float]]: + def read(self, filename: Optional[str] = None) -> Dict[int, Dict[str, float]]: """Initiate a read on the Absorbance Reader, and return the results. During Analysis, this will return a measurement of zero for all wells.""" wavelengths = self._engine_client.state.modules.get_absorbance_reader_substate( self.module_id diff --git a/api/src/opentrons/protocol_api/core/engine/protocol.py b/api/src/opentrons/protocol_api/core/engine/protocol.py index 0ed5270320a..dac8bc44a5b 100644 --- a/api/src/opentrons/protocol_api/core/engine/protocol.py +++ b/api/src/opentrons/protocol_api/core/engine/protocol.py @@ -449,9 +449,10 @@ def load_module( # When the protocol engine is created, we add Module Lids as part of the deck fixed labware # If a valid module exists in the deck config. For analysis, we add the labware here since - # deck fixed labware is not created under the same conditions. - if self._engine_client.state.config.use_virtual_modules: - self._load_virtual_module_lid(module_core) + # deck fixed labware is not created under the same conditions. We also need to inject the Module + # lids when the module isnt already on the deck config, like when adding a new + # module during a protocol setup. + self._load_virtual_module_lid(module_core) self._module_cores_by_id[module_core.module_id] = module_core @@ -461,20 +462,24 @@ def _load_virtual_module_lid( self, module_core: Union[ModuleCore, NonConnectedModuleCore] ) -> None: if isinstance(module_core, AbsorbanceReaderCore): - lid = self._engine_client.execute_command_without_recovery( - cmd.LoadLabwareParams( - loadName="opentrons_flex_lid_absorbance_plate_reader_module", - location=ModuleLocation(moduleId=module_core.module_id), - namespace="opentrons", - version=1, - displayName="Absorbance Reader Lid", - ) + substate = self._engine_client.state.modules.get_absorbance_reader_substate( + module_core.module_id ) + if substate.lid_id is None: + lid = self._engine_client.execute_command_without_recovery( + cmd.LoadLabwareParams( + loadName="opentrons_flex_lid_absorbance_plate_reader_module", + location=ModuleLocation(moduleId=module_core.module_id), + namespace="opentrons", + version=1, + displayName="Absorbance Reader Lid", + ) + ) - self._engine_client.add_absorbance_reader_lid( - module_id=module_core.module_id, - lid_id=lid.labwareId, - ) + self._engine_client.add_absorbance_reader_lid( + module_id=module_core.module_id, + lid_id=lid.labwareId, + ) def _create_non_connected_module_core( self, load_module_result: LoadModuleResult diff --git a/api/src/opentrons/protocol_api/core/module.py b/api/src/opentrons/protocol_api/core/module.py index c93e8ce8de8..e24fbbc54b0 100644 --- a/api/src/opentrons/protocol_api/core/module.py +++ b/api/src/opentrons/protocol_api/core/module.py @@ -365,7 +365,7 @@ def initialize( """Initialize the Absorbance Reader by taking zero reading.""" @abstractmethod - def read(self, filename: Optional[str]) -> Dict[int, Dict[str, float]]: + def read(self, filename: Optional[str] = None) -> Dict[int, Dict[str, float]]: """Get an absorbance reading from the Absorbance Reader.""" @abstractmethod diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index f7541da1836..9ae550f8d3f 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -1035,7 +1035,9 @@ def initialize( ) @requires_version(2, 21) - def read(self, export_filename: Optional[str]) -> Dict[int, Dict[str, float]]: + def read( + self, export_filename: Optional[str] = None + ) -> Dict[int, Dict[str, float]]: """Initiate read on the Absorbance Reader. Returns a dictionary of wavelengths to dictionary of values ordered by well name. diff --git a/robot-server/robot_server/runs/router/base_router.py b/robot-server/robot_server/runs/router/base_router.py index 23f1d6eb446..b7df09f8992 100644 --- a/robot-server/robot_server/runs/router/base_router.py +++ b/robot-server/robot_server/runs/router/base_router.py @@ -575,7 +575,7 @@ async def get_run_commands_error( status.HTTP_409_CONFLICT: {"model": ErrorBody[RunStopped]}, }, ) -async def get_current_state( +async def get_current_state( # noqa: C901 runId: str, run_data_manager: Annotated[RunDataManager, Depends(get_run_data_manager)], hardware: Annotated[HardwareControlAPI, Depends(get_hardware)], @@ -586,6 +586,8 @@ async def get_current_state( Arguments: runId: Run ID pulled from URL. run_data_manager: Run data retrieval interface. + hardware: Hardware control interface. + robot_type: The type of robot. """ try: run = run_data_manager.get(run_id=runId) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index e9ec31d9b18..0a0266c0c65 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -58,7 +58,6 @@ from robot_server.runs.run_models import RunNotFoundError from robot_server.runs.router.base_router import ( AllRunsLinks, - PlaceLabwareState, create_run, get_run_data_from_url, get_run, @@ -885,17 +884,17 @@ async def test_get_current_state_success( mock_nozzle_maps ) command_pointer = CommandPointer( - command_id="command-id", - command_key="command-key", - created_at=datetime(year=2024, month=4, day=4), - index=101, - ) + command_id="command-id", + command_key="command-key", + created_at=datetime(year=2024, month=4, day=4), + index=101, + ) decoy.when( mock_run_data_manager.get_last_completed_command(run_id=run_id) ).then_return(command_pointer) - decoy.when( - mock_run_data_manager.get_current_command(run_id=run_id) - ).then_return(command_pointer) + decoy.when(mock_run_data_manager.get_current_command(run_id=run_id)).then_return( + command_pointer + ) result = await get_current_state( runId=run_id, From 884b27728b19f44a063a1a8f24edc25904f1ce3a Mon Sep 17 00:00:00 2001 From: vegano1 Date: Thu, 24 Oct 2024 17:58:12 -0400 Subject: [PATCH 18/20] fix(api): raise CannotPerformModuleAction if the plate reader is initialized before calling close_lid. --- .../protocol_api/core/engine/module_core.py | 7 +++++++ .../core/engine/test_absorbance_reader_core.py | 12 ++++++++++++ 2 files changed, 19 insertions(+) diff --git a/api/src/opentrons/protocol_api/core/engine/module_core.py b/api/src/opentrons/protocol_api/core/engine/module_core.py index 9f2785b7432..1e6d4e26b2f 100644 --- a/api/src/opentrons/protocol_api/core/engine/module_core.py +++ b/api/src/opentrons/protocol_api/core/engine/module_core.py @@ -567,6 +567,7 @@ class AbsorbanceReaderCore(ModuleCore, AbstractAbsorbanceReaderCore): _sync_module_hardware: SynchronousAdapter[hw_modules.AbsorbanceReader] _initialized_value: Optional[List[int]] = None + _ready_to_initialize: bool = False def initialize( self, @@ -575,6 +576,11 @@ def initialize( reference_wavelength: Optional[int] = None, ) -> None: """Initialize the Absorbance Reader by taking zero reading.""" + if not self._ready_to_initialize: + raise CannotPerformModuleAction( + "Cannot perform Initialize action on Absorbance Reader without calling `.close_lid()` first." + ) + # TODO: check that the wavelengths are within the supported wavelengths self._engine_client.execute_command( cmd.absorbance_reader.InitializeParams( @@ -633,6 +639,7 @@ def close_lid( moduleId=self.module_id, ) ) + self._ready_to_initialize = True def open_lid(self) -> None: """Close the Absorbance Reader's lid.""" diff --git a/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py b/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py index a5fadde09cc..a9879e4ee94 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py @@ -11,6 +11,7 @@ from opentrons.protocol_engine.clients import SyncClient as EngineClient from opentrons.protocol_api.core.engine.module_core import AbsorbanceReaderCore from opentrons.protocol_api import MAX_SUPPORTED_VERSION +from opentrons.protocol_engine.errors.exceptions import CannotPerformModuleAction from opentrons.protocol_engine.state.module_substates import AbsorbanceReaderSubState from opentrons.protocol_engine.state.module_substates.absorbance_reader_substate import ( AbsorbanceReaderId, @@ -67,6 +68,7 @@ def test_initialize( decoy: Decoy, mock_engine_client: EngineClient, subject: AbsorbanceReaderCore ) -> None: """It should set the sample wavelength with the engine client.""" + subject._ready_to_initialize = True subject.initialize("single", [123]) decoy.verify( @@ -115,10 +117,20 @@ def test_initialize( assert subject._initialized_value == [124, 125, 126] +def test_initialize_not_ready( + subject: AbsorbanceReaderCore +) -> None: + """It should raise CannotPerformModuleAction if you dont call .close_lid() command.""" + subject._ready_to_initialize = False + with pytest.raises(CannotPerformModuleAction): + subject.initialize("single", [123]) + + def test_read( decoy: Decoy, mock_engine_client: EngineClient, subject: AbsorbanceReaderCore ) -> None: """It should call absorbance reader to read with the engine client.""" + subject._ready_to_initialize = True subject._initialized_value = [123] substate = AbsorbanceReaderSubState( module_id=AbsorbanceReaderId(subject.module_id), From ca2302aa3b749d7786ce2c842d7285864cd55194 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Sun, 27 Oct 2024 00:52:41 -0400 Subject: [PATCH 19/20] feat(api, robot-server): add source column to the data_files table so we can denote uploaded and generated csv data files. --- .../resources/file_provider.py | 2 +- .../engine/test_absorbance_reader_core.py | 4 +-- .../data_files/data_files_store.py | 23 ++++++++++--- .../data_files/file_auto_deleter.py | 10 +++--- .../robot_server/data_files/models.py | 12 +++++-- .../robot_server/data_files/router.py | 7 ++-- .../file_provider/fastapi_dependencies.py | 2 +- .../robot_server/file_provider/provider.py | 13 +++++-- .../persistence/_migrations/v6_to_v7.py | 34 +++++++++++++++++-- .../persistence/tables/__init__.py | 2 ++ .../persistence/tables/schema_7.py | 20 ++++++++++- .../robot_server/protocols/protocol_store.py | 1 + .../runs/run_orchestrator_store.py | 1 + robot-server/robot_server/settings.py | 2 +- 14 files changed, 106 insertions(+), 27 deletions(-) diff --git a/api/src/opentrons/protocol_engine/resources/file_provider.py b/api/src/opentrons/protocol_engine/resources/file_provider.py index d4ed7b71522..e1299605e76 100644 --- a/api/src/opentrons/protocol_engine/resources/file_provider.py +++ b/api/src/opentrons/protocol_engine/resources/file_provider.py @@ -5,7 +5,7 @@ from ..errors import StorageLimitReachedError -MAXIMUM_CSV_FILE_LIMIT = 40 +MAXIMUM_CSV_FILE_LIMIT = 400 class GenericCsvTransform: diff --git a/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py b/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py index a9879e4ee94..fd537d4cad9 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_absorbance_reader_core.py @@ -117,9 +117,7 @@ def test_initialize( assert subject._initialized_value == [124, 125, 126] -def test_initialize_not_ready( - subject: AbsorbanceReaderCore -) -> None: +def test_initialize_not_ready(subject: AbsorbanceReaderCore) -> None: """It should raise CannotPerformModuleAction if you dont call .close_lid() command.""" subject._ready_to_initialize = False with pytest.raises(CannotPerformModuleAction): diff --git a/robot-server/robot_server/data_files/data_files_store.py b/robot-server/robot_server/data_files/data_files_store.py index a209dfc8e3a..89351aa6aed 100644 --- a/robot-server/robot_server/data_files/data_files_store.py +++ b/robot-server/robot_server/data_files/data_files_store.py @@ -3,6 +3,7 @@ from dataclasses import dataclass from datetime import datetime +from enum import Enum from pathlib import Path from typing import Optional, List, Set @@ -19,6 +20,13 @@ from .models import FileIdNotFoundError, FileInUseError +class DataFileSource(Enum): + """The source this data file is from.""" + + UPLOADED = "uploaded" + GENERATED = "generated" + + @dataclass(frozen=True) class DataFileInfo: """Metadata info of a saved data file.""" @@ -27,6 +35,7 @@ class DataFileInfo: name: str file_hash: str created_at: datetime + source: DataFileSource class DataFilesStore: @@ -53,6 +62,7 @@ async def insert(self, file_info: DataFileInfo) -> None: file_info_dict = { "id": file_info.id, "name": file_info.name, + "source": file_info.source, "created_at": file_info.created_at, "file_hash": file_info.file_hash, } @@ -80,14 +90,18 @@ def sql_get_all_from_engine(self) -> List[DataFileInfo]: all_rows = transaction.execute(statement).all() return [_convert_row_data_file_info(sql_row) for sql_row in all_rows] - def get_usage_info(self) -> List[FileUsageInfo]: + def get_usage_info(self, source: Optional[DataFileSource] = None) -> List[FileUsageInfo]: """Return information about usage of all the existing data files in runs & analyses. Results are ordered with the oldest-added data file first. """ - select_all_data_file_ids = sqlalchemy.select(data_files_table.c.id).order_by( - sqlite_rowid - ) + if source is None: + select_all_data_file_ids = sqlalchemy.select(data_files_table.c.id).order_by(sqlite_rowid) + else: + select_all_data_file_ids = sqlalchemy.select( + data_files_table.c.id + ).where(data_files_table.c.source == source).order_by(sqlite_rowid) + select_ids_used_in_analyses = sqlalchemy.select( analysis_csv_rtp_table.c.file_id ).where(analysis_csv_rtp_table.c.file_id.is_not(None)) @@ -165,6 +179,7 @@ def _convert_row_data_file_info(row: sqlalchemy.engine.Row) -> DataFileInfo: return DataFileInfo( id=row.id, name=row.name, + source=row.source, created_at=row.created_at, file_hash=row.file_hash, ) diff --git a/robot-server/robot_server/data_files/file_auto_deleter.py b/robot-server/robot_server/data_files/file_auto_deleter.py index 46c26eb866a..f7cea10830e 100644 --- a/robot-server/robot_server/data_files/file_auto_deleter.py +++ b/robot-server/robot_server/data_files/file_auto_deleter.py @@ -1,14 +1,14 @@ -"""Auto-delete old data files to make room for new ones.""" +"""Auto-delete old user data files to make room for new ones.""" from logging import getLogger -from robot_server.data_files.data_files_store import DataFilesStore +from robot_server.data_files.data_files_store import DataFileSource, DataFilesStore from robot_server.deletion_planner import DataFileDeletionPlanner _log = getLogger(__name__) class DataFileAutoDeleter: - """Auto deleter for data files.""" + """Auto deleter for uploaded data files.""" def __init__( self, @@ -22,9 +22,7 @@ async def make_room_for_new_file(self) -> None: """Delete old data files to make room for a new one.""" # It feels wasteful to collect usage info of upto 50 files # even when there's no need for deletion - data_file_usage_info = [ - usage_info for usage_info in self._data_files_store.get_usage_info() - ] + data_file_usage_info = self._data_files_store.get_usage_info(DataFileSource.UPLOADED) if len(data_file_usage_info) < self._deletion_planner.maximum_allowed_files: return diff --git a/robot-server/robot_server/data_files/models.py b/robot-server/robot_server/data_files/models.py index f2da43bb0f6..fb532fe8b85 100644 --- a/robot-server/robot_server/data_files/models.py +++ b/robot-server/robot_server/data_files/models.py @@ -6,16 +6,22 @@ from opentrons_shared_data.errors import GeneralError +from robot_server.data_files.data_files_store import DataFileSource from robot_server.errors.error_responses import ErrorDetails from robot_server.service.json_api import ResourceModel class DataFile(ResourceModel): - """A model representing an uploaded data file.""" + """A model representing a data file.""" id: str = Field(..., description="A unique identifier for this file.") - name: str = Field(..., description="Name of the uploaded file.") - createdAt: datetime = Field(..., description="When this data file was *uploaded*.") + name: str = Field(..., description="Name of the data file.") + source: DataFileSource = Field( + ..., description="The origin of the file (uploaded or generated)" + ) + createdAt: datetime = Field( + ..., description="When this data file was uploaded or generated.." + ) class FileIdNotFoundError(GeneralError): diff --git a/robot-server/robot_server/data_files/router.py b/robot-server/robot_server/data_files/router.py index 39f191da553..51694c4eb4c 100644 --- a/robot-server/robot_server/data_files/router.py +++ b/robot-server/robot_server/data_files/router.py @@ -20,7 +20,7 @@ get_data_files_store, get_data_file_auto_deleter, ) -from .data_files_store import DataFilesStore, DataFileInfo +from .data_files_store import DataFileSource, DataFilesStore, DataFileInfo from .file_auto_deleter import DataFileAutoDeleter from .models import DataFile, FileIdNotFoundError, FileIdNotFound, FileInUseError from ..protocols.dependencies import get_file_hasher, get_file_reader_writer @@ -151,6 +151,7 @@ async def upload_data_file( name=buffered_file.name, file_hash=file_hash, created_at=created_at, + source=DataFileSource.UPLOADED, ) await data_files_store.insert(file_info) return await PydanticResponse.create( @@ -195,6 +196,7 @@ async def get_data_file_info_by_id( id=resource.id, name=resource.name, createdAt=resource.created_at, + source=resource.source, ) ), status_code=status.HTTP_200_OK, @@ -260,6 +262,7 @@ async def get_all_data_files( id=data_file_info.id, name=data_file_info.name, createdAt=data_file_info.created_at, + source=data_file_info.source, ) for data_file_info in data_files ], @@ -282,7 +285,7 @@ async def delete_file_by_id( dataFileId: str, data_files_store: DataFilesStore = Depends(get_data_files_store), ) -> PydanticResponse[SimpleEmptyBody]: - """Delete an uploaded data file by ID. + """Delete an uploaded or generated data file by ID. Arguments: dataFileId: ID of the data file to delete, pulled from URL. diff --git a/robot-server/robot_server/file_provider/fastapi_dependencies.py b/robot-server/robot_server/file_provider/fastapi_dependencies.py index 65042288f9a..7847e59b236 100644 --- a/robot-server/robot_server/file_provider/fastapi_dependencies.py +++ b/robot-server/robot_server/file_provider/fastapi_dependencies.py @@ -30,7 +30,7 @@ async def get_file_provider( FileProviderWrapper, fastapi.Depends(get_file_provider_wrapper) ], ) -> FileProvider: - """Return theengine `FileProvider` which accepts callbacks from FileProviderWrapper.""" + """Return the engine `FileProvider` which accepts callbacks from FileProviderWrapper.""" file_provider = FileProvider( data_files_write_csv_callback=file_provider_wrapper.write_csv_callback, data_files_filecount=file_provider_wrapper.csv_filecount_callback, diff --git a/robot-server/robot_server/file_provider/provider.py b/robot-server/robot_server/file_provider/provider.py index 5cfeb640fef..bedc7145e50 100644 --- a/robot-server/robot_server/file_provider/provider.py +++ b/robot-server/robot_server/file_provider/provider.py @@ -10,7 +10,11 @@ get_data_files_store, ) from ..service.dependencies import get_current_time, get_unique_id -from robot_server.data_files.data_files_store import DataFilesStore, DataFileInfo +from robot_server.data_files.data_files_store import ( + DataFileSource, + DataFilesStore, + DataFileInfo, +) from opentrons.protocol_engine.resources.file_provider import GenericCsvTransform @@ -62,13 +66,16 @@ async def write_csv_callback( name=csv_data.filename, file_hash="", created_at=created_at, + source=DataFileSource.GENERATED, ) await self._data_files_store.insert(file_info) return file_id async def csv_filecount_callback(self) -> int: - """Return the current count of files stored within the data files directory.""" + """Return the current count of generated files stored within the data files directory.""" data_file_usage_info = [ - usage_info for usage_info in self._data_files_store.get_usage_info() + usage_info + for usage_info in self._data_files_store.get_usage_info() + if usage_info.source == DataFileSource.GENERATED ] return len(data_file_usage_info) diff --git a/robot-server/robot_server/persistence/_migrations/v6_to_v7.py b/robot-server/robot_server/persistence/_migrations/v6_to_v7.py index faae646e5b7..ce528a12ab7 100644 --- a/robot-server/robot_server/persistence/_migrations/v6_to_v7.py +++ b/robot-server/robot_server/persistence/_migrations/v6_to_v7.py @@ -3,6 +3,7 @@ Summary of changes from schema 6: - Adds a new command_intent to store the commands intent in the commands table +- Adds a new source to store the data files origin in the data_files table - Adds the `boolean_setting` table. """ @@ -15,7 +16,7 @@ import sqlalchemy from ..database import sql_engine_ctx, sqlite_rowid -from ..tables import schema_7 +from ..tables import DataFileSourceSQLEnum, schema_7 from .._folder_migrator import Migration from ..file_and_directory_names import ( @@ -35,7 +36,7 @@ def migrate(self, source_dir: Path, dest_dir: Path) -> None: dest_db_file = dest_dir / DB_FILE - # Append the new column to existing protocols in v4 database + # Append the new column to existing protocols and data_files in v6 database with ExitStack() as exit_stack: dest_engine = exit_stack.enter_context(sql_engine_ctx(dest_db_file)) @@ -59,10 +60,20 @@ def add_column( schema_7.run_command_table.c.command_intent, ) + add_column( + dest_engine, + schema_7.data_files_table.name, + schema_7.data_files_table.c.source, + ) + _migrate_command_table_with_new_command_intent_col( dest_transaction=dest_transaction ) + _migrate_data_files_table_with_new_source_col( + dest_transaction=dest_transaction + ) + def _migrate_command_table_with_new_command_intent_col( dest_transaction: sqlalchemy.engine.Connection, @@ -83,3 +94,22 @@ def _migrate_command_table_with_new_command_intent_col( dest_transaction.execute( f"UPDATE run_command SET command_intent='{new_command_intent}' WHERE row_id={row.row_id}" ) + + +def _migrate_data_files_table_with_new_source_col( + dest_transaction: sqlalchemy.engine.Connection, +) -> None: + """Add a new 'source' column to data_files table.""" + select_data_files = sqlalchemy.select(schema_7.data_files_table).order_by( + sqlite_rowid + ) + insert_new_data = sqlalchemy.insert(schema_7.data_files_table) + for old_row in dest_transaction.execute(select_data_files).all(): + dest_transaction.execute( + insert_new_data, + id=old_row.id, + name=old_row.name, + file_hash=old_row.file_hash, + created_at=old_row.created_at, + source=DataFileSourceSQLEnum.UPLOADED, + ) diff --git a/robot-server/robot_server/persistence/tables/__init__.py b/robot-server/robot_server/persistence/tables/__init__.py index 006f5356d76..fa0129a4ee6 100644 --- a/robot-server/robot_server/persistence/tables/__init__.py +++ b/robot-server/robot_server/persistence/tables/__init__.py @@ -16,6 +16,7 @@ PrimitiveParamSQLEnum, ProtocolKindSQLEnum, BooleanSettingKey, + DataFileSourceSQLEnum, ) @@ -34,4 +35,5 @@ "PrimitiveParamSQLEnum", "ProtocolKindSQLEnum", "BooleanSettingKey", + "DataFileSourceSQLEnum", ] diff --git a/robot-server/robot_server/persistence/tables/schema_7.py b/robot-server/robot_server/persistence/tables/schema_7.py index cf1e2d8d717..1690298007f 100644 --- a/robot-server/robot_server/persistence/tables/schema_7.py +++ b/robot-server/robot_server/persistence/tables/schema_7.py @@ -1,4 +1,4 @@ -"""v6 of our SQLite schema.""" +"""v7 of our SQLite schema.""" import enum import sqlalchemy @@ -23,6 +23,13 @@ class ProtocolKindSQLEnum(enum.Enum): QUICK_TRANSFER = "quick-transfer" +class DataFileSourceSQLEnum(enum.Enum): + """The source this data file is from.""" + + UPLOADED = "uploaded" + GENERATED = "generated" + + protocol_table = sqlalchemy.Table( "protocol", metadata, @@ -238,6 +245,17 @@ class ProtocolKindSQLEnum(enum.Enum): UTCDateTime, nullable=False, ), + sqlalchemy.Column( + "source", + sqlalchemy.Enum( + DataFileSourceSQLEnum, + values_callable=lambda obj: [e.value for e in obj], + validate_strings=True, + create_constraint=True, + ), + index=True, + nullable=False, + ), ) run_csv_rtp_table = sqlalchemy.Table( diff --git a/robot-server/robot_server/protocols/protocol_store.py b/robot-server/robot_server/protocols/protocol_store.py index a3a4a954961..625199ccfe9 100644 --- a/robot-server/robot_server/protocols/protocol_store.py +++ b/robot-server/robot_server/protocols/protocol_store.py @@ -346,6 +346,7 @@ async def get_referenced_data_files(self, protocol_id: str) -> List[DataFile]: id=sql_row.id, name=sql_row.name, createdAt=sql_row.created_at, + source=sql_row.source, ) for sql_row in data_files_rows ] diff --git a/robot-server/robot_server/runs/run_orchestrator_store.py b/robot-server/robot_server/runs/run_orchestrator_store.py index efa97347ae9..8bbc4030e37 100644 --- a/robot-server/robot_server/runs/run_orchestrator_store.py +++ b/robot-server/robot_server/runs/run_orchestrator_store.py @@ -207,6 +207,7 @@ async def create( run_id: The run resource the run orchestrator is assigned to. labware_offsets: Labware offsets to create the run with. deck_configuration: A mapping of fixtures to cutout fixtures the deck will be loaded with. + file_provider: Wrapper to let the engine read/write data files. notify_publishers: Utilized by the engine to notify publishers of state changes. protocol: The protocol to load the runner with, if any. run_time_param_values: Any runtime parameter values to set. diff --git a/robot-server/robot_server/settings.py b/robot-server/robot_server/settings.py index d4508406510..40b0ed663bb 100644 --- a/robot-server/robot_server/settings.py +++ b/robot-server/robot_server/settings.py @@ -107,7 +107,7 @@ class RobotServerSettings(BaseSettings): default=50, gt=0, description=( - "The maximum number of data files to allow before auto-deleting old ones." + "The maximum number of uploaded data files to allow before auto-deleting old ones." ), ) From b7f4f243a84cca2c5a417bb2779a37ba41edc1b6 Mon Sep 17 00:00:00 2001 From: vegano1 Date: Sun, 27 Oct 2024 17:36:49 -0400 Subject: [PATCH 20/20] update tests --- .../data_files/data_files_store.py | 29 +++++++++---------- .../data_files/file_auto_deleter.py | 7 +++-- .../robot_server/data_files/models.py | 9 +++++- .../robot_server/data_files/router.py | 12 ++++++-- .../robot_server/file_provider/provider.py | 10 +++---- .../robot_server/protocols/protocol_store.py | 4 +-- .../tests/data_files/test_data_files_store.py | 14 ++++++++- .../data_files/test_file_auto_deleter.py | 5 +++- robot-server/tests/data_files/test_router.py | 25 ++++++++++++++-- .../data_files/test_deletion.tavern.yaml | 1 + .../test_upload_data_file.tavern.yaml | 3 ++ ...lyses_with_csv_file_parameters.tavern.yaml | 4 +++ ...t_csv_files_used_with_protocol.tavern.yaml | 3 ++ ...t_run_with_run_time_parameters.tavern.yaml | 1 + robot-server/tests/persistence/test_tables.py | 7 ++++- .../test_completed_analysis_store.py | 8 ++++- .../tests/protocols/test_protocol_store.py | 13 +++++++-- .../tests/protocols/test_protocols_router.py | 12 ++++++-- .../tests/runs/router/test_base_router.py | 7 ++++- robot-server/tests/runs/test_run_store.py | 8 ++++- 20 files changed, 142 insertions(+), 40 deletions(-) diff --git a/robot-server/robot_server/data_files/data_files_store.py b/robot-server/robot_server/data_files/data_files_store.py index 89351aa6aed..28257dbb8d2 100644 --- a/robot-server/robot_server/data_files/data_files_store.py +++ b/robot-server/robot_server/data_files/data_files_store.py @@ -3,7 +3,6 @@ from dataclasses import dataclass from datetime import datetime -from enum import Enum from pathlib import Path from typing import Optional, List, Set @@ -16,15 +15,9 @@ analysis_csv_rtp_table, run_csv_rtp_table, ) +from robot_server.persistence.tables.schema_7 import DataFileSourceSQLEnum -from .models import FileIdNotFoundError, FileInUseError - - -class DataFileSource(Enum): - """The source this data file is from.""" - - UPLOADED = "uploaded" - GENERATED = "generated" +from .models import DataFileSource, FileIdNotFoundError, FileInUseError @dataclass(frozen=True) @@ -62,7 +55,7 @@ async def insert(self, file_info: DataFileInfo) -> None: file_info_dict = { "id": file_info.id, "name": file_info.name, - "source": file_info.source, + "source": DataFileSourceSQLEnum(file_info.source.value), "created_at": file_info.created_at, "file_hash": file_info.file_hash, } @@ -90,17 +83,23 @@ def sql_get_all_from_engine(self) -> List[DataFileInfo]: all_rows = transaction.execute(statement).all() return [_convert_row_data_file_info(sql_row) for sql_row in all_rows] - def get_usage_info(self, source: Optional[DataFileSource] = None) -> List[FileUsageInfo]: + def get_usage_info( + self, source: Optional[DataFileSource] = None + ) -> List[FileUsageInfo]: """Return information about usage of all the existing data files in runs & analyses. Results are ordered with the oldest-added data file first. """ if source is None: - select_all_data_file_ids = sqlalchemy.select(data_files_table.c.id).order_by(sqlite_rowid) - else: select_all_data_file_ids = sqlalchemy.select( data_files_table.c.id - ).where(data_files_table.c.source == source).order_by(sqlite_rowid) + ).order_by(sqlite_rowid) + else: + select_all_data_file_ids = ( + sqlalchemy.select(data_files_table.c.id) + .where(data_files_table.c.source.name == source.name) + .order_by(sqlite_rowid) + ) select_ids_used_in_analyses = sqlalchemy.select( analysis_csv_rtp_table.c.file_id @@ -179,7 +178,7 @@ def _convert_row_data_file_info(row: sqlalchemy.engine.Row) -> DataFileInfo: return DataFileInfo( id=row.id, name=row.name, - source=row.source, + source=DataFileSource(row.source.value), created_at=row.created_at, file_hash=row.file_hash, ) diff --git a/robot-server/robot_server/data_files/file_auto_deleter.py b/robot-server/robot_server/data_files/file_auto_deleter.py index f7cea10830e..7f6c1e06493 100644 --- a/robot-server/robot_server/data_files/file_auto_deleter.py +++ b/robot-server/robot_server/data_files/file_auto_deleter.py @@ -1,7 +1,8 @@ """Auto-delete old user data files to make room for new ones.""" from logging import getLogger -from robot_server.data_files.data_files_store import DataFileSource, DataFilesStore +from robot_server.data_files.data_files_store import DataFilesStore +from robot_server.data_files.models import DataFileSource from robot_server.deletion_planner import DataFileDeletionPlanner _log = getLogger(__name__) @@ -22,7 +23,9 @@ async def make_room_for_new_file(self) -> None: """Delete old data files to make room for a new one.""" # It feels wasteful to collect usage info of upto 50 files # even when there's no need for deletion - data_file_usage_info = self._data_files_store.get_usage_info(DataFileSource.UPLOADED) + data_file_usage_info = self._data_files_store.get_usage_info( + DataFileSource.UPLOADED + ) if len(data_file_usage_info) < self._deletion_planner.maximum_allowed_files: return diff --git a/robot-server/robot_server/data_files/models.py b/robot-server/robot_server/data_files/models.py index fb532fe8b85..5c279df6cc9 100644 --- a/robot-server/robot_server/data_files/models.py +++ b/robot-server/robot_server/data_files/models.py @@ -1,16 +1,23 @@ """Data files models.""" from datetime import datetime from typing import Literal, Set +from enum import Enum from pydantic import Field from opentrons_shared_data.errors import GeneralError -from robot_server.data_files.data_files_store import DataFileSource from robot_server.errors.error_responses import ErrorDetails from robot_server.service.json_api import ResourceModel +class DataFileSource(Enum): + """The source this data file is from.""" + + UPLOADED = "uploaded" + GENERATED = "generated" + + class DataFile(ResourceModel): """A model representing a data file.""" diff --git a/robot-server/robot_server/data_files/router.py b/robot-server/robot_server/data_files/router.py index 51694c4eb4c..cf4ba9fa649 100644 --- a/robot-server/robot_server/data_files/router.py +++ b/robot-server/robot_server/data_files/router.py @@ -20,9 +20,15 @@ get_data_files_store, get_data_file_auto_deleter, ) -from .data_files_store import DataFileSource, DataFilesStore, DataFileInfo +from .data_files_store import DataFilesStore, DataFileInfo from .file_auto_deleter import DataFileAutoDeleter -from .models import DataFile, FileIdNotFoundError, FileIdNotFound, FileInUseError +from .models import ( + DataFile, + DataFileSource, + FileIdNotFoundError, + FileIdNotFound, + FileInUseError, +) from ..protocols.dependencies import get_file_hasher, get_file_reader_writer from ..service.dependencies import get_current_time, get_unique_id @@ -137,6 +143,7 @@ async def upload_data_file( id=existing_file_info.id, name=existing_file_info.name, createdAt=existing_file_info.created_at, + source=existing_file_info.source, ) ), status_code=status.HTTP_200_OK, @@ -160,6 +167,7 @@ async def upload_data_file( id=file_info.id, name=file_info.name, createdAt=created_at, + source=DataFileSource.UPLOADED, ) ), status_code=status.HTTP_201_CREATED, diff --git a/robot-server/robot_server/file_provider/provider.py b/robot-server/robot_server/file_provider/provider.py index bedc7145e50..bbf05b40485 100644 --- a/robot-server/robot_server/file_provider/provider.py +++ b/robot-server/robot_server/file_provider/provider.py @@ -9,9 +9,9 @@ get_data_files_directory, get_data_files_store, ) +from robot_server.data_files.models import DataFileSource from ..service.dependencies import get_current_time, get_unique_id from robot_server.data_files.data_files_store import ( - DataFileSource, DataFilesStore, DataFileInfo, ) @@ -73,9 +73,7 @@ async def write_csv_callback( async def csv_filecount_callback(self) -> int: """Return the current count of generated files stored within the data files directory.""" - data_file_usage_info = [ - usage_info - for usage_info in self._data_files_store.get_usage_info() - if usage_info.source == DataFileSource.GENERATED - ] + data_file_usage_info = self._data_files_store.get_usage_info( + DataFileSource.GENERATED + ) return len(data_file_usage_info) diff --git a/robot-server/robot_server/protocols/protocol_store.py b/robot-server/robot_server/protocols/protocol_store.py index 625199ccfe9..ee769b47c01 100644 --- a/robot-server/robot_server/protocols/protocol_store.py +++ b/robot-server/robot_server/protocols/protocol_store.py @@ -15,7 +15,7 @@ from opentrons.protocols.parse import PythonParseMode from opentrons.protocol_reader import ProtocolReader, ProtocolSource -from robot_server.data_files.models import DataFile +from robot_server.data_files.models import DataFile, DataFileSource from robot_server.persistence.database import sqlite_rowid from robot_server.persistence.tables import ( analysis_table, @@ -346,7 +346,7 @@ async def get_referenced_data_files(self, protocol_id: str) -> List[DataFile]: id=sql_row.id, name=sql_row.name, createdAt=sql_row.created_at, - source=sql_row.source, + source=DataFileSource(sql_row.source.value), ) for sql_row in data_files_rows ] diff --git a/robot-server/tests/data_files/test_data_files_store.py b/robot-server/tests/data_files/test_data_files_store.py index caef1599961..581577d0a16 100644 --- a/robot-server/tests/data_files/test_data_files_store.py +++ b/robot-server/tests/data_files/test_data_files_store.py @@ -13,7 +13,11 @@ DataFileInfo, ) from robot_server.deletion_planner import FileUsageInfo -from robot_server.data_files.models import FileIdNotFoundError, FileInUseError +from robot_server.data_files.models import ( + DataFileSource, + FileIdNotFoundError, + FileInUseError, +) from robot_server.protocols.analysis_memcache import MemoryCache from robot_server.protocols.analysis_models import ( CompletedAnalysis, @@ -107,6 +111,7 @@ async def test_insert_data_file_info_and_fetch_by_hash( id="file-id", name="file-name", file_hash="abc123", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=6, day=20, tzinfo=timezone.utc), ) assert subject.get_file_info_by_hash("abc123") is None @@ -122,12 +127,14 @@ async def test_insert_file_info_with_existing_id( id="file-id", name="file-name", file_hash="abc123", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=6, day=20, tzinfo=timezone.utc), ) data_file_info2 = DataFileInfo( id="file-id", name="file-name2", file_hash="abc1234", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=6, day=20, tzinfo=timezone.utc), ) await subject.insert(data_file_info1) @@ -143,6 +150,7 @@ async def test_insert_data_file_info_and_get_by_id( id="file-id", name="file-name", file_hash="abc", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=7, day=15, tzinfo=timezone.utc), ) await subject.insert(data_file_info) @@ -176,12 +184,14 @@ async def test_get_usage_info( id="file-id-1", name="file-name", file_hash="abc", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=7, day=15, tzinfo=timezone.utc), ) data_file_2 = DataFileInfo( id="file-id-2", name="file-name", file_hash="xyz", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=7, day=15, tzinfo=timezone.utc), ) await subject.insert(data_file_1) @@ -212,6 +222,7 @@ async def test_remove( id="file-id", name="file-name", file_hash="abc123", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=6, day=20, tzinfo=timezone.utc), ) await subject.insert(data_file_info) @@ -241,6 +252,7 @@ async def test_remove_raises_in_file_in_use( id="file-id", name="file-name", file_hash="abc123", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=6, day=20, tzinfo=timezone.utc), ) diff --git a/robot-server/tests/data_files/test_file_auto_deleter.py b/robot-server/tests/data_files/test_file_auto_deleter.py index 422af0891cb..000b55b4307 100644 --- a/robot-server/tests/data_files/test_file_auto_deleter.py +++ b/robot-server/tests/data_files/test_file_auto_deleter.py @@ -6,6 +6,7 @@ from robot_server.data_files.data_files_store import DataFilesStore from robot_server.data_files.file_auto_deleter import DataFileAutoDeleter +from robot_server.data_files.models import DataFileSource from robot_server.deletion_planner import DataFileDeletionPlanner, FileUsageInfo @@ -23,7 +24,9 @@ async def test_make_room_for_new_file( FileUsageInfo(file_id="file-2", used_by_run_or_analysis=True), ] decoy.when(mock_deletion_planner.maximum_allowed_files).then_return(1) - decoy.when(mock_data_files_store.get_usage_info()).then_return(files_usage) + decoy.when( + mock_data_files_store.get_usage_info(DataFileSource.UPLOADED) + ).then_return(files_usage) decoy.when(mock_deletion_planner.plan_for_new_file(files_usage)).then_return( {"id-to-be-deleted-1", "id-to-be-deleted-2"} ) diff --git a/robot-server/tests/data_files/test_router.py b/robot-server/tests/data_files/test_router.py index 751682fd422..ce10a9e56eb 100644 --- a/robot-server/tests/data_files/test_router.py +++ b/robot-server/tests/data_files/test_router.py @@ -10,8 +10,16 @@ from robot_server.service.json_api import MultiBodyMeta, SimpleEmptyBody -from robot_server.data_files.data_files_store import DataFilesStore, DataFileInfo -from robot_server.data_files.models import DataFile, FileIdNotFoundError, FileInUseError +from robot_server.data_files.data_files_store import ( + DataFilesStore, + DataFileInfo, +) +from robot_server.data_files.models import ( + DataFile, + DataFileSource, + FileIdNotFoundError, + FileInUseError, +) from robot_server.data_files.router import ( upload_data_file, get_data_file_info_by_id, @@ -84,6 +92,7 @@ async def test_upload_new_data_file( id="data-file-id", name="abc.csv", createdAt=datetime(year=2024, month=6, day=18), + source=DataFileSource.UPLOADED, ) assert result.status_code == 201 decoy.verify( @@ -97,6 +106,7 @@ async def test_upload_new_data_file( name="abc.csv", file_hash="abc123", created_at=datetime(year=2024, month=6, day=18), + source=DataFileSource.UPLOADED, ) ), ) @@ -127,6 +137,7 @@ async def test_upload_existing_data_file( name="abc.csv", file_hash="abc123", created_at=datetime(year=2023, month=6, day=18), + source=DataFileSource.UPLOADED, ) ) @@ -146,6 +157,7 @@ async def test_upload_existing_data_file( id="existing-file-id", name="abc.csv", createdAt=datetime(year=2023, month=6, day=18), + source=DataFileSource.UPLOADED, ) @@ -183,6 +195,7 @@ async def test_upload_new_data_file_path( id="data-file-id", name="abc.csv", createdAt=datetime(year=2024, month=6, day=18), + source=DataFileSource.UPLOADED, ) decoy.verify( await file_reader_writer.write( @@ -194,6 +207,7 @@ async def test_upload_new_data_file_path( name="abc.csv", file_hash="abc123", created_at=datetime(year=2024, month=6, day=18), + source=DataFileSource.UPLOADED, ) ), ) @@ -270,6 +284,7 @@ async def test_get_data_file_info( name="abc.xyz", file_hash="123", created_at=datetime(year=2024, month=7, day=15), + source=DataFileSource.UPLOADED, ) ) @@ -282,6 +297,7 @@ async def test_get_data_file_info( id="qwerty", name="abc.xyz", createdAt=datetime(year=2024, month=7, day=15), + source=DataFileSource.UPLOADED, ) @@ -317,6 +333,7 @@ async def test_get_data_file( name="abc.xyz", file_hash="123", created_at=datetime(year=2024, month=7, day=15), + source=DataFileSource.UPLOADED, ) ) @@ -358,12 +375,14 @@ async def test_get_all_data_file_info( name="abc.xyz", file_hash="123", created_at=datetime(year=2024, month=7, day=15), + source=DataFileSource.UPLOADED, ), DataFileInfo( id="hfhcjdeowjfie", name="mcd.kfc", file_hash="124", created_at=datetime(year=2024, month=7, day=22), + source=DataFileSource.UPLOADED, ), ] ) @@ -376,11 +395,13 @@ async def test_get_all_data_file_info( id="qwerty", name="abc.xyz", createdAt=datetime(year=2024, month=7, day=15), + source=DataFileSource.UPLOADED, ), DataFile( id="hfhcjdeowjfie", name="mcd.kfc", createdAt=datetime(year=2024, month=7, day=22), + source=DataFileSource.UPLOADED, ), ] assert result.content.meta == MultiBodyMeta(cursor=0, totalLength=2) diff --git a/robot-server/tests/integration/http_api/data_files/test_deletion.tavern.yaml b/robot-server/tests/integration/http_api/data_files/test_deletion.tavern.yaml index 7cc4f90c4cc..301127df89e 100644 --- a/robot-server/tests/integration/http_api/data_files/test_deletion.tavern.yaml +++ b/robot-server/tests/integration/http_api/data_files/test_deletion.tavern.yaml @@ -21,6 +21,7 @@ stages: id: !anystr name: "sample_record.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: "uploaded" - name: Delete the data file request: diff --git a/robot-server/tests/integration/http_api/data_files/test_upload_data_file.tavern.yaml b/robot-server/tests/integration/http_api/data_files/test_upload_data_file.tavern.yaml index 44c5b4700f7..ee10cdadd9e 100644 --- a/robot-server/tests/integration/http_api/data_files/test_upload_data_file.tavern.yaml +++ b/robot-server/tests/integration/http_api/data_files/test_upload_data_file.tavern.yaml @@ -21,6 +21,7 @@ stages: id: !anystr name: "color_codes.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: "uploaded" - name: Upload same file again. It should not create a new record. request: @@ -49,6 +50,7 @@ stages: id: !anystr name: "sample_record.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: "uploaded" - name: Get color_codes.csv file info request: @@ -61,3 +63,4 @@ stages: id: '{data_file_id}' name: "color_codes.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: "uploaded" diff --git a/robot-server/tests/integration/http_api/protocols/test_analyses_with_csv_file_parameters.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_analyses_with_csv_file_parameters.tavern.yaml index addd5a43c79..d9a55f023d1 100644 --- a/robot-server/tests/integration/http_api/protocols/test_analyses_with_csv_file_parameters.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_analyses_with_csv_file_parameters.tavern.yaml @@ -17,6 +17,7 @@ stages: csv_file_id: data.id csv_file_name: data.name file_created_at: data.createdAt + source: "uploaded" status_code: - 201 json: @@ -24,6 +25,7 @@ stages: id: !anystr name: "sample_record.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: "uploaded" - name: Upload a protocol request: @@ -40,6 +42,7 @@ stages: protocol_id: data.id analysis_id: data.analysisSummaries[0].id run_time_parameters_data1: data.analysisSummaries[0].runTimeParameters + source: "uploaded" strict: - json:off status_code: 201 @@ -130,3 +133,4 @@ stages: - id: '{csv_file_id}' name: '{csv_file_name}' createdAt: '{file_created_at}' + source: 'uploaded' diff --git a/robot-server/tests/integration/http_api/protocols/test_get_csv_files_used_with_protocol.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_get_csv_files_used_with_protocol.tavern.yaml index 63ac864856e..9b82f473491 100644 --- a/robot-server/tests/integration/http_api/protocols/test_get_csv_files_used_with_protocol.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_get_csv_files_used_with_protocol.tavern.yaml @@ -229,9 +229,12 @@ stages: - id: '{data_file_1_id}' name: "test.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: 'uploaded' - id: '{data_file_2_id}' name: "sample_record.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: 'uploaded' - id: '{data_file_3_id}' name: "sample_plates.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: 'uploaded' diff --git a/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml index 8916ebd1cf2..1f44f7101c7 100644 --- a/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_run_with_run_time_parameters.tavern.yaml @@ -34,6 +34,7 @@ stages: id: !anystr name: "sample_plates.csv" createdAt: !re_fullmatch "\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d+(Z|([+-]\\d{2}:\\d{2}))" + source: "uploaded" - name: Create run from protocol request: diff --git a/robot-server/tests/persistence/test_tables.py b/robot-server/tests/persistence/test_tables.py index a970bb86d89..0277919aa30 100644 --- a/robot-server/tests/persistence/test_tables.py +++ b/robot-server/tests/persistence/test_tables.py @@ -117,6 +117,9 @@ CREATE UNIQUE INDEX ix_run_run_id_index_in_run ON run_command (run_id, index_in_run) """, """ + CREATE INDEX ix_data_files_source ON data_files (source) + """, + """ CREATE INDEX ix_protocol_protocol_kind ON protocol (protocol_kind) """, """ @@ -128,7 +131,9 @@ name VARCHAR NOT NULL, file_hash VARCHAR NOT NULL, created_at DATETIME NOT NULL, - PRIMARY KEY (id) + source VARCHAR(9) NOT NULL, + PRIMARY KEY (id), + CONSTRAINT datafilesourcesqlenum CHECK (source IN ('uploaded', 'generated')) ) """, """ diff --git a/robot-server/tests/protocols/test_completed_analysis_store.py b/robot-server/tests/protocols/test_completed_analysis_store.py index 4426ad062c7..42c12565c14 100644 --- a/robot-server/tests/protocols/test_completed_analysis_store.py +++ b/robot-server/tests/protocols/test_completed_analysis_store.py @@ -8,6 +8,7 @@ from sqlalchemy.engine import Engine from decoy import Decoy +from robot_server.data_files.models import DataFileSource from robot_server.persistence.tables import ( analysis_table, analysis_primitive_type_rtp_table, @@ -21,7 +22,10 @@ ProtocolSource, JsonProtocolConfig, ) -from robot_server.data_files.data_files_store import DataFilesStore, DataFileInfo +from robot_server.data_files.data_files_store import ( + DataFilesStore, + DataFileInfo, +) from robot_server.protocols.analysis_memcache import MemoryCache from robot_server.protocols.analysis_models import ( CompletedAnalysis, @@ -326,6 +330,7 @@ async def test_store_and_get_csv_rtps_by_analysis_id( id="file-id", name="my_csv_file.csv", file_hash="file-hash", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=1, day=1, tzinfo=timezone.utc), ) ) @@ -460,6 +465,7 @@ async def test_make_room_and_add_handles_rtp_tables_correctly( id="file-id", name="my_csv_file.csv", file_hash="file-hash", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=1, day=1, tzinfo=timezone.utc), ) ) diff --git a/robot-server/tests/protocols/test_protocol_store.py b/robot-server/tests/protocols/test_protocol_store.py index 5d413ad7fa3..ca965d471a8 100644 --- a/robot-server/tests/protocols/test_protocol_store.py +++ b/robot-server/tests/protocols/test_protocol_store.py @@ -14,8 +14,11 @@ PythonProtocolConfig, ) -from robot_server.data_files.data_files_store import DataFilesStore, DataFileInfo -from robot_server.data_files.models import DataFile +from robot_server.data_files.data_files_store import ( + DataFilesStore, + DataFileInfo, +) +from robot_server.data_files.models import DataFile, DataFileSource from robot_server.protocols.analysis_memcache import MemoryCache from robot_server.protocols.analysis_models import ( CompletedAnalysis, @@ -588,6 +591,7 @@ async def test_get_referenced_data_files( id="data-file-id-1", name="file-name", file_hash="abc123", + source=DataFileSource.UPLOADED, created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), ) ) @@ -596,6 +600,7 @@ async def test_get_referenced_data_files( id="data-file-id-2", name="file-name", file_hash="abc123", + source=DataFileSource.UPLOADED, created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), ) ) @@ -604,6 +609,7 @@ async def test_get_referenced_data_files( id="data-file-id-3", name="file-name", file_hash="abc123", + source=DataFileSource.UPLOADED, created_at=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), ) ) @@ -653,15 +659,18 @@ async def test_get_referenced_data_files( id="data-file-id-1", name="file-name", createdAt=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + source=DataFileSource.UPLOADED, ), DataFile( id="data-file-id-2", name="file-name", createdAt=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + source=DataFileSource.UPLOADED, ), DataFile( id="data-file-id-3", name="file-name", createdAt=datetime(year=2021, month=1, day=1, tzinfo=timezone.utc), + source=DataFileSource.UPLOADED, ), ] diff --git a/robot-server/tests/protocols/test_protocols_router.py b/robot-server/tests/protocols/test_protocols_router.py index a2ad10dbda0..637a2ee082f 100644 --- a/robot-server/tests/protocols/test_protocols_router.py +++ b/robot-server/tests/protocols/test_protocols_router.py @@ -30,8 +30,11 @@ BufferedFile, ) -from robot_server.data_files.data_files_store import DataFilesStore, DataFileInfo -from robot_server.data_files.models import DataFile +from robot_server.data_files.data_files_store import ( + DataFilesStore, + DataFileInfo, +) +from robot_server.data_files.models import DataFile, DataFileSource from robot_server.errors.error_responses import ApiError from robot_server.protocols.analyses_manager import AnalysesManager from robot_server.protocols.protocol_analyzer import ProtocolAnalyzer @@ -728,6 +731,7 @@ async def test_create_new_protocol_with_run_time_params( id="123", name="file.abc", file_hash="xyz", + source=DataFileSource.UPLOADED, created_at=datetime(year=2022, month=2, day=2), ) ) @@ -993,6 +997,7 @@ async def test_create_existing_protocol_with_different_run_time_params( id="123", name="file.abc", file_hash="xyz", + source=DataFileSource.UPLOADED, created_at=datetime(year=2022, month=2, day=2), ) ) @@ -1813,6 +1818,7 @@ async def test_update_protocol_analyses_with_new_rtp_values( id="123", name="foo.csv", file_hash="xyz", + source=DataFileSource.UPLOADED, created_at=datetime(year=2022, month=2, day=2), ) ) @@ -2172,11 +2178,13 @@ async def test_get_data_files( id="id1", name="csv-file1.csv", createdAt=datetime(year=2024, month=1, day=1), + source=DataFileSource.UPLOADED, ), DataFile( id="id2", name="csv-file2.csv", createdAt=datetime(year=2024, month=1, day=1), + source=DataFileSource.UPLOADED, ), ] decoy.when(protocol_store.has(protocol_id="protocol-id")).then_return(True) diff --git a/robot-server/tests/runs/router/test_base_router.py b/robot-server/tests/runs/router/test_base_router.py index 0a0266c0c65..9052b588bc9 100644 --- a/robot-server/tests/runs/router/test_base_router.py +++ b/robot-server/tests/runs/router/test_base_router.py @@ -20,8 +20,12 @@ from opentrons.hardware_control.nozzle_manager import NozzleConfigurationType, NozzleMap -from robot_server.data_files.data_files_store import DataFilesStore, DataFileInfo +from robot_server.data_files.data_files_store import ( + DataFilesStore, + DataFileInfo, +) +from robot_server.data_files.models import DataFileSource from robot_server.errors.error_responses import ApiError from robot_server.runs.error_recovery_models import ErrorRecoveryPolicy from robot_server.service.json_api import ( @@ -250,6 +254,7 @@ async def test_create_protocol_run( name="abc.xyz", file_hash="987", created_at=datetime(month=1, day=2, year=2024), + source=DataFileSource.UPLOADED, ) ) decoy.when( diff --git a/robot-server/tests/runs/test_run_store.py b/robot-server/tests/runs/test_run_store.py index ce6f8326c22..17a5c3b252f 100644 --- a/robot-server/tests/runs/test_run_store.py +++ b/robot-server/tests/runs/test_run_store.py @@ -5,13 +5,17 @@ import pytest from decoy import Decoy -from robot_server.data_files.data_files_store import DataFileInfo, DataFilesStore +from robot_server.data_files.data_files_store import ( + DataFileInfo, + DataFilesStore, +) from sqlalchemy.engine import Engine from unittest import mock from opentrons_shared_data.pipette.types import PipetteNameType from opentrons_shared_data.errors.codes import ErrorCodes +from robot_server.data_files.models import DataFileSource from robot_server.protocols.protocol_store import ProtocolNotFoundError from robot_server.runs.run_store import ( CSVParameterRunResource, @@ -296,6 +300,7 @@ async def test_insert_and_get_csv_rtp( id="file-id", name="my_csv_file.csv", file_hash="file-hash", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=1, day=1, tzinfo=timezone.utc), ) ) @@ -524,6 +529,7 @@ async def test_remove_run( id="file-id", name="my_csv_file.csv", file_hash="file-hash", + source=DataFileSource.UPLOADED, created_at=datetime(year=2024, month=1, day=1, tzinfo=timezone.utc), ) )