opentrons 8.1.0a0__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.0a0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0a0.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.0a0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -201,6 +201,22 @@ NonStackedLocation = Union[
201
201
  class WellOrigin(str, Enum):
202
202
  """Origin of WellLocation offset.
203
203
 
204
+ Props:
205
+ TOP: the top-center of the well
206
+ BOTTOM: the bottom-center of the well
207
+ CENTER: the middle-center of the well
208
+ MENISCUS: the meniscus-center of the well
209
+ """
210
+
211
+ TOP = "top"
212
+ BOTTOM = "bottom"
213
+ CENTER = "center"
214
+ MENISCUS = "meniscus"
215
+
216
+
217
+ class PickUpTipWellOrigin(str, Enum):
218
+ """The origin of a PickUpTipWellLocation offset.
219
+
204
220
  Props:
205
221
  TOP: the top-center of the well
206
222
  BOTTOM: the bottom-center of the well
@@ -243,6 +259,34 @@ class WellLocation(BaseModel):
243
259
 
244
260
  origin: WellOrigin = WellOrigin.TOP
245
261
  offset: WellOffset = Field(default_factory=WellOffset)
262
+ volumeOffset: float = Field(
263
+ default=0.0,
264
+ description="""A volume of liquid, in µL, to offset the z-axis offset.""",
265
+ )
266
+
267
+
268
+ class LiquidHandlingWellLocation(BaseModel):
269
+ """A relative location in reference to a well's location.
270
+
271
+ To be used with commands that handle liquids.
272
+ """
273
+
274
+ origin: WellOrigin = WellOrigin.TOP
275
+ offset: WellOffset = Field(default_factory=WellOffset)
276
+ volumeOffset: Union[float, Literal["operationVolume"]] = Field(
277
+ default=0.0,
278
+ description="""A volume of liquid, in µL, to offset the z-axis offset. When "operationVolume" is specified, this volume is pulled from the command volume parameter.""",
279
+ )
280
+
281
+
282
+ class PickUpTipWellLocation(BaseModel):
283
+ """A relative location in reference to a well's location.
284
+
285
+ To be used for picking up tips.
286
+ """
287
+
288
+ origin: PickUpTipWellOrigin = PickUpTipWellOrigin.TOP
289
+ offset: WellOffset = Field(default_factory=WellOffset)
246
290
 
247
291
 
248
292
  class DropTipWellLocation(BaseModel):
@@ -311,6 +355,48 @@ class CurrentWell:
311
355
  well_name: str
312
356
 
313
357
 
358
+ class LoadedVolumeInfo(BaseModel):
359
+ """A well's liquid volume, initialized by a LoadLiquid, updated by Aspirate and Dispense."""
360
+
361
+ volume: Optional[float] = None
362
+ last_loaded: datetime
363
+ operations_since_load: int
364
+
365
+
366
+ class ProbedHeightInfo(BaseModel):
367
+ """A well's liquid height, initialized by a LiquidProbe, cleared by Aspirate and Dispense."""
368
+
369
+ height: Optional[float] = None
370
+ last_probed: datetime
371
+
372
+
373
+ class ProbedVolumeInfo(BaseModel):
374
+ """A well's liquid volume, initialized by a LiquidProbe, updated by Aspirate and Dispense."""
375
+
376
+ volume: Optional[float] = None
377
+ last_probed: datetime
378
+ operations_since_probe: int
379
+
380
+
381
+ class WellInfoSummary(BaseModel):
382
+ """Payload for a well's liquid info in StateSummary."""
383
+
384
+ labware_id: str
385
+ well_name: str
386
+ loaded_volume: Optional[float] = None
387
+ probed_height: Optional[float] = None
388
+ probed_volume: Optional[float] = None
389
+
390
+
391
+ @dataclass
392
+ class WellLiquidInfo:
393
+ """Tracked and sensed information about liquid in a well."""
394
+
395
+ probed_height: Optional[ProbedHeightInfo]
396
+ loaded_volume: Optional[LoadedVolumeInfo]
397
+ probed_volume: Optional[ProbedVolumeInfo]
398
+
399
+
314
400
  @dataclass(frozen=True)
315
401
  class CurrentAddressableArea:
316
402
  """The latest addressable area the robot has accessed."""
@@ -797,6 +883,7 @@ class AreaType(Enum):
797
883
  TEMPERATURE = "temperatureModule"
798
884
  MAGNETICBLOCK = "magneticBlock"
799
885
  ABSORBANCE_READER = "absorbanceReader"
886
+ LID_DOCK = "lidDock"
800
887
 
801
888
 
802
889
  @dataclass(frozen=True)
@@ -1071,3 +1158,6 @@ PrimitiveRunTimeParamValuesType = Mapping[
1071
1158
 
1072
1159
  CSVRunTimeParamFilesType = Mapping[StrictStr, StrictStr]
1073
1160
  CSVRuntimeParamPaths = Dict[str, Path]
1161
+
1162
+
1163
+ ABSMeasureMode = Literal["single", "multi"]
@@ -1,5 +1,5 @@
1
1
  """File format validation interface."""
2
-
2
+ from __future__ import annotations
3
3
 
4
4
  from typing import Iterable
5
5
 
@@ -29,6 +29,16 @@ from .protocol_files_invalid_error import ProtocolFilesInvalidError
29
29
  class FileFormatValidationError(ProtocolFilesInvalidError):
30
30
  """Raised when a file does not conform to the format it's supposed to."""
31
31
 
32
+ @classmethod
33
+ def _generic_json_failure(
34
+ cls, info: IdentifiedJsonMain, exc: Exception
35
+ ) -> FileFormatValidationError:
36
+ return cls(
37
+ message=f"{info.original_file.name} could not be read as a JSON protocol.",
38
+ detail={"kind": "bad-json-protocol"},
39
+ wrapping=[PythonException(exc)],
40
+ )
41
+
32
42
 
33
43
  class FileFormatValidator:
34
44
  """File format validation interface."""
@@ -61,22 +71,80 @@ async def _validate_labware_definition(info: IdentifiedLabwareDefinition) -> Non
61
71
  await anyio.to_thread.run_sync(validate_sync)
62
72
 
63
73
 
74
+ def _handle_v8_json_protocol_validation_error(
75
+ info: IdentifiedJsonMain, pve: PydanticValidationError
76
+ ) -> None:
77
+ for error in pve.errors():
78
+ if error["loc"] == ("commandSchemaId",) and error["type"] == "type_error.enum":
79
+ # type_error.enum is for "this entry is not in this enum" and happens if you constrain a field by
80
+ # annotating it with Enum, as we now do for command schema IDs
81
+ raise FileFormatValidationError(
82
+ message=(
83
+ f"{info.original_file.name} could not be read as a JSON protocol, in part because its command schema "
84
+ "id is unknown. This protocol may have been exported from a future version of authorship software. "
85
+ "Updating your Opentrons software may help."
86
+ ),
87
+ detail={
88
+ "kind": "bad-command-schema-id",
89
+ "command-schema-id": info.unvalidated_json.get(
90
+ "commandSchemaId", "<unknown>"
91
+ ),
92
+ },
93
+ wrapping=[PythonException(pve)],
94
+ ) from pve
95
+ if (
96
+ error["loc"] == ("labwareDefinitionSchemaId",)
97
+ and error["type"] == "value_error.const"
98
+ ):
99
+ # value_error.const is for "this entry is not one of these const values", which is different from type_error.enum
100
+ # for I'm sure a very good reason, and happens if you constrain a field by annotating it with a Literal
101
+ raise FileFormatValidationError(
102
+ message=(
103
+ f"{info.original_file.name} could not be read as a JSON protocol, in part because its labware schema "
104
+ "id is unknown. This protocol may have been exported from a future version of authorship software. "
105
+ "Updating your Opentrons software may help."
106
+ ),
107
+ detail={
108
+ "kind": "bad-labware-schema-id",
109
+ "labware-schema-id": info.unvalidated_json.get(
110
+ "labwareDefinitionSchemaId", "<unknown>"
111
+ ),
112
+ },
113
+ )
114
+ if error["loc"] == ("liquidSchemaId",) and error["type"] == "value_error.const":
115
+ raise FileFormatValidationError(
116
+ message=(
117
+ f"{info.original_file.name} could not be read as a JSON protocol, in part because its liquid schema "
118
+ "id is unknown. This protocol may have been exported from a future version of authorship software. "
119
+ "Updating your Opentrons software may help."
120
+ ),
121
+ detail={
122
+ "kind": "bad-liquid-schema-id",
123
+ "liquid-schema-id": info.unvalidated_json.get(
124
+ "liquidSchemaId", "<unknown>"
125
+ ),
126
+ },
127
+ )
128
+ else:
129
+ raise FileFormatValidationError._generic_json_failure(info, pve) from pve
130
+
131
+
64
132
  async def _validate_json_protocol(info: IdentifiedJsonMain) -> None:
65
133
  def validate_sync() -> None:
66
- try:
67
- if info.schema_version == 8:
134
+ if info.schema_version == 8:
135
+ try:
68
136
  JsonProtocolV8.parse_obj(info.unvalidated_json)
69
- elif info.schema_version == 7:
70
- JsonProtocolV7.parse_obj(info.unvalidated_json)
71
- elif info.schema_version == 6:
72
- JsonProtocolV6.parse_obj(info.unvalidated_json)
73
- else:
74
- JsonProtocolUpToV5.parse_obj(info.unvalidated_json)
75
- except PydanticValidationError as e:
76
- raise FileFormatValidationError(
77
- message=f"{info.original_file.name} could not be read as a JSON protocol.",
78
- detail={"kind": "bad-json-protocol"},
79
- wrapping=[PythonException(e)],
80
- ) from e
137
+ except PydanticValidationError as pve:
138
+ _handle_v8_json_protocol_validation_error(info, pve)
139
+ else:
140
+ try:
141
+ if info.schema_version == 7:
142
+ JsonProtocolV7.parse_obj(info.unvalidated_json)
143
+ elif info.schema_version == 6:
144
+ JsonProtocolV6.parse_obj(info.unvalidated_json)
145
+ else:
146
+ JsonProtocolUpToV5.parse_obj(info.unvalidated_json)
147
+ except PydanticValidationError as e:
148
+ raise FileFormatValidationError._generic_json_failure(info, e) from e
81
149
 
82
150
  await anyio.to_thread.run_sync(validate_sync)
@@ -1,6 +1,6 @@
1
1
  """Translation of JSON protocol commands into ProtocolEngine commands."""
2
- from typing import cast, List, Union
3
- from pydantic import parse_obj_as
2
+ from typing import cast, List, Union, Iterator
3
+ from pydantic import parse_obj_as, ValidationError as PydanticValidationError
4
4
 
5
5
  from opentrons_shared_data.pipette.types import PipetteNameType
6
6
  from opentrons_shared_data.protocol.models import (
@@ -12,6 +12,7 @@ from opentrons_shared_data.protocol.models import (
12
12
  protocol_schema_v8,
13
13
  )
14
14
  from opentrons_shared_data import command as command_schema
15
+ from opentrons_shared_data.errors.exceptions import InvalidProtocolData, PythonException
15
16
 
16
17
  from opentrons.types import MountType
17
18
  from opentrons.protocol_engine import (
@@ -196,7 +197,7 @@ class JsonTranslator:
196
197
  """Class that translates commands/liquids from PD/JSON to ProtocolEngine."""
197
198
 
198
199
  def translate_liquids(
199
- self, protocol: Union[ProtocolSchemaV6, ProtocolSchemaV7]
200
+ self, protocol: Union[ProtocolSchemaV6, ProtocolSchemaV7, ProtocolSchemaV8]
200
201
  ) -> List[Liquid]:
201
202
  """Takes json protocol v6 and translates liquids->protocol engine liquids."""
202
203
  protocol_liquids = protocol.liquids or {}
@@ -258,7 +259,8 @@ class JsonTranslator:
258
259
  self, protocol: ProtocolSchemaV8
259
260
  ) -> List[pe_commands.CommandCreate]:
260
261
  """Translate commands in json protocol schema v8, which might be of different command schemas."""
261
- command_schema_ref = protocol.commandSchemaId
262
+ command_schema_ref = protocol.commandSchemaId.value
263
+
262
264
  # these calls will raise if the command schema version is invalid or unknown
263
265
  command_schema_version = command_schema.schema_version_from_ref(
264
266
  command_schema_ref
@@ -267,4 +269,18 @@ class JsonTranslator:
267
269
  command_schema_version
268
270
  )
269
271
 
270
- return [_translate_simple_command(command) for command in protocol.commands]
272
+ def translate_all_commands() -> Iterator[pe_commands.CommandCreate]:
273
+ for command in protocol.commands:
274
+ try:
275
+ yield _translate_simple_command(command)
276
+ except PydanticValidationError as pve:
277
+ raise InvalidProtocolData(
278
+ message=(
279
+ "The protocol is invalid because it contains an unknown or malformed command, "
280
+ f'"{command.commandType}".'
281
+ ),
282
+ detail={"kind": "invalid-command"},
283
+ wrapping=[PythonException(pve)],
284
+ )
285
+
286
+ return list(translate_all_commands())
@@ -34,6 +34,9 @@ from opentrons.protocol_engine.resources import (
34
34
  ModuleDataProvider,
35
35
  pipette_data_provider,
36
36
  )
37
+ from opentrons.protocol_engine.state.update_types import (
38
+ StateUpdate,
39
+ )
37
40
 
38
41
  from opentrons_shared_data.labware.labware_definition import LabwareDefinition
39
42
  from opentrons_shared_data.errors import ErrorCodes, EnumeratedError, PythonException
@@ -267,7 +270,8 @@ class LegacyCommandMapper:
267
270
  )
268
271
  results.append(
269
272
  pe_actions.SucceedCommandAction(
270
- completed_command, private_result=None
273
+ completed_command,
274
+ state_update=StateUpdate(),
271
275
  )
272
276
  )
273
277
 
@@ -672,9 +676,19 @@ class LegacyCommandMapper:
672
676
  # We just set this above, so we know it's not None.
673
677
  started_at=succeeded_command.startedAt, # type: ignore[arg-type]
674
678
  )
679
+ state_update = StateUpdate()
680
+ assert succeeded_command.result is not None
681
+ state_update.set_loaded_labware(
682
+ labware_id=labware_id,
683
+ definition=succeeded_command.result.definition,
684
+ display_name=labware_load_info.labware_display_name,
685
+ offset_id=labware_load_info.offset_id,
686
+ location=location,
687
+ )
688
+
675
689
  succeed_action = pe_actions.SucceedCommandAction(
676
690
  command=succeeded_command,
677
- private_result=None,
691
+ state_update=state_update,
678
692
  )
679
693
 
680
694
  self._command_count["LOAD_LABWARE"] = count + 1
@@ -715,7 +729,14 @@ class LegacyCommandMapper:
715
729
  result=pe_commands.LoadPipetteResult.construct(pipetteId=pipette_id),
716
730
  )
