Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api, robot-server, app): Handle the Plate Reader lid while held by the gripper when the Estop is pressed. #16574

Merged
merged 27 commits into from
Oct 29, 2024

Conversation

vegano1
Copy link
Contributor

@vegano1 vegano1 commented Oct 22, 2024

Overview

We don't want customers manually handling the Plate Reader lid as this can cause damage to the device when you press the estop while the gripper is holding the plate reader lid it will eventually home the gantry and hold onto the lid. When a new protocol is started the gripper jaws will open, causing the plate reader lid to fall and potentially get damaged. This pull request makes it so we automatically place the plate reader lid in its dock (staging area) when the robot resumes operating after physically and logically disengaging the estop.

Closes: PLAT-451

Test Plan and Hands on Testing

  • Make sure we can run a plate reader protocol without issues
  • Make sure that the plate reader open/close_lid commands still work
  • Make sure the plate reader lid is returned to its dock (staging area) after the stop is physically and logically disengaged.
  • Make sure the run is canceled when the estop is pressed
  • Make sure the estop modal is shown while placing the plate reader lid back in its dock
  • Make sure that we only call placeLabware when the estop is pressed
  • Make sure all the above works on the ODD and Desktop app.
  • Make sure the plate reader does not get disconnected when the e-stop is pressed
  • Make sure the plate reader lid can still be moved when loading the module from the protocol setup.
from typing import cast
from opentrons import protocol_api
from opentrons.protocol_api.module_contexts import AbsorbanceReaderContext

# metadata
metadata = {
    'protocolName': 'Absorbance Reader Multi read test',
    'author': 'Platform Expansion',
}

requirements = {
    "robotType": "Flex",
    "apiLevel": "2.21",
}

# protocol run function
def run(protocol: protocol_api.ProtocolContext):
    mod = cast(AbsorbanceReaderContext, protocol.load_module("absorbanceReaderV1", "C3"))
    plate = protocol.load_labware("nest_96_wellplate_200ul_flat", "C2")
    tiprack_1000 = protocol.load_labware(load_name='opentrons_flex_96_tiprack_1000ul', location="B2")
    trash_labware = protocol.load_trash_bin("B3")
    instrument = protocol.load_instrument("flex_8channel_1000", "right", tip_racks=[tiprack_1000])
    instrument.trash_container = trash_labware

    # pick up tip and perform action
    instrument.pick_up_tip(tiprack_1000.wells_by_name()['A1'])
    instrument.aspirate(100, plate.wells_by_name()['A1'])
    instrument.dispense(100, plate.wells_by_name()['B1'])
    instrument.return_tip()
   
    # Initialize to a single wavelength with reference wavelength
    # Issue: Make sure there is no labware here or youll get an error
    mod.close_lid()
    mod.initialize('single', [600], 450)

    # NOTE: CANNOT INITIALIZE WITH THE LID OPEN

    # Remove the Plate Reader lid using the Gripper.
    mod.open_lid()
    protocol.move_labware(plate, mod, use_gripper=True)
    mod.close_lid()

    # Take a reading and show the resulting absorbance values.
    # Issue: cant read before you initialize or you an get an error
    result = mod.read()
    msg = f"single: {result}"
    protocol.comment(msg=msg)
    protocol.pause(msg=msg)

    # Initialize to multiple wavelengths
    protocol.pause(msg="Perform Multi Read")
    mod.open_lid()
    protocol.move_labware(plate, "C2", use_gripper=True)
    mod.close_lid()
    mod.initialize('multi', [450, 570, 600])

    # Open the lid and move the labware into the reader
    mod.open_lid()
    protocol.move_labware(plate, mod, use_gripper=True)

    # pick up tip and perform action on labware inside plate reader
    instrument.pick_up_tip(tiprack_1000.wells_by_name()['A1'])
    instrument.aspirate(100, plate.wells_by_name()['A1'])
    instrument.dispense(100, plate.wells_by_name()['B1'])
    instrument.return_tip()

    mod.close_lid()

    # Take reading
    result = mod.read()
    msg = f"multi: {result}"
    protocol.comment(msg=msg)
    protocol.pause(msg=msg)

    # Place the Plate Reader lid back on using the Gripper.
    mod.open_lid()
    protocol.move_labware(plate, "C2", use_gripper=True)
    mod.close_lid() 

