opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__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 (190) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +4 -1
  3. opentrons/config/__init__.py +7 -0
  4. opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
  5. opentrons/drivers/heater_shaker/abstract.py +5 -0
  6. opentrons/drivers/heater_shaker/driver.py +10 -0
  7. opentrons/drivers/heater_shaker/simulator.py +4 -0
  8. opentrons/drivers/thermocycler/abstract.py +6 -0
  9. opentrons/drivers/thermocycler/driver.py +61 -10
  10. opentrons/drivers/thermocycler/simulator.py +6 -0
  11. opentrons/drivers/vacuum_module/__init__.py +5 -0
  12. opentrons/drivers/vacuum_module/abstract.py +93 -0
  13. opentrons/drivers/vacuum_module/driver.py +208 -0
  14. opentrons/drivers/vacuum_module/errors.py +39 -0
  15. opentrons/drivers/vacuum_module/simulator.py +85 -0
  16. opentrons/drivers/vacuum_module/types.py +79 -0
  17. opentrons/execute.py +3 -0
  18. opentrons/hardware_control/api.py +24 -5
  19. opentrons/hardware_control/backends/controller.py +8 -2
  20. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  21. opentrons/hardware_control/backends/ot3controller.py +35 -2
  22. opentrons/hardware_control/backends/ot3simulator.py +3 -1
  23. opentrons/hardware_control/backends/ot3utils.py +37 -0
  24. opentrons/hardware_control/backends/simulator.py +2 -1
  25. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  26. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  27. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  28. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  29. opentrons/hardware_control/emulation/settings.py +1 -1
  30. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  31. opentrons/hardware_control/module_control.py +105 -10
  32. opentrons/hardware_control/modules/__init__.py +3 -0
  33. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  34. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  35. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  36. opentrons/hardware_control/modules/magdeck.py +8 -4
  37. opentrons/hardware_control/modules/mod_abc.py +14 -6
  38. opentrons/hardware_control/modules/tempdeck.py +25 -5
  39. opentrons/hardware_control/modules/thermocycler.py +68 -11
  40. opentrons/hardware_control/modules/types.py +20 -1
  41. opentrons/hardware_control/modules/utils.py +11 -4
  42. opentrons/hardware_control/motion_utilities.py +6 -6
  43. opentrons/hardware_control/nozzle_manager.py +3 -0
  44. opentrons/hardware_control/ot3api.py +92 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +12 -4
  47. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  48. opentrons/hardware_control/types.py +43 -2
  49. opentrons/legacy_commands/commands.py +58 -5
  50. opentrons/legacy_commands/module_commands.py +52 -0
  51. opentrons/legacy_commands/protocol_commands.py +53 -1
  52. opentrons/legacy_commands/types.py +155 -1
  53. opentrons/motion_planning/deck_conflict.py +17 -12
  54. opentrons/motion_planning/waypoints.py +15 -29
  55. opentrons/protocol_api/__init__.py +5 -1
  56. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  57. opentrons/protocol_api/_types.py +8 -1
  58. opentrons/protocol_api/core/common.py +3 -1
  59. opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
  60. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  61. opentrons/protocol_api/core/engine/instrument.py +109 -26
  62. opentrons/protocol_api/core/engine/labware.py +8 -1
  63. opentrons/protocol_api/core/engine/module_core.py +95 -4
  64. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +4 -18
  65. opentrons/protocol_api/core/engine/protocol.py +51 -2
  66. opentrons/protocol_api/core/engine/stringify.py +2 -0
  67. opentrons/protocol_api/core/engine/tasks.py +48 -0
  68. opentrons/protocol_api/core/engine/well.py +8 -0
  69. opentrons/protocol_api/core/instrument.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  71. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  72. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  73. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  74. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  75. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  76. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  77. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  78. opentrons/protocol_api/core/module.py +58 -2
  79. opentrons/protocol_api/core/protocol.py +23 -2
  80. opentrons/protocol_api/core/tasks.py +31 -0
  81. opentrons/protocol_api/core/well.py +4 -0
  82. opentrons/protocol_api/instrument_context.py +388 -2
  83. opentrons/protocol_api/labware.py +10 -2
  84. opentrons/protocol_api/module_contexts.py +170 -6
  85. opentrons/protocol_api/protocol_context.py +87 -21
  86. opentrons/protocol_api/robot_context.py +41 -25
  87. opentrons/protocol_api/tasks.py +48 -0
  88. opentrons/protocol_api/validation.py +49 -3
  89. opentrons/protocol_engine/__init__.py +4 -0
  90. opentrons/protocol_engine/actions/__init__.py +6 -2
  91. opentrons/protocol_engine/actions/actions.py +31 -9
  92. opentrons/protocol_engine/clients/sync_client.py +42 -7
  93. opentrons/protocol_engine/commands/__init__.py +56 -0
  94. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  96. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  97. opentrons/protocol_engine/commands/aspirate.py +1 -0
  98. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  99. opentrons/protocol_engine/commands/capture_image.py +302 -0
  100. opentrons/protocol_engine/commands/command.py +2 -0
  101. opentrons/protocol_engine/commands/command_unions.py +62 -0
  102. opentrons/protocol_engine/commands/create_timer.py +83 -0
  103. opentrons/protocol_engine/commands/dispense.py +1 -0
  104. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  105. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  106. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  107. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  108. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  109. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  110. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  111. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  112. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  113. opentrons/protocol_engine/commands/move_labware.py +3 -4
  114. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  115. opentrons/protocol_engine/commands/movement_common.py +31 -2
  116. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  117. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  118. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  119. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  120. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  122. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  123. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  124. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  125. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  126. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  127. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  128. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  129. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  130. opentrons/protocol_engine/engine_support.py +3 -0
  131. opentrons/protocol_engine/errors/__init__.py +12 -0
  132. opentrons/protocol_engine/errors/exceptions.py +119 -0
  133. opentrons/protocol_engine/execution/__init__.py +4 -0
  134. opentrons/protocol_engine/execution/command_executor.py +62 -1
  135. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  136. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  137. opentrons/protocol_engine/execution/movement.py +2 -0
  138. opentrons/protocol_engine/execution/pipetting.py +26 -25
  139. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  140. opentrons/protocol_engine/execution/run_control.py +8 -0
  141. opentrons/protocol_engine/execution/task_handler.py +157 -0
  142. opentrons/protocol_engine/protocol_engine.py +137 -36
  143. opentrons/protocol_engine/resources/__init__.py +4 -0
  144. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  145. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  146. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  147. opentrons/protocol_engine/resources/file_provider.py +133 -58
  148. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  149. opentrons/protocol_engine/slot_standardization.py +2 -0
  150. opentrons/protocol_engine/state/_well_math.py +60 -18
  151. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  152. opentrons/protocol_engine/state/camera.py +54 -0
  153. opentrons/protocol_engine/state/commands.py +37 -14
  154. opentrons/protocol_engine/state/geometry.py +276 -379
  155. opentrons/protocol_engine/state/labware.py +62 -108
  156. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  157. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  158. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  159. opentrons/protocol_engine/state/modules.py +30 -8
  160. opentrons/protocol_engine/state/motion.py +60 -18
  161. opentrons/protocol_engine/state/preconditions.py +59 -0
  162. opentrons/protocol_engine/state/state.py +44 -0
  163. opentrons/protocol_engine/state/state_summary.py +4 -0
  164. opentrons/protocol_engine/state/tasks.py +139 -0
  165. opentrons/protocol_engine/state/tips.py +177 -258
  166. opentrons/protocol_engine/state/update_types.py +26 -9
  167. opentrons/protocol_engine/types/__init__.py +23 -4
  168. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  169. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  170. opentrons/protocol_engine/types/instrument.py +8 -1
  171. opentrons/protocol_engine/types/labware.py +1 -13
  172. opentrons/protocol_engine/types/location.py +26 -2
  173. opentrons/protocol_engine/types/module.py +11 -1
  174. opentrons/protocol_engine/types/tasks.py +38 -0
  175. opentrons/protocol_engine/types/tip.py +9 -0
  176. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  177. opentrons/protocol_runner/protocol_runner.py +14 -1
  178. opentrons/protocol_runner/run_orchestrator.py +49 -2
  179. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  180. opentrons/protocols/api_support/definitions.py +1 -1
  181. opentrons/protocols/api_support/types.py +2 -1
  182. opentrons/simulate.py +51 -15
  183. opentrons/system/camera.py +334 -4
  184. opentrons/system/ffmpeg.py +110 -0
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
  186. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
  187. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
  190. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,83 @@
