opentrons 8.1.0__py2.py3-none-any.whl → 8.2.0__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (238) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +208 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/backends/ot3utils.py +1 -0
  20. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  21. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  22. opentrons/hardware_control/module_control.py +43 -2
  23. opentrons/hardware_control/modules/__init__.py +7 -1
  24. opentrons/hardware_control/modules/absorbance_reader.py +232 -83
  25. opentrons/hardware_control/modules/errors.py +7 -0
  26. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  27. opentrons/hardware_control/modules/magdeck.py +12 -3
  28. opentrons/hardware_control/modules/mod_abc.py +27 -2
  29. opentrons/hardware_control/modules/tempdeck.py +15 -7
  30. opentrons/hardware_control/modules/thermocycler.py +69 -3
  31. opentrons/hardware_control/modules/types.py +11 -5
  32. opentrons/hardware_control/modules/update.py +11 -5
  33. opentrons/hardware_control/modules/utils.py +3 -1
  34. opentrons/hardware_control/ot3_calibration.py +6 -6
  35. opentrons/hardware_control/ot3api.py +131 -94
  36. opentrons/hardware_control/poller.py +15 -11
  37. opentrons/hardware_control/protocols/__init__.py +1 -7
  38. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  39. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  40. opentrons/hardware_control/protocols/position_estimator.py +3 -1
  41. opentrons/hardware_control/types.py +2 -0
  42. opentrons/legacy_commands/helpers.py +8 -2
  43. opentrons/motion_planning/__init__.py +2 -0
  44. opentrons/motion_planning/waypoints.py +32 -0
  45. opentrons/protocol_api/__init__.py +2 -1
  46. opentrons/protocol_api/_liquid.py +87 -1
  47. opentrons/protocol_api/_parameter_context.py +10 -1
  48. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  49. opentrons/protocol_api/core/engine/instrument.py +29 -25
  50. opentrons/protocol_api/core/engine/labware.py +20 -4
  51. opentrons/protocol_api/core/engine/module_core.py +166 -17
  52. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +362 -0
  53. opentrons/protocol_api/core/engine/protocol.py +30 -2
  54. opentrons/protocol_api/core/instrument.py +2 -0
  55. opentrons/protocol_api/core/labware.py +4 -0
  56. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  57. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  58. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +6 -2
  59. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  60. opentrons/protocol_api/core/module.py +22 -4
  61. opentrons/protocol_api/core/protocol.py +6 -2
  62. opentrons/protocol_api/instrument_context.py +52 -20
  63. opentrons/protocol_api/labware.py +13 -1
  64. opentrons/protocol_api/module_contexts.py +115 -17
  65. opentrons/protocol_api/protocol_context.py +49 -5
  66. opentrons/protocol_api/validation.py +5 -3
  67. opentrons/protocol_engine/__init__.py +10 -9
  68. opentrons/protocol_engine/actions/__init__.py +3 -0
  69. opentrons/protocol_engine/actions/actions.py +30 -25
  70. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  71. opentrons/protocol_engine/clients/sync_client.py +1 -1
  72. opentrons/protocol_engine/clients/transports.py +1 -1
  73. opentrons/protocol_engine/commands/__init__.py +0 -4
  74. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  75. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +148 -0
  76. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +65 -9
  77. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +148 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/read.py +200 -0
  79. opentrons/protocol_engine/commands/aspirate.py +29 -16
  80. opentrons/protocol_engine/commands/aspirate_in_place.py +33 -16
  81. opentrons/protocol_engine/commands/blow_out.py +63 -14
  82. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  83. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  84. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  85. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  86. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  87. opentrons/protocol_engine/commands/command.py +31 -18
  88. opentrons/protocol_engine/commands/command_unions.py +37 -24
  89. opentrons/protocol_engine/commands/comment.py +5 -3
  90. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  91. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  92. opentrons/protocol_engine/commands/custom.py +5 -3
  93. opentrons/protocol_engine/commands/dispense.py +42 -20
  94. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  95. opentrons/protocol_engine/commands/drop_tip.py +70 -16
  96. opentrons/protocol_engine/commands/drop_tip_in_place.py +59 -13
  97. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  98. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  99. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  100. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  101. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  102. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  103. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  104. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  105. opentrons/protocol_engine/commands/home.py +11 -5
  106. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  107. opentrons/protocol_engine/commands/load_labware.py +28 -5
  108. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  109. opentrons/protocol_engine/commands/load_module.py +4 -6
  110. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  111. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  112. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  113. opentrons/protocol_engine/commands/move_labware.py +155 -23
  114. opentrons/protocol_engine/commands/move_relative.py +15 -3
  115. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  116. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  117. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  118. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  119. opentrons/protocol_engine/commands/pick_up_tip.py +51 -30
  120. opentrons/protocol_engine/commands/pipetting_common.py +47 -16
  121. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  122. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  123. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  124. opentrons/protocol_engine/commands/save_position.py +2 -3
  125. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  126. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  127. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  128. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  129. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  130. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  131. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  132. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  133. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  135. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  136. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  137. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  138. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  139. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  140. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  141. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  142. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  143. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  144. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  145. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  146. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  147. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  148. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +10 -4
  149. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  150. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  151. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  152. opentrons/protocol_engine/create_protocol_engine.py +60 -10
  153. opentrons/protocol_engine/engine_support.py +2 -1
  154. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  155. opentrons/protocol_engine/errors/__init__.py +20 -0
  156. opentrons/protocol_engine/errors/error_occurrence.py +8 -3
  157. opentrons/protocol_engine/errors/exceptions.py +127 -2
  158. opentrons/protocol_engine/execution/__init__.py +2 -0
  159. opentrons/protocol_engine/execution/command_executor.py +22 -13
  160. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  161. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  162. opentrons/protocol_engine/execution/equipment.py +2 -1
  163. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  164. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  165. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  166. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  167. opentrons/protocol_engine/execution/labware_movement.py +73 -22
  168. opentrons/protocol_engine/execution/movement.py +17 -7
  169. opentrons/protocol_engine/execution/pipetting.py +7 -4
  170. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  171. opentrons/protocol_engine/execution/run_control.py +1 -1
  172. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  173. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  174. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  175. opentrons/protocol_engine/notes/__init__.py +14 -2
  176. opentrons/protocol_engine/notes/notes.py +18 -1
  177. opentrons/protocol_engine/plugins.py +1 -1
  178. opentrons/protocol_engine/protocol_engine.py +47 -31
  179. opentrons/protocol_engine/resources/__init__.py +2 -0
  180. opentrons/protocol_engine/resources/deck_data_provider.py +19 -5
  181. opentrons/protocol_engine/resources/file_provider.py +161 -0
  182. opentrons/protocol_engine/resources/fixture_validation.py +11 -1
  183. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  184. opentrons/protocol_engine/state/__init__.py +0 -70
  185. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  186. opentrons/protocol_engine/state/command_history.py +21 -2
  187. opentrons/protocol_engine/state/commands.py +110 -31
  188. opentrons/protocol_engine/state/files.py +59 -0
  189. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  190. opentrons/protocol_engine/state/geometry.py +445 -59
  191. opentrons/protocol_engine/state/labware.py +264 -84
  192. opentrons/protocol_engine/state/liquids.py +1 -1
  193. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +21 -3
  194. opentrons/protocol_engine/state/modules.py +145 -90
  195. opentrons/protocol_engine/state/motion.py +33 -14
  196. opentrons/protocol_engine/state/pipettes.py +157 -317
  197. opentrons/protocol_engine/state/state.py +30 -1
  198. opentrons/protocol_engine/state/state_summary.py +3 -0
  199. opentrons/protocol_engine/state/tips.py +69 -114
  200. opentrons/protocol_engine/state/update_types.py +424 -0
  201. opentrons/protocol_engine/state/wells.py +236 -0
  202. opentrons/protocol_engine/types.py +90 -0
  203. opentrons/protocol_reader/file_format_validator.py +83 -15
  204. opentrons/protocol_runner/json_translator.py +21 -5
  205. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  206. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  207. opentrons/protocol_runner/protocol_runner.py +6 -3
  208. opentrons/protocol_runner/run_orchestrator.py +41 -6
  209. opentrons/protocols/advanced_control/mix.py +3 -5
  210. opentrons/protocols/advanced_control/transfers.py +125 -56
  211. opentrons/protocols/api_support/constants.py +1 -1
  212. opentrons/protocols/api_support/definitions.py +1 -1
  213. opentrons/protocols/api_support/labware_like.py +4 -4
  214. opentrons/protocols/api_support/tip_tracker.py +2 -2
  215. opentrons/protocols/api_support/types.py +15 -2
  216. opentrons/protocols/api_support/util.py +30 -42
  217. opentrons/protocols/duration/errors.py +1 -1
  218. opentrons/protocols/duration/estimator.py +50 -29
  219. opentrons/protocols/execution/dev_types.py +2 -2
  220. opentrons/protocols/execution/execute_json_v4.py +15 -10
  221. opentrons/protocols/execution/execute_python.py +8 -3
  222. opentrons/protocols/geometry/planning.py +12 -12
  223. opentrons/protocols/labware.py +17 -33
  224. opentrons/protocols/parameters/csv_parameter_interface.py +3 -1
  225. opentrons/simulate.py +3 -3
  226. opentrons/types.py +30 -3
  227. opentrons/util/logging_config.py +34 -0
  228. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/RECORD +235 -223
  230. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  231. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  232. opentrons/protocol_runner/thread_async_queue.py +0 -174
  233. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  234. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  235. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -17,11 +17,15 @@ from opentrons.protocol_engine.actions.actions import (
17
17
  RunCommandAction,
18
18
  SetErrorRecoveryPolicyAction,
19
19
  )