717
731
  serial = instrument_load_info.pipette_dict.get("pipette_id", None) or ""
718
- pipette_config_result = pe_commands.LoadPipettePrivateResult(
732
+ state_update = StateUpdate()
733
+ state_update.set_load_pipette(
734
+ pipette_id=pipette_id,
735
+ mount=succeeded_command.params.mount,
736
+ pipette_name=succeeded_command.params.pipetteName,
737
+ liquid_presence_detection=succeeded_command.params.liquidPresenceDetection,
738
+ )
739
+ state_update.update_pipette_config(
719
740
  pipette_id=pipette_id,
720
741
  serial_number=serial,
721
742
  config=pipette_data_provider.get_pipette_static_config(
@@ -738,9 +759,10 @@ class LegacyCommandMapper:
738
759
  # We just set this above, so we know it's not None.
739
760
  started_at=succeeded_command.startedAt, # type: ignore[arg-type]
740
761
  )
762
+
741
763
  succeed_action = pe_actions.SucceedCommandAction(
742
764
  command=succeeded_command,
743
- private_result=pipette_config_result,
765
+ state_update=state_update,
744
766
  )
745
767
 
746
768
  self._command_count["LOAD_PIPETTE"] = count + 1
@@ -805,8 +827,7 @@ class LegacyCommandMapper:
805
827
  started_at=succeeded_command.startedAt, # type: ignore[arg-type]
806
828
  )
807
829
  succeed_action = pe_actions.SucceedCommandAction(
808
- command=succeeded_command,
809
- private_result=None,
830
+ command=succeeded_command, state_update=StateUpdate()
810
831
  )
811
832
 
812
833
  self._command_count["LOAD_MODULE"] = count + 1
@@ -1,9 +1,9 @@
1
1
  """Customize the ProtocolEngine to monitor and control legacy (APIv2) protocols."""
2
2
  from __future__ import annotations
3
3
 
4
- from asyncio import create_task, Task
4
+ import asyncio
5
5
  from contextlib import ExitStack
6
- from typing import List, Optional
6
+ from typing import Optional
7
7
 
8
8
  from opentrons.legacy_commands.types import CommandMessage as LegacyCommand
9
9
  from opentrons.legacy_broker import LegacyBroker
@@ -12,7 +12,6 @@ from opentrons.protocol_engine import AbstractPlugin, actions as pe_actions
12
12
  from opentrons.util.broker import ReadOnlyBroker
13
13
 
14
14
  from .legacy_command_mapper import LegacyCommandMapper
15
- from .thread_async_queue import ThreadAsyncQueue
16
15
 
17
16
 
18
17
  class LegacyContextPlugin(AbstractPlugin):
@@ -21,59 +20,36 @@ class LegacyContextPlugin(AbstractPlugin):
21
20
  In the legacy ProtocolContext, protocol execution is accomplished
22
21
  by direct communication with the HardwareControlAPI, as opposed to an
23
22
  intermediate layer like the ProtocolEngine. This plugin wraps up
24
- and hides this behavior, so the ProtocolEngine can monitor and control
23
+ and hides this behavior, so the ProtocolEngine can monitor
25
24
  the run of a legacy protocol without affecting the execution of
26
25
  the protocol commands themselves.
27
26
 
28
- This plugin allows a ProtocolEngine to:
29
-
30
- 1. Play/pause the protocol run using the HardwareControlAPI, as was done before
31
- the ProtocolEngine existed.
32
- 2. Subscribe to what is being done with the legacy ProtocolContext,
33
- and insert matching commands into ProtocolEngine state for
34
- purely progress-tracking purposes.
27
+ This plugin allows a ProtocolEngine to subscribe to what is being done with the
28
+ legacy ProtocolContext, and insert matching commands into ProtocolEngine state for
29
+ purely progress-tracking purposes.
35
30
  """
36
31
 
37
32
  def __init__(
38
33
  self,
34
+ engine_loop: asyncio.AbstractEventLoop,
39
35
  broker: LegacyBroker,
40
36
  equipment_broker: ReadOnlyBroker[LoadInfo],
41
37
  legacy_command_mapper: Optional[LegacyCommandMapper] = None,
42
38
  ) -> None:
43
39
  """Initialize the plugin with its dependencies."""
40
+ self._engine_loop = engine_loop
41
+
44
42
  self._broker = broker
45
43
  self._equipment_broker = equipment_broker
46
44
  self._legacy_command_mapper = legacy_command_mapper or LegacyCommandMapper()
47
45
 
48
- # We use a non-blocking queue to communicate activity
49
- # from the APIv2 protocol, which is running in its own thread,
50
- # to the ProtocolEngine, which is running in the main thread's async event loop.
51
- #
52
- # The queue being non-blocking lets the protocol communicate its activity
53
- # instantly *even if the event loop is currently occupied by something else.*
54
- # Various things can accidentally occupy the event loop for too long.
55
- # So if the protocol had to wait for the event loop to be free
56
- # every time it reported some activity,
57
- # it could visibly stall for a moment, making its motion jittery.
58
- #
59
- # TODO(mm, 2024-03-22): See if we can remove this non-blockingness now.
60
- # It was one of several band-aids introduced in ~v5.0.0 to mitigate performance
61
- # problems. v6.3.0 started running some Python protocols directly through
62
- # Protocol Engine, without this plugin, and without any non-blocking queue.
63
- # If performance is sufficient for those, that probably means the
64
- # performance problems have been resolved in better ways elsewhere
65
- # and we don't need this anymore.
66
- self._actions_to_dispatch = ThreadAsyncQueue[List[pe_actions.Action]]()
67
- self._action_dispatching_task: Optional[Task[None]] = None
68
-
69
46
  self._subscription_exit_stack: Optional[ExitStack] = None
70
47
 
71
48
  def setup(self) -> None:
72
49
  """Set up the plugin.
73
50
 
74
- * Subscribe to the APIv2 context's message brokers to be informed
75
- of the APIv2 protocol's activity.
76
- * Kick off a background task to inform Protocol Engine of that activity.
51
+ Subscribe to the APIv2 context's message brokers to be informed
52
+ of the APIv2 protocol's activity.
77
53
  """
78
54
  # Subscribe to activity on the APIv2 context,
79
55
  # and arrange to unsubscribe when this plugin is torn down.
@@ -97,24 +73,16 @@ class LegacyContextPlugin(AbstractPlugin):
97
73
  # to clean up these subscriptions.
98
74
  self._subscription_exit_stack = exit_stack.pop_all()
99
75
 
100
- # Kick off a background task to report activity to the ProtocolEngine.
101
- self._action_dispatching_task = create_task(self._dispatch_all_actions())
102
-
76
+ # todo(mm, 2024-08-21): This no longer needs to be async.
103
77
  async def teardown(self) -> None:
104
78
  """Tear down the plugin, undoing the work done in `setup()`.
105
79
 
106
80
  Called by Protocol Engine.
107
81
  At this point, the APIv2 protocol script must have exited.
108
82
  """
109
- self._actions_to_dispatch.done_putting()
110
- try:
111
- if self._action_dispatching_task is not None:
112
- await self._action_dispatching_task
113
- self._action_dispatching_task = None
114
- finally:
115
- if self._subscription_exit_stack is not None:
116
- self._subscription_exit_stack.close()
117
- self._subscription_exit_stack = None
83
+ if self._subscription_exit_stack is not None:
84
+ self._subscription_exit_stack.close()
85
+ self._subscription_exit_stack = None
118
86
 
119
87
  def handle_action(self, action: pe_actions.Action) -> None:
120
88
  """React to a ProtocolEngine action."""
@@ -127,7 +95,10 @@ class LegacyContextPlugin(AbstractPlugin):
127
95
  Used as a broker callback, so this will run in the APIv2 protocol's thread.
128
96
  """
129
97
  pe_actions = self._legacy_command_mapper.map_command(command=command)
130
- self._actions_to_dispatch.put(pe_actions)
98
+ future = asyncio.run_coroutine_threadsafe(
99
+ self._dispatch_action_list(pe_actions), self._engine_loop
100
+ )
101
+ future.result()
131
102
 
132
103
  def _handle_equipment_loaded(self, load_info: LoadInfo) -> None:
133
104
  """Handle an equipment load reported by the legacy APIv2 protocol.
@@ -135,26 +106,11 @@ class LegacyContextPlugin(AbstractPlugin):
135
106
  Used as a broker callback, so this will run in the APIv2 protocol's thread.
136
107
  """
137
108
  pe_actions = self._legacy_command_mapper.map_equipment_load(load_info=load_info)
138
- self._actions_to_dispatch.put(pe_actions)
139
-
140
- async def _dispatch_all_actions(self) -> None:
141
- """Dispatch all actions to the `ProtocolEngine`.
142
-
143
- Exits only when `self._actions_to_dispatch` is closed
144
- (or an unexpected exception is raised).
145
- """
146
- async for action_batch in self._actions_to_dispatch.get_async_until_closed():
147
- # It's critical that we dispatch this batch of actions as one atomic
148
- # sequence, without yielding to the event loop.
149
- # Although this plugin only means to use the ProtocolEngine as a way of
150
- # passively exposing the protocol's progress, the ProtocolEngine is still
151
- # theoretically active, which means it's constantly watching in the
152
- # background to execute any commands that it finds `queued`.
153
- #
154
- # For example, one of these action batches will often want to
155
- # instantaneously create a running command by having a queue action
156
- # immediately followed by a run action. We cannot let the
157
- # ProtocolEngine's background task see the command in the `queued` state,
158
- # or it will try to execute it, which the legacy protocol is already doing.
159
- for action in action_batch:
160
- self.dispatch(action)
109
+ future = asyncio.run_coroutine_threadsafe(
110
+ self._dispatch_action_list(pe_actions), self._engine_loop
111
+ )
112
+ future.result()
113
+
114
+ async def _dispatch_action_list(self, actions: list[pe_actions.Action]) -> None:
115
+ for action in actions:
116
+ self.dispatch(action)
@@ -1,4 +1,5 @@
1
1
  """Protocol run control and management."""
2
+ import asyncio
2
3
  from typing import List, NamedTuple, Optional, Union
3
4
 
4
5
  from abc import ABC, abstractmethod
@@ -122,9 +123,9 @@ class AbstractRunner(ABC):
122
123
  post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE,
123
124
  )