1
+ """CreateTimer command request, result, and implementation models."""
2
+
3
+ from __future__ import annotations
4
+ from pydantic import BaseModel, Field
5
+ from typing import Optional, Type, TYPE_CHECKING
6
+ from typing_extensions import Literal
7
+
8
+ from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
9
+ from ..errors.error_occurrence import ErrorOccurrence
10
+
11
+ if TYPE_CHECKING:
12
+ from ..execution import TaskHandler, RunControlHandler
13
+
14
+
15
+ CreateTimerCommandType = Literal["createTimer"]
16
+
17
+
18
+ class CreateTimerParams(BaseModel):
19
+ """Payload required to annotate execution with a CreateTimer."""
20
+
21
+ time: float = Field(
22
+ ...,
23
+ description="The time before the timer should elapse in seconds. This is the minimum time before the timer elapses; it may in practice take longer than this.",
24
+ )
25
+ task_id: str | None = Field(
26
+ None,
27
+ description="The id of the timer task",
28
+ )
29
+
30
+
31
+ class CreateTimerResult(BaseModel):
32
+ """Result data from the execution of a CreateTimer command."""
33
+
34
+ task_id: str = Field(..., description="The id of the timer task")
35
+ time: float = Field(..., description="The same time as the parameter.")
36
+
37
+
38
+ class CreateTimerImplementation(
39
+ AbstractCommandImpl[CreateTimerParams, SuccessData[CreateTimerResult]]
40
+ ):
41
+ """CreateTimer command implementation."""
42
+
43
+ def __init__(
44
+ self,
45
+ task_handler: TaskHandler,
46
+ run_control: RunControlHandler,
47
+ **kwargs: object,
48
+ ) -> None:
49
+ self._task_handler = task_handler
50
+ self._run_control = run_control
51
+
52
+ async def execute(
53
+ self, params: CreateTimerParams
54
+ ) -> SuccessData[CreateTimerResult]:
55
+ """No operation taken other than capturing message in command."""
56
+
57
+ async def timer(task_handler: TaskHandler) -> None:
58
+ async with task_handler.synchronize_concurrent("createTimer"):
59
+ await self._run_control.wait_for_duration(params.time)
60
+
61
+ task = await self._task_handler.create_task(timer, params.task_id)
62
+ return SuccessData(
63
+ public=CreateTimerResult(task_id=task.id, time=params.time),
64
+ )
65
+
66
+
67
+ class CreateTimer(BaseCommand[CreateTimerParams, CreateTimerResult, ErrorOccurrence]):
68
+ """CreateTimer command model."""
69
+
70
+ commandType: CreateTimerCommandType = "createTimer"
71
+ params: CreateTimerParams
72
+ result: Optional[CreateTimerResult] = None
73
+
74
+ _ImplementationCls: Type[CreateTimerImplementation] = CreateTimerImplementation
75
+
76
+
77
+ class CreateTimerCreate(BaseCommandCreate[CreateTimerParams]):
78
+ """CreateTimer command request model."""
79
+
80
+ commandType: CreateTimerCommandType = "createTimer"
81
+ params: CreateTimerParams
82
+
83
+ _CommandCls: Type[CreateTimer] = CreateTimer
@@ -101,6 +101,7 @@ class DispenseImplementation(AbstractCommandImpl[DispenseParams, _ExecuteReturn]
101
101
  well_name=well_name,
102
102
  well_location=well_location,
103
103
  operation_volume=volume,
104
+ offset_pipette_for_reservoir_subwells=False,
104
105
  )
