-
Notifications
You must be signed in to change notification settings - Fork 178
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): Pause when pick_up_tip()
errors in a Python protocol
#14753
Changes from 9 commits
42a36ec
33225f7
98446ba
459f14d
b4d4d80
b61913f
9322657
33d33c9
dbb03db
b13db26
838572b
1d5ba4b
6353d7d
1ef9bc9
cb5bca1
51bd1d2
6b79625
0213657
ff22758
8ad0eef
bd14851
a880096
50e1706
97b0ada
cf1e936
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -167,6 +167,7 @@ async def execute(self, command_id: str) -> None: | |||||||||
FailCommandAction( | ||||||||||
error=error, | ||||||||||
command_id=running_command.id, | ||||||||||
running_command=running_command, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess I am wondering why we need this in the action? dont we have the failed command stored in PE already? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's a hack, we probably don't need it. See this comment: opentrons/api/src/opentrons/protocol_engine/actions/actions.py Lines 177 to 180 in bd14851
|
||||||||||
error_id=self._model_utils.generate_id(), | ||||||||||
failed_at=self._model_utils.get_timestamp(), | ||||||||||
notes=note_tracker.get_notes(), | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -242,6 +242,27 @@ async def add_and_execute_command( | |
await self.wait_for_command(command.id) | ||
return self._state_store.commands.get(command.id) | ||
|
||
async def add_and_execute_command_wait_for_recovery( | ||
self, request: commands.CommandCreate | ||
) -> None: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm open to ideas for better names for this. Or maybe we should make it a |
||
"""Like `add_and_execute_command()`, except wait for error recovery. | ||
|
||
Unlike `add_and_execute_command()`, if the command fails, this will not | ||
immediately return the failed command. Instead, if the error is recoverable, | ||
it will wait until error recovery has completed (e.g. when some other task | ||
calls `self.resume_from_recovery()`). | ||
""" | ||
command = self.add_command(request) | ||
await self.wait_for_command(command_id=command.id) | ||
await self._state_store.wait_for( | ||
# Per the warnings on `wait_for()`, we want our condition function to | ||
# specifically check that *this* command's recovery has completed, | ||
# rather than just checking that the overall run state | ||
# != "awaiting-recovery." | ||
self.state_view.commands.get_command_recovery_complete, | ||
command_id=command.id, | ||
) | ||
|
||
def estop( | ||
self, | ||
# TODO(mm, 2024-03-26): Maintenance runs are a robot-server concept that | ||
|
@@ -251,6 +272,15 @@ def estop( | |
) -> None: | ||
"""Signal to the engine that an estop event occurred. | ||
|
||
If an estop happens while the robot is moving, lower layers physically stop | ||
motion and raise the event as an exception, which fails the Protocol Engine | ||
command. No action from the `ProtocolEngine` caller is needed to handle that. | ||
|
||
However, if an estop happens in between commands, or in the middle of | ||
a command like `comment` or `waitForDuration` that doesn't access the hardware, | ||
`ProtocolEngine` needs to be told about it so it can treat it as a fatal run | ||
error and stop executing more commands. This method is how to do that. | ||
|
||
If there are any queued commands for the engine, they will be marked | ||
as failed due to the estop event. If there aren't any queued commands | ||
*and* this is a maintenance run (which has commands queued one-by-one), | ||
|
@@ -261,14 +291,26 @@ def estop( | |
""" | ||
if self._state_store.commands.get_is_stopped(): | ||
return | ||
current_id = ( | ||
running_or_next_queued_id = ( | ||
self._state_store.commands.get_running_command_id() | ||
or self._state_store.commands.state.queued_command_ids.head(None) | ||
# TODO(mm, 2024-04-02): Is it possible for the next queued command to | ||
SyntaxColoring marked this conversation as resolved.
Show resolved
Hide resolved
|
||
# be a setup command? That wouldn't show up in queued_command_ids. | ||
) | ||
running_or_next_queued = ( | ||
self._state_store.commands.get(running_or_next_queued_id) | ||
if running_or_next_queued_id is not None | ||
else None | ||
) | ||
|
||
if current_id is not None: | ||
if running_or_next_queued_id is not None: | ||
assert running_or_next_queued is not None | ||
|
||
fail_action = FailCommandAction( | ||
command_id=current_id, | ||
command_id=running_or_next_queued_id, | ||
# FIXME(mm, 2024-04-02): As of https://github.com/Opentrons/opentrons/pull/14726, | ||
# this action is only legal if the command is running, not queued. | ||
Comment on lines
+322
to
+323
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think a lot of this |
||
running_command=running_or_next_queued, | ||
error_id=self._model_utils.generate_id(), | ||
failed_at=self._model_utils.get_timestamp(), | ||
error=EStopActivatedError(message="Estop Activated"), | ||
|
@@ -277,12 +319,21 @@ def estop( | |
) | ||
self._action_dispatcher.dispatch(fail_action) | ||
|
||
# In the case where the running command was a setup command - check if there | ||
# are any pending *run* commands and, if so, clear them all | ||
current_id = self._state_store.commands.state.queued_command_ids.head(None) | ||
if current_id is not None: | ||
# The FailCommandAction above will have cleared all the queued protocol | ||
# OR setup commands, depending on whether we gave it a protocol or setup | ||
# command. We want both to be cleared in either case. So, do that here. | ||
running_or_next_queued_id = ( | ||
self._state_store.commands.state.queued_command_ids.head(None) | ||
) | ||
if running_or_next_queued_id is not None: | ||
running_or_next_queued = self._state_store.commands.get( | ||
running_or_next_queued_id | ||
) | ||
fail_action = FailCommandAction( | ||
command_id=current_id, | ||
command_id=running_or_next_queued_id, | ||
# FIXME(mm, 2024-04-02): As of https://github.com/Opentrons/opentrons/pull/14726, | ||
# this action is only legal if the command is running, not queued. | ||
running_command=running_or_next_queued, | ||
error_id=self._model_utils.generate_id(), | ||
failed_at=self._model_utils.get_timestamp(), | ||
error=EStopActivatedError(message="Estop Activated"), | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -715,6 +715,26 @@ def get_all_commands_final(self) -> bool: | |
|
||
return no_command_running and no_command_to_execute | ||
|
||
def get_command_recovery_complete(self, command_id: str) -> bool: | ||
SyntaxColoring marked this conversation as resolved.
Show resolved
Hide resolved
|
||
"""Return whether we're finished with error recovery for the given command. | ||
|
||
If the given command didn't have a recovery phase (because it hasn't completed | ||
yet, or because it succeeded without an error, or because it failed with an | ||
error that wasn't recoverable), that counts as `True`. | ||
|
||
If the given command did have a recovery phase, but it was interrupted by a | ||
stop, that also counts as `True`. | ||
""" | ||
command_is_most_recent_to_fail = ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is a little gross to me, could we save the command we're currently recovering from explicitly? like, what happens if a fixit command fails, wouldn't it instantly make this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changed in a880096. |
||
self._state.failed_command is not None | ||
and command_id == self._state.failed_command.command.id | ||
) | ||
if command_is_most_recent_to_fail: | ||
recovery_is_ongoing = self.get_status() == EngineStatus.AWAITING_RECOVERY | ||
return not recovery_is_ongoing | ||
else: | ||
return True | ||
|
||
def raise_fatal_command_error(self) -> None: | ||
"""Raise the run's fatal command error, if there was one, as an exception. | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I disagree - I think there are three answers each of which is honestly reasonable, but one which is the best (and also hardest). As a preamble, we should probably push last-location down to the engine.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
After looking at this closer, I think it's appropriate to call this
set_last_location()
unconditionally. See this new comment:opentrons/api/src/opentrons/protocol_api/core/engine/instrument.py
Lines 412 to 423 in 6b79625