124
125
 
125
- def resume_from_recovery(self) -> None:
126
+ def resume_from_recovery(self, reconcile_false_positive: bool) -> None:
126
127
  """See `ProtocolEngine.resume_from_recovery()`."""
127
- self._protocol_engine.resume_from_recovery()
128
+ self._protocol_engine.resume_from_recovery(reconcile_false_positive)
128
129
 
129
130
  @abstractmethod
130
131
  async def run(
@@ -220,7 +221,9 @@ class PythonAndLegacyRunner(AbstractRunner):
220
221
  equipment_broker = Broker[LoadInfo]()
221
222
  self._protocol_engine.add_plugin(
222
223
  LegacyContextPlugin(
223
- broker=self._broker, equipment_broker=equipment_broker
224
+ engine_loop=asyncio.get_running_loop(),
225
+ broker=self._broker,
226
+ equipment_broker=equipment_broker,
224
227
  )
225
228
  )
226
229
  self._hardware_api.should_taskify_movement_execution(taskify=True)
@@ -14,6 +14,7 @@ from opentrons_shared_data.robot.types import RobotType
14
14
  from . import protocol_runner, RunResult, JsonRunner, PythonAndLegacyRunner
15
15
  from ..hardware_control import HardwareControlAPI
16
16
  from ..hardware_control.modules import AbstractModule as HardwareModuleAPI
17
+ from ..hardware_control.nozzle_manager import NozzleMap
17
18
  from ..protocol_engine import (
18
19
  ProtocolEngine,
19
20
  CommandCreate,
@@ -204,9 +205,9 @@ class RunOrchestrator:
204
205
  post_run_hardware_state=PostRunHardwareState.STAY_ENGAGED_IN_PLACE,
205
206
  )
206
207
 
207
- def resume_from_recovery(self) -> None:
208
+ def resume_from_recovery(self, reconcile_false_positive: bool) -> None:
208
209
  """Resume the run from recovery."""
209
- self._protocol_engine.resume_from_recovery()
210
+ self._protocol_engine.resume_from_recovery(reconcile_false_positive)
210
211
 
211
212
  async def finish(
212
213
  self,
@@ -256,19 +257,34 @@ class RunOrchestrator:
256
257
  """Get the "current" command, if any."""
257
258
  return self._protocol_engine.state_view.commands.get_current()
258
259
 
260
+ def get_most_recently_finalized_command(self) -> Optional[CommandPointer]:
261
+ """Get the most recently finalized command, if any."""
262
+ most_recently_finalized_command = (
263
+ self._protocol_engine.state_view.commands.get_most_recently_finalized_command()
264
+ )
265
+ return (
266
+ CommandPointer(
267
+ command_id=most_recently_finalized_command.command.id,
268
+ command_key=most_recently_finalized_command.command.key,
269
+ created_at=most_recently_finalized_command.command.createdAt,
270
+ index=most_recently_finalized_command.index,
271
+ )
272
+ if most_recently_finalized_command
273
+ else None
274
+ )
275
+
259
276
  def get_command_slice(
260
- self,
261
- cursor: Optional[int],
262
- length: int,
277
+ self, cursor: Optional[int], length: int, include_fixit_commands: bool
263
278
  ) -> CommandSlice:
264
279
  """Get a slice of run commands.
265
280
 
266
281
  Args:
267
282
  cursor: Requested index of first command in the returned slice.
268
283
  length: Length of slice to return.
284
+ include_fixit_commands: Get all command intents.
269
285
  """
270
286
  return self._protocol_engine.state_view.commands.get_slice(
271
- cursor=cursor, length=length
287
+ cursor=cursor, length=length, include_fixit_commands=include_fixit_commands
272
288
  )
273
289
 
274
290
  def get_command_error_slice(
@@ -398,6 +414,25 @@ class RunOrchestrator:
398
414
  """Get engine deck type."""
399
415
  return self._protocol_engine.state_view.config.deck_type
400
416
 
417
+ def get_nozzle_maps(self) -> Dict[str, NozzleMap]:
418
+ """Get current nozzle maps keyed by pipette id."""
419
+ return self._protocol_engine.state_view.tips.get_pipette_nozzle_maps()
420
+
421
+ def get_tip_attached(self) -> Dict[str, bool]:
422
+ """Get current tip state keyed by pipette id."""
423
+
424
+ def has_tip_attached(pipette_id: str) -> bool:
425
+ return (
426
+ self._protocol_engine.state_view.pipettes.get_attached_tip(pipette_id)
427
+ is not None
428
+ )
429
+
430
+ pipette_ids = (
431
+ pipette.id
432
+ for pipette in self._protocol_engine.state_view.pipettes.get_all()
433
+ )
434
+ return {pipette_id: has_tip_attached(pipette_id) for pipette_id in pipette_ids}
435
+
401
436
  def set_error_recovery_policy(self, policy: ErrorRecoveryPolicy) -> None:
402
437
  """Create error recovery policy for the run."""
403
438
  self._protocol_engine.set_error_recovery_policy(policy)
@@ -1,15 +1,13 @@
1
- import typing
1
+ from typing import Any, Dict, Tuple
2
2
 
3
3
  from opentrons.protocols.advanced_control.transfers import MixStrategy, Mix
4
4
 
5
5
 
6
- def mix_from_kwargs(
7
- top_kwargs: typing.Dict[str, typing.Any]
8
- ) -> typing.Tuple[MixStrategy, Mix]:
6
+ def mix_from_kwargs(top_kwargs: Dict[str, Any]) -> Tuple[MixStrategy, Mix]:
9
7
  """A utility function to determine mix strategy from key word arguments
10
8
  to InstrumentContext.mix"""
11
9
 
12
- def _mix_requested(kwargs, opt):
10
+ def _mix_requested(kwargs: Dict[str, Any], opt: str) -> bool:
13
11
  """
14
12
  Helper for determining mix options from :py:meth:`transfer` kwargs
15
13
  Mixes can be ignored in kwargs by either