105
106
  if isinstance(move_result, DefinedErrorData):
106
107
  return move_result
@@ -19,7 +19,7 @@ from .pipetting_common import (
19
19
  dispense_while_tracking,
20
20
  )
21
21
  from .movement_common import (
22
- LiquidHandlingWellLocationMixin,
22
+ DynamicLiquidHandlingWellLocationMixin,
23
23
  DestinationPositionResult,
24
24
  StallOrCollisionError,
25
25
  move_to_well,
@@ -49,7 +49,7 @@ class DispenseWhileTrackingParams(
49
49
  PipetteIdMixin,
50
50
  DispenseVolumeMixin,
51
51
  FlowRateMixin,
52
- LiquidHandlingWellLocationMixin,
52
+ DynamicLiquidHandlingWellLocationMixin,
53
53
  ):
54
54
  """Payload required to dispense to a specific well."""
55
55
 
@@ -100,14 +100,24 @@ class DispenseWhileTrackingImplementation(
100
100
  # TODO(pbm, 10-15-24): call self._state_view.geometry.validate_dispense_volume_into_well()
101
101
 
102
102
  state_update = StateUpdate()
103
+
104
+ end_point = self._state_view.geometry.get_well_position(
105
+ labware_id=params.labwareId,
106
+ well_name=params.wellName,
107
+ well_location=params.trackToLocation,
108
+ operation_volume=params.volume,
109
+ pipette_id=params.pipetteId,
110
+ )
111
+
103
112
  move_result = await move_to_well(
104
113
  movement=self._movement,
105
114
  model_utils=self._model_utils,
106
115
  pipette_id=params.pipetteId,
107
116
  labware_id=params.labwareId,
108
117
  well_name=params.wellName,
109
- well_location=params.wellLocation,
118
+ well_location=params.trackFromLocation,
110
119
  )
120
+
111
121
  state_update.append(move_result.state_update)
112
122
  if isinstance(move_result, DefinedErrorData):
113
123
  return DefinedErrorData(
@@ -120,6 +130,7 @@ class DispenseWhileTrackingImplementation(
120
130
  well_name=well_name,
121
131
  volume=params.volume,
122
132
  flow_rate=params.flowRate,
133
+ end_point=end_point,
123
134
  push_out=params.pushOut,
124
135
  location_if_error={
125
136
  "retryLocation": (
@@ -130,7 +141,49 @@ class DispenseWhileTrackingImplementation(
130
141
  },
131
142
  pipetting=self._pipetting,
132
143
  model_utils=self._model_utils,
144
+ movement_delay=params.movement_delay,
133
145
  )
146
+
147
+ if isinstance(dispense_result, DefinedErrorData):
148
+ if isinstance(dispense_result.public, OverpressureError):
149
+ return DefinedErrorData(
150
+ public=OverpressureError(
151
+ id=dispense_result.public.id,
152
+ createdAt=dispense_result.public.createdAt,
153
+ wrappedErrors=dispense_result.public.wrappedErrors,
154
+ errorInfo=dispense_result.public.errorInfo,
155
+ ),
156
+ state_update=dispense_result.state_update.set_liquid_operated(
157
+ labware_id=params.labwareId,
158
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
159
+ params.labwareId,
160
+ params.wellName,
161
+ params.pipetteId,
162
+ ),
163
+ volume_added=CLEAR,
164
+ ),
165
+ state_update_if_false_positive=dispense_result.state_update_if_false_positive,
166
+ )
167
+ elif isinstance(dispense_result.public, StallOrCollisionError):
168
+ return DefinedErrorData(
169
+ public=StallOrCollisionError(
170
+ id=dispense_result.public.id,
171
+ createdAt=dispense_result.public.createdAt,
172
+ wrappedErrors=dispense_result.public.wrappedErrors,
173
+ errorInfo=dispense_result.public.errorInfo,
174
+ ),
175
+ state_update=dispense_result.state_update.set_liquid_operated(
176
+ labware_id=params.labwareId,
177
+ well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
178
+ params.labwareId,
179
+ params.wellName,
180
+ params.pipetteId,
181
+ ),
182
+ volume_added=CLEAR,
183
+ ),
184
+ state_update_if_false_positive=dispense_result.state_update_if_false_positive,
185
+ )
186
+
134
187
  position_after_dispense = await self._gantry_mover.get_position(
135
188
  params.pipetteId
136
189
  )
@@ -139,22 +192,6 @@ class DispenseWhileTrackingImplementation(
139
192
  y=position_after_dispense.y,
140
193
  z=position_after_dispense.z,
141
194
  )
142
-
143
- if isinstance(dispense_result, DefinedErrorData):
144
- return DefinedErrorData(
145
- public=dispense_result.public,
146
- state_update=dispense_result.state_update.set_liquid_operated(
147
- labware_id=params.labwareId,
148
- well_names=self._state_view.geometry.get_wells_covered_by_pipette_with_active_well(
149
- params.labwareId,
150
- params.wellName,
151
- params.pipetteId,
152
- ),
153
- volume_added=CLEAR,
154
- ),
155
- state_update_if_false_positive=dispense_result.state_update_if_false_positive,
156
- )
157
-
158
195
  return SuccessData(
159
196
  public=DispenseWhileTrackingResult(
160
197
  volume=dispense_result.public.volume,
@@ -12,7 +12,7 @@ from opentrons.protocol_engine.errors.exceptions import TipAttachedError
12
12
  from opentrons.protocol_engine.resources.model_utils import ModelUtils
13
13
 
14
14
  from ..state.update_types import StateUpdate
15
- from ..types import DropTipWellLocation
15
+ from ..types import DropTipWellLocation, TipRackWellState
16
16
  from .pipetting_common import (
17
17
  PipetteIdMixin,
18
18
  TipPhysicallyAttachedError,
@@ -140,6 +140,25 @@ class DropTipImplementation(AbstractCommandImpl[DropTipParams, _ExecuteReturn]):
140
140
  partially_configured=is_partially_configured,
141
141
  )
142
142
 
143
+ is_tip_rack = self._state_view.labware.get_definition(
144
+ labware_id
145
+ ).parameters.isTiprack
146
+
147
+ # It's possible that we are dropping tips into a labware trash for pre API v2.14 OT-2 protocols
148
+ # (or something else unexpected), so if it is not a tip rack mark no wells as used
149
+ if is_tip_rack:
150
+ tips_to_mark_as_used = (
151
+ self._state_view.tips.compute_tips_to_mark_as_used_or_empty(
152
+ labware_id=labware_id,
153
+ well_name=well_name,
154
+ nozzle_map=self._state_view.pipettes.get_nozzle_configuration(
155
+ pipette_id
156
+ ),
157
+ )
158
+ )
159
+ else:
160
+ tips_to_mark_as_used = []
161
+
143
162
  move_result = await move_to_well(
144
163
  movement=self._movement_handler,
145
164
  model_utils=self._model_utils,
@@ -152,12 +171,7 @@ class DropTipImplementation(AbstractCommandImpl[DropTipParams, _ExecuteReturn]):
152
171
  return move_result
153
172
 
154
173
  scrape_type = TipScrapeType.NONE
155
- if (
156
- params.scrape_tips
157
- and self._state_view.geometry._labware.get_definition(
158
- labware_id
159
- ).parameters.isTiprack
160
- ):
174
+ if params.scrape_tips and is_tip_rack:
161
175
  if int("".join(filter(str.isdigit, well_name))) <= 6:
162
176
  scrape_type = TipScrapeType.RIGHT_ONE_COL
163
177
  else:
@@ -194,6 +208,10 @@ class DropTipImplementation(AbstractCommandImpl[DropTipParams, _ExecuteReturn]):
194
208
  pipette_id=params.pipetteId,
195
209
  tip_geometry=None,
196
210
  tip_source=None,
211
+ ).update_tip_rack_well_state(
212
+ tip_state=TipRackWellState.USED,
213
+ labware_id=labware_id,
214
+ well_names=tips_to_mark_as_used,
197
215
  ),
198
216
  )
199
217
  else:
@@ -201,10 +219,16 @@ class DropTipImplementation(AbstractCommandImpl[DropTipParams, _ExecuteReturn]):
201
219
  public=DropTipResult(position=move_result.public.position),
202
220
  state_update=move_result.state_update.set_fluid_unknown(
203
221
  pipette_id=pipette_id
204
- ).update_pipette_tip_state(
222
+ )
223
+ .update_pipette_tip_state(
205
224
  pipette_id=params.pipetteId,
206
225
  tip_geometry=None,
207
226
  tip_source=None,
227
+ )
228
+ .update_tip_rack_well_state(
229
+ tip_state=TipRackWellState.USED,
230
+ labware_id=labware_id,
231
+ well_names=tips_to_mark_as_used,
208
232
  ),
209
233
  )
210
234
 
@@ -10,6 +10,10 @@ from opentrons_shared_data.errors import ErrorCodes
10
10
  from opentrons_shared_data.errors.exceptions import CommandPreconditionViolated
11
11
  from opentrons_shared_data.labware.labware_definition import LabwareDefinition
12
12
 
13
+ from opentrons.protocol_engine.errors.exceptions import (
14
+ LabwarePoolNotCompatibleWithModuleError,
15
+ )
16
+
13
17
 
14
18
  from ...errors import ErrorOccurrence
15
19
  from ...types import (
@@ -35,6 +39,15 @@ if TYPE_CHECKING:
35
39
  from opentrons.protocol_engine.execution.equipment import LoadedLabwarePoolData
36
40
  from opentrons.protocol_engine.state.module_substates import FlexStackerSubState
37
41
 
42
+
43
+ # The stacker cannot dispense labware where there is no gap between the top surface
44
+ # of the bottom labware being dispensed, and bottom surface of the top labware.
45
+ # This is because the stacker latch, which holds the labware stack, needs enough
46
+ # empty space to free the bottom labware, but still hold the top labware once it
47
+ # closes.
48
+ STACKER_INCOMPATIBLE_LABWARE = set(["opentrons_tough_universal_lid"])
49
+
50
+
38
51
  INITIAL_COUNT_DESCRIPTION = dedent(
39
52
  """\
40
53
  The number of labware that should be initially stored in the stacker. This number will be silently clamped to
@@ -911,3 +924,25 @@ def build_retrieve_labware_move_updates(
911
924
  lid_offset_location,
912
925
  )
913
926
  return locations_for_ids, offset_ids_by_id
927
+
928
+
929
+ def validate_labware_pool_compatible_with_stacker(
930
+ pool_primary_definition: LabwareDefinition,
931
+ pool_adapter_definition: LabwareDefinition | None,
932
+ pool_lid_definition: LabwareDefinition | None,
933
+ ) -> None:
934
+ """Verifies that the given labware pool is compatible with the stacker."""
935
+ labware_pool = set(
936
+ lw.parameters.loadName
937
+ for lw in [
938
+ pool_primary_definition,
939
+ pool_adapter_definition,
940
+ pool_lid_definition,
941
+ ]
942
+ if lw is not None
943
+ )
944
+ incompatible_labware = list(labware_pool & STACKER_INCOMPATIBLE_LABWARE)
945
+ if incompatible_labware:
946
+ raise LabwarePoolNotCompatibleWithModuleError(
947
+ f"The stacker cannot store {incompatible_labware}"
948
+ )
@@ -31,6 +31,7 @@ from .common import (
31
31
  primary_location_sequences,
32
32
  adapter_location_sequences_with_default,
33
33
  lid_location_sequences_with_default,
34
+ validate_labware_pool_compatible_with_stacker,
34
35
  )
35
36
 
36
37
  if TYPE_CHECKING:
@@ -210,6 +211,12 @@ class SetStoredLabwareImpl(
210
211
  )
211
212
  )
212
213
 
214
+ validate_labware_pool_compatible_with_stacker(
215
+ pool_primary_definition=labware_def,
216
+ pool_adapter_definition=adapter_def,
217
+ pool_lid_definition=lid_def,
218
+ )
219
+
213
220
  pool_height = self._state_view.geometry.get_height_of_labware_stack(
214
221
  pool_definitions
215
222
  )
@@ -32,6 +32,14 @@ from .set_and_wait_for_shake_speed import (
32
32
  SetAndWaitForShakeSpeedCommandType,
33
33
  )
34
34
 
35
+ from .set_shake_speed import (
36
+ SetShakeSpeed,
37
+ SetShakeSpeedCreate,
38
+ SetShakeSpeedParams,
39
+ SetShakeSpeedResult,
40
+ SetShakeSpeedCommandType,
41
+ )
42
+
35
43
  from .deactivate_shaker import (
36
44
  DeactivateShaker,
37
45
  DeactivateShakerCreate,
@@ -81,6 +89,12 @@ __all__ = [
81
89
  "SetAndWaitForShakeSpeedParams",
82
90
  "SetAndWaitForShakeSpeedResult",
83
91
  "SetAndWaitForShakeSpeedCommandType",
92
+ # heaterShaker/setShakeSpeed
93
+ "SetShakeSpeed",
94
+ "SetShakeSpeedCreate",
95
+ "SetShakeSpeedParams",
96
+ "SetShakeSpeedResult",
97
+ "SetShakeSpeedCommandType",
84
98
  # heaterShaker/deactivateShaker
85
99
  "DeactivateShaker",
86
100
  "DeactivateShakerCreate",
@@ -0,0 +1,20 @@
1
+ """Common heatershaker base models."""
2
+ from opentrons.protocol_engine.state.module_substates.heater_shaker_module_substate import (
3
+ HeaterShakerModuleSubState,
4
+ )
5
+
6
+
7
+ async def get_heatershaker_ready_to_shake(
8
+ hs_module_substate: HeaterShakerModuleSubState,
9
+ rpm: float,
10
+ ) -> int:
11
+ """Check heatershaker state to confirm if it is ready to shake.
12
+
13
+ This includes
14
+ - Checking latch closure
15
+ - Validating target speed
16
+ """
17
+ hs_module_substate.raise_if_labware_latch_not_closed()
18
+ # Verify speed from hs module view
19
+ validated_speed = hs_module_substate.validate_target_speed(rpm)
20
+ return validated_speed
@@ -8,6 +8,8 @@ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, Succe
8
8
  from ...errors.error_occurrence import ErrorOccurrence
9
9
  from ...state import update_types
10
10
 
11
+ from .common import get_heatershaker_ready_to_shake
12
+
11
13
  if TYPE_CHECKING:
12
14
  from opentrons.protocol_engine.state.state import StateView
13
15
  from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler
@@ -64,10 +66,9 @@ class SetAndWaitForShakeSpeedImpl(
64
66
  module_id=params.moduleId
65
67
  )
66
68
 
67
- hs_module_substate.raise_if_labware_latch_not_closed()
68
-
69
- # Verify speed from hs module view
70
- validated_speed = hs_module_substate.validate_target_speed(params.rpm)
69
+ validated_speed = await get_heatershaker_ready_to_shake(
70
+ hs_module_substate, params.rpm
71
+ )
71
72
 
72
73
  pipette_should_retract = (
73
74
  self._state_view.motion.check_pipette_blocking_hs_shaker(
@@ -0,0 +1,136 @@
1
+ """Command models to set a shake speed for a Heater-Shaker Module."""
2
+ from __future__ import annotations
3
+ from typing import Optional, TYPE_CHECKING
4
+ from typing_extensions import Literal, Type
5
+ from pydantic import BaseModel, Field
6
+
7
+ from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
8
+ from ...errors.error_occurrence import ErrorOccurrence
9
+ from ...state import update_types
10
+ from .common import get_heatershaker_ready_to_shake
11
+
12
+ if TYPE_CHECKING:
13
+ from opentrons.protocol_engine.state.state import StateView
14
+ from opentrons.protocol_engine.execution import (
15
+ EquipmentHandler,
16
+ MovementHandler,
17
+ TaskHandler,
18
+ )
19
+
20
+ SetShakeSpeedCommandType = Literal["heaterShaker/setShakeSpeed"]
21
+
22
+
23
+ class SetShakeSpeedParams(BaseModel):
24
+ """Input parameters to set a shake speed for a Heater-Shaker Module."""
25
+
26
+ moduleId: str = Field(..., description="Unique ID of the Heater-Shaker Module.")
27
+ rpm: float = Field(..., description="Target speed in rotations per minute.")
28
+ taskId: str | None = Field(
29
+ None,
30
+ description="Id for the background task that manages the temperature",
31
+ )
32
+
33
+
34
+ class SetShakeSpeedResult(BaseModel):
35
+ """Result data from setting and waiting for a Heater-Shaker's shake speed."""
36
+
37
+ pipetteRetracted: bool = Field(
38
+ ...,
39
+ description=(
40
+ "Whether this command automatically retracted the pipettes"
41
+ " before starting the shake, to avoid a potential collision."
42
+ ),
43
+ )
44
+ taskId: str = Field(
45
+ description="The task id for the setTargetTemperature task",
46
+ )
47
+
48
+
49
+ class SetShakeSpeedImpl(
50
+ AbstractCommandImpl[SetShakeSpeedParams, SuccessData[SetShakeSpeedResult]]
51
+ ):
52
+ """Execution implementation of Heater-Shaker's set shake speed command."""
53
+
54
+ def __init__(
55
+ self,
56
+ state_view: StateView,
57
+ equipment: EquipmentHandler,
58
+ movement: MovementHandler,
59
+ task_handler: TaskHandler,
60
+ **unused_dependencies: object,
61
+ ) -> None:
62
+ self._state_view = state_view
63
+ self._equipment = equipment
64
+ self._movement = movement
65
+ self._task_handler = task_handler
66
+
67
+ async def execute(
68
+ self,
69
+ params: SetShakeSpeedParams,
70
+ ) -> SuccessData[SetShakeSpeedResult]:
71
+ """Set a Heater-Shaker's target shake speed."""
72
+ state_update = update_types.StateUpdate()
73
+
74
+ # Allow propagation of ModuleNotLoadedError and WrongModuleTypeError.
75
+ hs_module_substate = self._state_view.modules.get_heater_shaker_module_substate(
76
+ module_id=params.moduleId
77
+ )
78
+
79
+ validated_speed = await get_heatershaker_ready_to_shake(
80
+ hs_module_substate, params.rpm
81
+ )
82
+ pipette_should_retract = (
83
+ self._state_view.motion.check_pipette_blocking_hs_shaker(
84
+ hs_module_substate.module_id
85
+ )
86
+ )
87
+ if pipette_should_retract:
88
+ # Move pipette away if it is close to the heater-shaker
89
+ # TODO(jbl 2022-07-28) replace home movement with a retract movement
90
+ await self._movement.home(
91
+ axes=self._state_view.motion.get_robot_mount_axes()
92
+ )
93
+ state_update.clear_all_pipette_locations()
94
+
95
+ # Allow propagation of ModuleNotAttachedError.
96
+ hs_hardware_module = self._equipment.get_module_hardware_api(
97
+ hs_module_substate.module_id
98
+ )
99
+
100
+ async def start_shake(task_handler: TaskHandler) -> None:
101
+ if hs_hardware_module is not None:
102
+ async with task_handler.synchronize_cancel_previous(
103
+ hs_module_substate.module_id + "-shake"
104
+ ):
105
+ await hs_hardware_module.set_speed(rpm=validated_speed)
106
+
107
+ task = await self._task_handler.create_task(
108
+ task_function=start_shake, id=params.taskId
109
+ )
110
+ return SuccessData(
111
+ public=SetShakeSpeedResult(
112
+ pipetteRetracted=pipette_should_retract, taskId=task.id
113
+ ),
114
+ state_update=state_update,
115
+ )
116
+
117
+
118
+ class SetShakeSpeed(
119
+ BaseCommand[SetShakeSpeedParams, SetShakeSpeedResult, ErrorOccurrence]
120
+ ):
121
+ """A command to set a Heater-Shaker's shake speed."""
122
+
123
+ commandType: SetShakeSpeedCommandType = "heaterShaker/setShakeSpeed"
124
+ params: SetShakeSpeedParams
125
+ result: Optional[SetShakeSpeedResult] = None
126
+
127
+ _ImplementationCls: Type[SetShakeSpeedImpl] = SetShakeSpeedImpl
128
+
129
+
130
+ class SetShakeSpeedCreate(BaseCommandCreate[SetShakeSpeedParams]):
131
+ """A request to create a Heater-Shaker's set shake speed command."""
132
+
133
+ commandType: SetShakeSpeedCommandType = "heaterShaker/setShakeSpeed"
134
+ params: SetShakeSpeedParams
135
+
136
+ _CommandCls: Type[SetShakeSpeed] = SetShakeSpeed
@@ -1,31 +1,47 @@
1
1
  """Command models to start heating a Heater-Shaker Module."""
2
2
  from __future__ import annotations
3
- from typing import Optional, TYPE_CHECKING
3
+ from typing import Optional, TYPE_CHECKING, Any
4
4
  from typing_extensions import Literal, Type
5
5
 
6
6
  from pydantic import BaseModel, Field
7
+ from pydantic.json_schema import SkipJsonSchema
7
8
 
8
9
  from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
9
10
  from ...errors.error_occurrence import ErrorOccurrence
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from opentrons.protocol_engine.state.state import StateView
13
- from opentrons.protocol_engine.execution import EquipmentHandler
14
+ from opentrons.protocol_engine.execution import EquipmentHandler, TaskHandler
14
15
 
15
16
 
16
17
  SetTargetTemperatureCommandType = Literal["heaterShaker/setTargetTemperature"]
17
18
 
18
19
 
20
+ def _remove_default(s: dict[str, Any]) -> None:
21
+ s.pop("default", None)
22
+
23
+
19
24
  class SetTargetTemperatureParams(BaseModel):
20
25
  """Input parameters to set a Heater-Shaker's target temperature."""
21
26
 
22
27
  moduleId: str = Field(..., description="Unique ID of the Heater-Shaker Module.")
23
28
  celsius: float = Field(..., description="Target temperature in °C.")
29
+ taskId: str | SkipJsonSchema[None] = Field(
30
+ None,
31
+ description="Id for the background task that manages the temperature",
32
+ json_schema_extra=_remove_default,
33
+ )
24
34
 
25
35
 
26
36
  class SetTargetTemperatureResult(BaseModel):
27
37
  """Result data from setting a Heater-Shaker's target temperature."""
28
38
 
39
+ taskId: str | SkipJsonSchema[None] = Field(
40
+ None,
41
+ description="The task id for the setTargetTemperature task",
42
+ json_schema_extra=_remove_default,
43
+ )
44
+
29
45
 
30
46
  class SetTargetTemperatureImpl(
31
47
  AbstractCommandImpl[
@@ -38,10 +54,12 @@ class SetTargetTemperatureImpl(
38
54
  self,
39
55
  state_view: StateView,
40
56
  equipment: EquipmentHandler,
57
+ task_handler: TaskHandler,
41
58
  **unused_dependencies: object,
42
59
  ) -> None:
43
60
  self._state_view = state_view
44
61
  self._equipment = equipment
62
+ self._task_handler = task_handler
45
63
 
46
64
  async def execute(
47
65
  self,
@@ -61,11 +79,19 @@ class SetTargetTemperatureImpl(
61
79
  hs_module_substate.module_id
62
80
  )
63
81
 
64
- if hs_hardware_module is not None:
65
- await hs_hardware_module.start_set_temperature(validated_temp)
82
+ async def start_set_temperature(task_handler: TaskHandler) -> None:
83
+ if hs_hardware_module is not None:
84
+ async with task_handler.synchronize_cancel_previous(
85
+ hs_module_substate.module_id + "-temp"
86
+ ):
87
+ await hs_hardware_module.start_set_temperature(validated_temp)
88
+ await hs_hardware_module.await_temperature(validated_temp)
66
89
 
90
+ task = await self._task_handler.create_task(
91
+ task_function=start_set_temperature, id=params.taskId
92
+ )
67
93
  return SuccessData(
68
- public=SetTargetTemperatureResult(),
94
+ public=SetTargetTemperatureResult(taskId=task.id),
69
95
  )
70
96
 
71
97