20
+ from opentrons.protocol_engine.commands.unsafe.unsafe_ungrip_labware import (
21
+ UnsafeUngripLabwareCommandType,
22
+ )
20
23
  from opentrons.protocol_engine.error_recovery_policy import (
21
24
  ErrorRecoveryPolicy,
22
25
  ErrorRecoveryType,
23
26
  )
24
27
  from opentrons.protocol_engine.notes.notes import CommandNote
28
+ from opentrons.protocol_engine.state import update_types
25
29
 
26
30
  from ..actions import (
27
31
  Action,
@@ -36,7 +40,7 @@ from ..actions import (
36
40
  DoorChangeAction,
37
41
  )
38
42
 
39
- from ..commands import Command, CommandStatus, CommandIntent
43
+ from ..commands import Command, CommandStatus, CommandIntent, CommandCreate
40
44
  from ..errors import (
41
45
  RunStoppedError,
42
46
  ErrorOccurrence,
@@ -49,7 +53,7 @@ from ..errors import (
49
53
  ProtocolCommandFailedError,
50
54
  )
51
55
  from ..types import EngineStatus
52
- from .abstract_store import HasState, HandlesActions
56
+ from ._abstract_store import HasState, HandlesActions
53
57
  from .command_history import (
54
58
  CommandEntry,
55
59
  CommandHistory,
@@ -95,7 +99,9 @@ class QueueStatus(enum.Enum):
95
99
  AWAITING_RECOVERY_PAUSED = enum.auto()
96
100
  """Execution of fixit commands has been paused.
97
101
 
98
- New protocol and fixit commands may be enqueued, but will wait to execute.
102
+ New protocol and fixit commands may be enqueued, but will usually wait to execute.
103
+ There are certain exceptions where fixit commands will still run.
104
+
99
105
  New setup commands may not be enqueued.
100
106
  """
101
107
 
@@ -136,6 +142,16 @@ class CommandPointer:
136
142
  index: int
137
143
 
138
144
 
145
+ @dataclass(frozen=True)
146
+ class _RecoveryTargetInfo:
147
+ """Info about the failed command that we're currently recovering from."""
148
+
149
+ command_id: str
150
+
151
+ state_update_if_false_positive: update_types.StateUpdate
152
+ """See `CommandView.get_state_update_if_continued()`."""
153
+
154
+
139
155
  @dataclass
140
156
  class CommandState:
141
157
  """State of all protocol engine command resources."""
@@ -200,8 +216,8 @@ class CommandState:
200
216
  stable. Eventually, we might want this info to be stored directly on each command.
201
217
  """
202
218
 
203
- recovery_target_command_id: Optional[str]
204
- """If we're currently recovering from a command failure, which command it was."""
219
+ recovery_target: Optional[_RecoveryTargetInfo]
220
+ """If we're currently recovering from a command failure, info about that command."""
205
221
 
206
222
  finish_error: Optional[ErrorOccurrence]
207
223
  """The error that happened during the post-run finish steps (homing & dropping tips), if any."""
@@ -248,7 +264,7 @@ class CommandStore(HasState[CommandState], HandlesActions):
248
264
  finish_error=None,
249
265
  failed_command=None,
250
266
  command_error_recovery_types={},
251
- recovery_target_command_id=None,
267
+ recovery_target=None,
252
268
  run_completed_at=None,
253
269
  run_started_at=None,
254
270
  latest_protocol_command_hash=None,
@@ -330,14 +346,17 @@ class CommandStore(HasState[CommandState], HandlesActions):
330
346
  def _handle_fail_command_action(self, action: FailCommandAction) -> None:
331
347
  prev_entry = self.state.command_history.get(action.command_id)
332
348
 
333
- if isinstance(action.error, EnumeratedError):
349
+ if isinstance(action.error, EnumeratedError): # The error was undefined.
334
350
  public_error_occurrence = ErrorOccurrence.from_failed(
335
351
  id=action.error_id,
336
352
  createdAt=action.failed_at,
337
353
  error=action.error,
338
354
  )
339
- else:
355
+ # An empty state update, to no-op.
356
+ state_update_if_false_positive = update_types.StateUpdate()
357
+ else: # The error was defined.
340
358
  public_error_occurrence = action.error.public
359
+ state_update_if_false_positive = action.error.state_update_if_false_positive
341
360
 
342
361
  self._update_to_failed(
343
362
  command_id=action.command_id,
@@ -349,6 +368,19 @@ class CommandStore(HasState[CommandState], HandlesActions):
349
368
  self._state.failed_command = self._state.command_history.get(action.command_id)
350
369
  self._state.failed_command_errors.append(public_error_occurrence)
351
370
 
371
+ if (
372
+ prev_entry.command.intent in (CommandIntent.PROTOCOL, None)
373
+ and action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY
374
+ ):
375
+ self._state.queue_status = QueueStatus.AWAITING_RECOVERY
376
+ self._state.recovery_target = _RecoveryTargetInfo(
377
+ command_id=action.command_id,
378
+ state_update_if_false_positive=state_update_if_false_positive,
379
+ )
380
+ self._state.has_entered_error_recovery = True
381
+
382
+ # When one command fails, we generally also cancel the commands that
383
+ # would have been queued after it.
352
384
  other_command_ids_to_fail: List[str]
353
385
  if prev_entry.command.intent == CommandIntent.SETUP:
354
386
  other_command_ids_to_fail = list(
@@ -368,7 +400,8 @@ class CommandStore(HasState[CommandState], HandlesActions):
368
400
  )
369
401
  elif (
370
402
  action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY
371
- or action.type == ErrorRecoveryType.IGNORE_AND_CONTINUE
403
+ or action.type == ErrorRecoveryType.CONTINUE_WITH_ERROR
404
+ or action.type == ErrorRecoveryType.ASSUME_FALSE_POSITIVE_AND_CONTINUE
372
405
  ):
373
406
  other_command_ids_to_fail = []
374
407
  else:
@@ -385,14 +418,6 @@ class CommandStore(HasState[CommandState], HandlesActions):
385
418
  notes=None,
386
419
  )
387
420
 
388
- if (
389
- prev_entry.command.intent in (CommandIntent.PROTOCOL, None)
390
- and action.type == ErrorRecoveryType.WAIT_FOR_RECOVERY
391
- ):
392
- self._state.queue_status = QueueStatus.AWAITING_RECOVERY
393
- self._state.recovery_target_command_id = action.command_id
394
- self._state.has_entered_error_recovery = True
395
-
396
421
  def _handle_play_action(self, action: PlayAction) -> None:
397
422
  if not self._state.run_result:
398
423
  self._state.run_started_at = (
@@ -420,13 +445,13 @@ class CommandStore(HasState[CommandState], HandlesActions):
420
445
  self, action: ResumeFromRecoveryAction
421
446
  ) -> None:
422
447
  self._state.queue_status = QueueStatus.RUNNING
423
- self._state.recovery_target_command_id = None
448
+ self._state.recovery_target = None
424
449
 
425
450
  def _handle_stop_action(self, action: StopAction) -> None:
426
451
  if not self._state.run_result:
427
- self._state.recovery_target_command_id = None
428
-
452
+ self._state.recovery_target = None
429
453
  self._state.queue_status = QueueStatus.PAUSED
454
+
430
455
  if action.from_estop:
431
456
  self._state.stopped_by_estop = True
432
457
  self._state.run_result = RunResult.FAILED
@@ -435,7 +460,9 @@ class CommandStore(HasState[CommandState], HandlesActions):
435
460
 
436
461
  def _handle_finish_action(self, action: FinishAction) -> None:
437
462
  if not self._state.run_result:
463
+ self._state.recovery_target = None
438
464
  self._state.queue_status = QueueStatus.PAUSED
465
+
439
466
  if action.set_run_status:
440
467
  self._state.run_result = (
441
468
  RunResult.SUCCEEDED
@@ -580,18 +607,19 @@ class CommandView(HasState[CommandState]):
580
607
  return self._state.command_history.get_all_commands()
581
608
 
582
609
  def get_slice(
583
- self,
584
- cursor: Optional[int],
585
- length: int,
610
+ self, cursor: Optional[int], length: int, include_fixit_commands: bool
586
611
  ) -> CommandSlice:
587
612
  """Get a subset of commands around a given cursor.
588
613
 
589
614
  If the cursor is omitted, a cursor will be selected automatically
590
615
  based on the currently running or most recently executed command.
591
616
  """
617
+ command_ids = self._state.command_history.get_filtered_command_ids(
618
+ include_fixit_commands=include_fixit_commands
619
+ )
592
620
  running_command = self._state.command_history.get_running_command()
593
621
  queued_command_ids = self._state.command_history.get_queue_ids()
594
- total_length = self._state.command_history.length()
622
+ total_length = len(command_ids)
595
623
 
596
624
  # TODO(mm, 2024-05-17): This looks like it's attempting to do the same thing
597
625
  # as self.get_current(), but in a different way. Can we unify them?
@@ -620,7 +648,9 @@ class CommandView(HasState[CommandState]):
620
648
  # start is inclusive, stop is exclusive
621
649
  actual_cursor = max(0, min(cursor, total_length - 1))
622
650
  stop = min(total_length, actual_cursor + length)
623
- commands = self._state.command_history.get_slice(start=actual_cursor, stop=stop)
651
+ commands = self._state.command_history.get_slice(
652
+ start=actual_cursor, stop=stop, command_ids=command_ids
653
+ )
624
654
 
625
655
  return CommandSlice(
626
656
  commands=commands,
@@ -737,6 +767,12 @@ class CommandView(HasState[CommandState]):
737
767
  next_fixit_cmd = self._state.command_history.get_fixit_queue_ids().head(None)
738
768
  if next_fixit_cmd and self._state.queue_status == QueueStatus.AWAITING_RECOVERY:
739
769
  return next_fixit_cmd
770
+ if (
771
+ next_fixit_cmd
772
+ and self._state.queue_status == QueueStatus.AWAITING_RECOVERY_PAUSED
773
+ and self._may_run_with_door_open(fixit_command=self.get(next_fixit_cmd))
774
+ ):
775
+ return next_fixit_cmd
740
776
 
741
777
  # if there is a setup command queued, prioritize it
742
778
  next_setup_cmd = self._state.command_history.get_setup_queue_ids().head(None)
@@ -852,11 +888,11 @@ class CommandView(HasState[CommandState]):
852
888
 
853
889
  def get_recovery_target(self) -> Optional[CommandPointer]:
854
890
  """Return the command currently undergoing error recovery, if any."""
855
- recovery_target_command_id = self._state.recovery_target_command_id
856
- if recovery_target_command_id is None:
891
+ recovery_target = self._state.recovery_target
892
+ if recovery_target is None:
857
893
  return None
858
894
  else:
859
- entry = self._state.command_history.get(recovery_target_command_id)
895
+ entry = self._state.command_history.get(recovery_target.command_id)
860
896
  return CommandPointer(
861
897
  command_id=entry.command.id,
862
898
  command_key=entry.command.key,
@@ -967,12 +1003,23 @@ class CommandView(HasState[CommandState]):
967
1003
  "Setup commands are not allowed after run has started."
968
1004
  )
969
1005
  elif action.request.intent == CommandIntent.FIXIT:
970
- if self._state.queue_status != QueueStatus.AWAITING_RECOVERY:
1006
+ if self.get_status() == EngineStatus.AWAITING_RECOVERY:
1007
+ return action
1008
+ elif self.get_status() in (
1009
+ EngineStatus.AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR,
1010
+ EngineStatus.AWAITING_RECOVERY_PAUSED,
1011
+ ):
1012
+ if self._may_run_with_door_open(fixit_command=action.request):
1013
+ return action
1014
+ else:
1015
+ raise FixitCommandNotAllowedError(
1016
+ f"{action.request.commandType} fixit command may not run"
1017
+ " until the door is closed and the run is played again."
1018
+ )
1019
+ else:
971
1020
  raise FixitCommandNotAllowedError(
972
1021
  "Fixit commands are not allowed when the run is not in a recoverable state."
973
1022
  )
974
- else:
975
- return action
976
1023
  else:
977
1024
  return action
978
1025
 
@@ -1057,3 +1104,35 @@ class CommandView(HasState[CommandState]):
1057
1104
  higher-level code.
1058
1105
  """
1059
1106
  return self._state.error_recovery_policy
1107
+
1108
+ def get_state_update_for_false_positive(self) -> update_types.StateUpdate:
1109
+ """Return the state update for if the current recovery target was a false positive.
1110
+
1111
+ If we're currently in error recovery mode, and you have decided that the
1112
+ underlying command error was a false positive, this returns a state update
1113
+ that will undo the error's effects on engine state.
1114
+ See `ProtocolEngine.resume_from_recovery(reconcile_false_positive=True)`.
1115
+ """
1116
+ if self._state.recovery_target is None:
1117
+ return update_types.StateUpdate() # Empty/no-op.
1118
+ else:
1119
+ return self._state.recovery_target.state_update_if_false_positive
1120
+
1121
+ def _may_run_with_door_open(
1122
+ self, *, fixit_command: Command | CommandCreate
1123
+ ) -> bool:
1124
+ """Return whether the given fixit command is exempt from the usual open-door auto pause.
1125
+
1126
+ This is required for certain error recovery flows, where we want the robot to
1127
+ do stuff while the door is open.
1128
+ """
1129
+ # CommandIntent.PROTOCOL and CommandIntent.SETUP have their own rules for whether
1130
+ # they run while the door is open. Passing one of those commands to this function
1131
+ # is probably a mistake in the caller's logic.
1132
+ assert fixit_command.intent == CommandIntent.FIXIT
1133
+
1134
+ # This type annotation is to make sure the string constant stays in sync and isn't typo'd.
1135
+ required_command_type: UnsafeUngripLabwareCommandType = "unsafe/ungripLabware"
1136
+ # todo(mm, 2024-10-04): Instead of allowlisting command types, maybe we should
1137
+ # add a `mayRunWithDoorOpen: bool` field to command requests.
1138
+ return fixit_command.commandType == required_command_type
@@ -0,0 +1,59 @@
1
+ """Basic protocol engine create file data state and store."""
2
+ from dataclasses import dataclass
3
+ from typing import List
4
+
5
+ from ._abstract_store import HasState, HandlesActions
6
+ from ..actions import Action, SucceedCommandAction
7
+ from ..commands import (
8
+ Command,
9
+ absorbance_reader,
10
+ )
11
+
12
+
13
+ @dataclass
14
+ class FileState:
15
+ """State of Engine created files."""
16
+
17
+ file_ids: List[str]
18
+
19
+
20
+ class FileStore(HasState[FileState], HandlesActions):
21
+ """File state container."""
22
+
23
+ _state: FileState
24
+
25
+ def __init__(self) -> None:
26
+ """Initialize a File store and its state."""
27
+ self._state = FileState(file_ids=[])
28
+
29
+ def handle_action(self, action: Action) -> None:
30
+ """Modify state in reaction to an action."""
31
+ if isinstance(action, SucceedCommandAction):
32
+ self._handle_command(action.command)
33
+
34
+ def _handle_command(self, command: Command) -> None:
35
+ if isinstance(command.result, absorbance_reader.ReadAbsorbanceResult):
36
+ if command.result.fileIds is not None:
37
+ self._state.file_ids.extend(command.result.fileIds)
38
+
39
+
40
+ class FileView(HasState[FileState]):
41
+ """Read-only engine created file state view."""
42
+
43
+ _state: FileState
44
+
45
+ def __init__(self, state: FileState) -> None:
46
+ """Initialize the view of file state.
47
+
48
+ Arguments:
49
+ state: File state dataclass used for tracking file creation status.
50
+ """
51
+ self._state = state
52
+
53
+ def get_filecount(self) -> int:
54
+ """Get the number of files currently created by the protocol."""
55
+ return len(self._state.file_ids)
56
+
57
+ def get_file_id_list(self) -> List[str]:
58
+ """Get the list of files by file ID created by the protocol."""
59
+ return self._state.file_ids