Changelog

  • Add a new unsafe/placeLabware command to PE to place labware the gripper is already holding on to.
  • Add a new placeLabwareState to the runs/{run_id}/currentState endpoint to inform the client of the plate reader lid.
  • Add a new usePlacePlateReaderLid hook on the client to conditionally call the unsafe/placeLabware command when the placeLabwareState returns the appropriate values.
  • Fix an issue with the plate reader disconnecting when the estop was pressed
  • Fix an issue where the plate reader lid was not loaded if the module was added during module setup.

Review requests

  • My react is rusty, please ensure I'm not doing something dumb.
  • Is there anything else we should be checking in the unsafe/placeLabware command?

Risk assessment

med, this changes post-estop behavior.

@vegano1 vegano1 requested review from a team as code owners October 22, 2024 23:17
@vegano1 vegano1 requested review from TamarZanzouri, smb2268, SyntaxColoring and mjhuff and removed request for a team and TamarZanzouri October 22, 2024 23:17
Copy link
Contributor

@mjhuff mjhuff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice first go at one of the more complicated parts of React/JS!

See comments. I think the serious things are:

  1. We're always creating a maintenance run when the estop modal is popped.
  2. We can use existing utils to do most of the heavy lifting in usePlacePlateReaderLid.
  3. We're always closing the estop modal while plate reader commands may be running.

app/src/App/OnDeviceDisplayApp.tsx Outdated Show resolved Hide resolved
app/src/organisms/EmergencyStop/EstopPressedModal.tsx Outdated Show resolved Hide resolved
app/src/resources/modules/hooks/usePlacePlateReaderLid.ts Outdated Show resolved Hide resolved
app/src/resources/modules/hooks/usePlacePlateReaderLid.ts Outdated Show resolved Hide resolved
app/src/organisms/EmergencyStop/EstopPressedModal.tsx Outdated Show resolved Hide resolved
app/src/organisms/EmergencyStop/EstopPressedModal.tsx Outdated Show resolved Hide resolved
app/src/organisms/EmergencyStop/EstopPressedModal.tsx Outdated Show resolved Hide resolved
app/src/organisms/EmergencyStop/EstopTakeover.tsx Outdated Show resolved Hide resolved
@vegano1 vegano1 force-pushed the PLAT-451-handle-estop-with-plate-reader branch from cafa0c5 to 2cb3765 Compare October 23, 2024 14:19
Copy link
Contributor

@mjhuff mjhuff left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice job! I do like the e-stop control flow much more now.

(I only reviewed the app side of things).

Comment on lines +67 to +69
closeModal={closeModal}
isWaitingForResumeOperation={isWaitingForResumeOperation}
setIsWaitingForResumeOperation={setIsWaitingForResumeOperation}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two separate setters responsible for the same state seems strange to me, but I get that this code was here already, and I don't think it's worth addressing now.

@vegano1 vegano1 force-pushed the PLAT-451-handle-estop-with-plate-reader branch from 7af2905 to ddc24cb Compare October 24, 2024 20:55
Copy link

codecov bot commented Oct 24, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 74.02%. Comparing base (9797d74) to head (ddc24cb).
Report is 75 commits behind head on edge.

❗ There is a different number of reports uploaded between BASE (9797d74) and HEAD (ddc24cb). Click for more details.

HEAD has 1 upload less than BASE
Flag BASE (9797d74) HEAD (ddc24cb)
shared-data 3 2
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             edge   #16574      +/-   ##
==========================================
- Coverage   79.88%   74.02%   -5.87%     
==========================================
  Files         120       43      -77     
  Lines        4410     3176    -1234     
==========================================
- Hits         3523     2351    -1172     
+ Misses        887      825      -62     
Flag Coverage Δ
g-code-testing ?
shared-data 74.02% <ø> (-0.72%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

see 80 files with indirect coverage changes

Copy link
Contributor

@CaseyBatten CaseyBatten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Engine side logic looks good, nice work on this. A non-blocking question down below regarding the fallback case for how some of our geometry is calculated/resolved in this implementation that may effect future labware recovery behavior, but otherwise this looks great!

)
if substate.lid_id is None:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good, should provide coverage for the current run setup bug.

final_offsets = self._state_view.labware.get_labware_gripper_offsets(
labware_id, None
)
drop_offset = cast(Point, final_offsets.dropOffset) if final_offsets else None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything in the UnsafePlaceLabware command logic looks good, but just want to make sure I understand. When we're doing this None case here we are basically just lowering and releasing the labware in place + its movement center? So if we were between two slots, would we end up releasing the labware in a awkward deck space? Not really too much of an issue for this PR since the primary use case for now is plate reader lids which will always have resolved locations with final offsets.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, since these are just the dropoff offsets, in that case, we would navigate to the slot and drop off the labware +- some hardware stack-up margin. We should never be dropping labware in between 2 slots for example, unless the definition for that is way off.

Copy link
Contributor

@ahiuchingau ahiuchingau left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!! Just got a small question

await self._poller.stop()
await self._driver.disconnect()
"""Deactivate the module."""
pass
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does it mean when we deactivate the module now?

Copy link
Contributor Author

@vegano1 vegano1 Oct 28, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the context of modules, the deactivate function means we stop whatever target action we are doing, such as heating, shaking, etc. We don't have any actions other than reading for the plate reader, but these happen relatively quickly, so we don't have to worry about them. The problem here was that I thought deactivate meant stop and disconnect the module, which is not correct as deactivate gets called when the estop is pressed. In the case of the plate reader it would disconnect and reconnect when the estop was pressed. This change fixes that behavior by removing the stop and disconnect to the cleanup method.

Copy link
Contributor

@SyntaxColoring SyntaxColoring left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some drive-by comments. No objections to any of this merging as-is if you need to get it in for the v8.2.0 feature freeze.

async def execute(
self, params: UnsafePlaceLabwareParams
) -> SuccessData[UnsafePlaceLabwareResult, None]:
"""Place Labware."""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having not really been involved in this, I would find it helpful if one of these docstrings described when it's appropriate and legal for a client to use this unsafe command, and when it's not.

i.e., why is this called unsafe/placeLabware, and not just placeLabware.

I would also consider that the unfamiliar audience is probably comparing this with moveLabware and loadLabware, so we should describe how it's distinct from those.

class RunCurrentState(BaseModel):
"""Current details about a run."""

estopEngaged: bool = Field(..., description="")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: If we don't have a description to offer, let's just leave description unset instead of doing description="".

i.e. estopEngaged: bool instead of estopEngaged: bool = Field(..., description="").


labwareId: str = Field(..., description="The ID of the labware to place.")
location: OnDeckLabwareLocation = Field(
..., description="The location the Plate Reade lid should be in."
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
..., description="The location the Plate Reade lid should be in."
..., description="The location the Plate Reader lid should be in."

Comment on lines 319 to 330
class PlaceLabwareState(BaseModel):
"""Details the labware being placed by the gripper."""

labwareId: str = Field(..., description="The ID of the labware to place.")
location: OnDeckLabwareLocation = Field(
..., description="The location the Plate Reade lid should be in."
)
shouldPlaceDown: bool = Field(
..., description="Whether the gripper should place down the labware."
)


Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

placeLabwareState.location is defined here in terms of the plate reader lid, specifically, as opposed to labware locations in general. So, should we either:

  • Rename placeLabwareState to something that reflects that it's Plate-Reader–specific, and is not meant to be used for labware in general
  • Or, redefine placeLabwareState.location to not be specifically about the plate reader lid

@vegano1 vegano1 merged commit 272c4b6 into edge Oct 29, 2024
61 checks passed
@vegano1 vegano1 deleted the PLAT-451-handle-estop-with-plate-reader branch October 29, 2024 19:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants