opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a7__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 (189) 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 +85 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +6 -2
  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/protocol.py +51 -2
  65. opentrons/protocol_api/core/engine/stringify.py +2 -0
  66. opentrons/protocol_api/core/engine/tasks.py +48 -0
  67. opentrons/protocol_api/core/engine/well.py +8 -0
  68. opentrons/protocol_api/core/instrument.py +19 -2
  69. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  71. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  72. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  73. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  74. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  75. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  76. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  77. opentrons/protocol_api/core/module.py +58 -2
  78. opentrons/protocol_api/core/protocol.py +23 -2
  79. opentrons/protocol_api/core/tasks.py +31 -0
  80. opentrons/protocol_api/core/well.py +4 -0
  81. opentrons/protocol_api/instrument_context.py +388 -2
  82. opentrons/protocol_api/labware.py +10 -2
  83. opentrons/protocol_api/module_contexts.py +170 -6
  84. opentrons/protocol_api/protocol_context.py +87 -21
  85. opentrons/protocol_api/robot_context.py +41 -25
  86. opentrons/protocol_api/tasks.py +48 -0
  87. opentrons/protocol_api/validation.py +49 -3
  88. opentrons/protocol_engine/__init__.py +4 -0
  89. opentrons/protocol_engine/actions/__init__.py +6 -2
  90. opentrons/protocol_engine/actions/actions.py +31 -9
  91. opentrons/protocol_engine/clients/sync_client.py +42 -7
  92. opentrons/protocol_engine/commands/__init__.py +56 -0
  93. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  94. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  96. opentrons/protocol_engine/commands/aspirate.py +1 -0
  97. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  98. opentrons/protocol_engine/commands/capture_image.py +302 -0
  99. opentrons/protocol_engine/commands/command.py +2 -0
  100. opentrons/protocol_engine/commands/command_unions.py +62 -0
  101. opentrons/protocol_engine/commands/create_timer.py +83 -0
  102. opentrons/protocol_engine/commands/dispense.py +1 -0
  103. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  104. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  105. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  106. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  107. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  108. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  109. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  110. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  111. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  112. opentrons/protocol_engine/commands/move_labware.py +3 -4
  113. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  114. opentrons/protocol_engine/commands/movement_common.py +31 -2
  115. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  116. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  117. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  118. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  119. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  120. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  122. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  123. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  124. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  125. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  126. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  127. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  128. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  129. opentrons/protocol_engine/engine_support.py +3 -0
  130. opentrons/protocol_engine/errors/__init__.py +12 -0
  131. opentrons/protocol_engine/errors/exceptions.py +119 -0
  132. opentrons/protocol_engine/execution/__init__.py +4 -0
  133. opentrons/protocol_engine/execution/command_executor.py +62 -1
  134. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  135. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  136. opentrons/protocol_engine/execution/movement.py +2 -0
  137. opentrons/protocol_engine/execution/pipetting.py +19 -25
  138. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  139. opentrons/protocol_engine/execution/run_control.py +8 -0
  140. opentrons/protocol_engine/execution/task_handler.py +157 -0
  141. opentrons/protocol_engine/protocol_engine.py +137 -36
  142. opentrons/protocol_engine/resources/__init__.py +4 -0
  143. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  144. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  145. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  146. opentrons/protocol_engine/resources/file_provider.py +133 -58
  147. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  148. opentrons/protocol_engine/slot_standardization.py +2 -0
  149. opentrons/protocol_engine/state/_well_math.py +60 -18
  150. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  151. opentrons/protocol_engine/state/camera.py +54 -0
  152. opentrons/protocol_engine/state/commands.py +37 -14
  153. opentrons/protocol_engine/state/geometry.py +276 -379
  154. opentrons/protocol_engine/state/labware.py +62 -108
  155. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  156. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  157. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  158. opentrons/protocol_engine/state/modules.py +30 -8
  159. opentrons/protocol_engine/state/motion.py +44 -0
  160. opentrons/protocol_engine/state/preconditions.py +59 -0
  161. opentrons/protocol_engine/state/state.py +44 -0
  162. opentrons/protocol_engine/state/state_summary.py +4 -0
  163. opentrons/protocol_engine/state/tasks.py +139 -0
  164. opentrons/protocol_engine/state/tips.py +177 -258
  165. opentrons/protocol_engine/state/update_types.py +26 -9
  166. opentrons/protocol_engine/types/__init__.py +23 -4
  167. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  168. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  169. opentrons/protocol_engine/types/instrument.py +8 -1
  170. opentrons/protocol_engine/types/labware.py +1 -13
  171. opentrons/protocol_engine/types/location.py +26 -2
  172. opentrons/protocol_engine/types/module.py +11 -1
  173. opentrons/protocol_engine/types/tasks.py +38 -0
  174. opentrons/protocol_engine/types/tip.py +9 -0
  175. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  176. opentrons/protocol_runner/protocol_runner.py +14 -1
  177. opentrons/protocol_runner/run_orchestrator.py +49 -2
  178. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  179. opentrons/protocols/api_support/definitions.py +1 -1
  180. opentrons/protocols/api_support/types.py +2 -1
  181. opentrons/simulate.py +51 -15
  182. opentrons/system/camera.py +334 -4
  183. opentrons/system/ffmpeg.py +110 -0
  184. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +188 -160
  186. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  187. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
@@ -2,19 +2,21 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from typing import Optional, Mapping
5
+ from typing import Optional, Mapping, Callable
6
6
  from typing_extensions import Final
7
7
 
8
8
  from opentrons.drivers.rpi_drivers.types import USBPort
9
9
  from opentrons.drivers.heater_shaker.driver import HeaterShakerDriver
10
10
  from opentrons.drivers.heater_shaker.abstract import AbstractHeaterShakerDriver
11
11
  from opentrons.drivers.heater_shaker.simulator import SimulatingDriver
12
+ from opentrons.drivers.asyncio.communication.errors import UnhandledGcode
12
13
  from opentrons.drivers.types import Temperature, RPM, HeaterShakerLabwareLatchStatus
13
14
  from opentrons.hardware_control.execution_manager import ExecutionManager
14
15
  from opentrons.hardware_control.poller import Reader, Poller
15
16
  from opentrons.hardware_control.modules import mod_abc, update
16
17
  from opentrons.hardware_control.modules.types import (
17
18
  ModuleDisconnectedCallback,
19
+ ModuleErrorCallback,
18
20
  ModuleType,
19
21
  TemperatureStatus,
20
22
  SpeedStatus,
@@ -47,12 +49,13 @@ class HeaterShaker(mod_abc.AbstractModule):
47
49
  port: str,
48
50
  usb_port: USBPort,
49
51
  hw_control_loop: asyncio.AbstractEventLoop,
50
- execution_manager: Optional[ExecutionManager] = None,
52
+ execution_manager: ExecutionManager,
53
+ disconnected_callback: ModuleDisconnectedCallback,
54
+ error_callback: ModuleErrorCallback,
51
55
  poll_interval_seconds: Optional[float] = None,
52
56
  simulating: bool = False,
53
57
  sim_model: Optional[str] = None,
54
58
  sim_serial_number: Optional[str] = None,
55
- disconnected_callback: ModuleDisconnectedCallback = None,
56
59
  ) -> "HeaterShaker":
57
60
  """
58
61
  Build a HeaterShaker
@@ -67,6 +70,7 @@ class HeaterShaker(mod_abc.AbstractModule):
67
70
  loop: Loop
68
71
  sim_model: The model name used by simulator
69
72
  disconnected_callback: Callback to inform the module controller that the device was disconnected
73
+ error_callback: Callback to inform the module controller of an asynchronous error
70
74
 
71
75
  Returns:
72
76
  HeaterShaker instance
@@ -91,6 +95,7 @@ class HeaterShaker(mod_abc.AbstractModule):
91
95
  hw_control_loop=hw_control_loop,
92
96
  execution_manager=execution_manager,
93
97
  disconnected_callback=disconnected_callback,
98
+ error_callback=error_callback,
94
99
  )
95
100
 
96
101
  try:
@@ -109,8 +114,9 @@ class HeaterShaker(mod_abc.AbstractModule):
109
114
  poller: Poller,
110
115
  device_info: Mapping[str, str],
111
116
  hw_control_loop: asyncio.AbstractEventLoop,
112
- execution_manager: Optional[ExecutionManager] = None,
113
- disconnected_callback: ModuleDisconnectedCallback = None,
117
+ execution_manager: ExecutionManager,
118
+ disconnected_callback: ModuleDisconnectedCallback,
119
+ error_callback: ModuleErrorCallback,
114
120
  ):
115
121
  super().__init__(
116
122
  port=port,
@@ -118,14 +124,22 @@ class HeaterShaker(mod_abc.AbstractModule):
118
124
  hw_control_loop=hw_control_loop,
119
125
  execution_manager=execution_manager,
120
126
  disconnected_callback=disconnected_callback,
127
+ error_callback=error_callback,
121
128
  )
122
129
  self._device_info = device_info
123
130
  self._driver = driver
124
131
  self._reader = reader
125
132
  self._poller = poller
133
+ self._unsubscribe_reader = self._reader.register_error_handler(
134
+ self._handle_error
135
+ )
136
+
137
+ def _handle_error(self, error: Exception) -> None:
138
+ self.error_callback(error)
126
139
 
127
140
  async def cleanup(self) -> None:
128
141
  """Stop the poller task"""
142
+ self._unsubscribe_reader()
129
143
  await self._poller.stop()
130
144
  await self._driver.disconnect()
131
145
 
@@ -397,11 +411,22 @@ class HeaterShakerReader(Reader):
397
411
  self.labware_latch = HeaterShakerLabwareLatchStatus.IDLE_UNKNOWN
398
412
  self.error: Optional[str] = None
399
413
  self._driver = driver
414
+ self._handle_error: Callable[[Exception], None] | None = None
415
+
416
+ def register_error_handler(
417
+ self, handle_error: Callable[[Exception], None]
418
+ ) -> Callable[[], None]:
419
+ self._handle_error = handle_error
420
+ return self._unsubscribe_error_handler
421
+
422
+ def _unsubscribe_error_handler(self) -> None:
423
+ self._handle_error = None
400
424
 
401
425
  async def read(self) -> None:
402
426
  await self.read_temperature()
403
427
  await self.read_rpm()
404
428
  await self.read_labware_latch()
429
+ await self._read_errors()
405
430
  self._set_error(None)
406
431
 
407
432
  def on_error(self, exception: Exception) -> None:
@@ -420,7 +445,19 @@ class HeaterShakerReader(Reader):
420
445
  if exception is None:
421
446
  self.error = None
422
447
  else:
448
+ if self._handle_error:
449
+ self._handle_error(exception)
423
450
  try:
424
451
  self.error = str(exception.args[0])
425
452
  except Exception:
426
453
  self.error = repr(exception)
454
+
455
+ async def _read_errors(self) -> None:
456
+ try:
457
+ await self._driver.get_error_state()
458
+ except UnhandledGcode:
459
+ # This device's firmware cannot accept this command, because it
460
+ # hasn't been updated or because it's a gen1. Ignore the result.
461
+ pass
462
+ # If the error is one we should let pass, raise it so the top level
463
+ # error handler can take it.
@@ -49,12 +49,13 @@ class MagDeck(mod_abc.AbstractModule):
49
49
  port: str,
50
50
  usb_port: USBPort,
51
51
  hw_control_loop: asyncio.AbstractEventLoop,
52
- execution_manager: Optional[ExecutionManager] = None,
52
+ execution_manager: ExecutionManager,
53
+ disconnected_callback: types.ModuleDisconnectedCallback,
54
+ error_callback: types.ModuleErrorCallback,
53
55
  poll_interval_seconds: Optional[float] = None,
54
56
  simulating: bool = False,
55
57
  sim_model: Optional[str] = None,
56
58
  sim_serial_number: Optional[str] = None,
57
- disconnected_callback: types.ModuleDisconnectedCallback = None,
58
59
  ) -> "MagDeck":
59
60
  """Factory function."""
60
61
  driver: AbstractMagDeckDriver
@@ -73,6 +74,7 @@ class MagDeck(mod_abc.AbstractModule):
73
74
  device_info=await driver.get_device_info(),
74
75
  driver=driver,
75
76
  disconnected_callback=disconnected_callback,
77
+ error_callback=error_callback,
76
78
  )
77
79
  return mod
78
80
 
@@ -83,8 +85,9 @@ class MagDeck(mod_abc.AbstractModule):
83
85
  hw_control_loop: asyncio.AbstractEventLoop,
84
86
  driver: AbstractMagDeckDriver,
85
87
  device_info: Dict[str, str],
86
- execution_manager: Optional[ExecutionManager] = None,
87
- disconnected_callback: types.ModuleDisconnectedCallback = None,
88
+ execution_manager: ExecutionManager,
89
+ disconnected_callback: types.ModuleDisconnectedCallback,
90
+ error_callback: types.ModuleErrorCallback,
88
91
  ) -> None:
89
92
  """Constructor"""
90
93
  super().__init__(
@@ -93,6 +96,7 @@ class MagDeck(mod_abc.AbstractModule):
93
96
  hw_control_loop=hw_control_loop,
94
97
  execution_manager=execution_manager,
95
98
  disconnected_callback=disconnected_callback,
99
+ error_callback=error_callback,
96
100
  )
97
101
  self._device_info = device_info
98
102
  self._driver = driver
@@ -11,6 +11,7 @@ from ..execution_manager import ExecutionManager
11
11
  from .types import (
12
12
  BundledFirmware,
13
13
  ModuleDisconnectedCallback,
14
+ ModuleErrorCallback,
14
15
  UploadFunction,
15
16
  LiveData,
16
17
  ModuleType,
@@ -47,12 +48,13 @@ class AbstractModule(abc.ABC):
47
48
  port: str,
48
49
  usb_port: USBPort,
49
50
  hw_control_loop: asyncio.AbstractEventLoop,
50
- execution_manager: Optional[ExecutionManager] = None,
51
- poll_interval_seconds: Optional[float] = None,
51
+ execution_manager: ExecutionManager,
52
+ disconnected_callback: ModuleDisconnectedCallback,
53
+ error_callback: ModuleErrorCallback,
54
+ poll_interval_seconds: float | None = None,
52
55
  simulating: bool = False,
53
56
  sim_model: Optional[str] = None,
54
57
  sim_serial_number: Optional[str] = None,
55
- disconnected_callback: ModuleDisconnectedCallback = None,
56
58
  ) -> "AbstractModule":
57
59
  """Modules should always be created using this factory.
58
60
 
@@ -65,8 +67,9 @@ class AbstractModule(abc.ABC):
65
67
  port: str,
66
68
  usb_port: USBPort,
67
69
  hw_control_loop: asyncio.AbstractEventLoop,
68
- execution_manager: Optional[ExecutionManager] = None,
69
- disconnected_callback: ModuleDisconnectedCallback = None,
70
+ execution_manager: ExecutionManager,
71
+ disconnected_callback: ModuleDisconnectedCallback,
72
+ error_callback: ModuleErrorCallback,
70
73
  ) -> None:
71
74
  self._port = port
72
75
  self._usb_port = usb_port
@@ -75,6 +78,7 @@ class AbstractModule(abc.ABC):
75
78
  self._bundled_fw: Optional[BundledFirmware] = self.get_bundled_fw()
76
79
  self._disconnected_callback = disconnected_callback
77
80
  self._updating = False
81
+ self._error_callback = error_callback
78
82
 
79
83
  @staticmethod
80
84
  def sort_key(inst: "AbstractModule") -> int:
@@ -101,7 +105,11 @@ class AbstractModule(abc.ABC):
101
105
  def disconnected_callback(self) -> None:
102
106
  """Called from within the module object to signify the object is no longer connected"""
103
107
  if self._disconnected_callback is not None:
104
- self._disconnected_callback(self.port, self.serial_number)
108
+ self._disconnected_callback(self.model(), self.port, self.serial_number)
109
+
110
+ def error_callback(self, exc: Exception) -> None:
111
+ """Called from within the module object when an asynchronous hardware error occurrs."""
112
+ self._error_callback(exc, self.model(), self.port, self.serial_number)
105
113
 
106
114
  def get_bundled_fw(self) -> Optional[BundledFirmware]:
107
115
  """Get absolute path to bundled version of module fw if available."""
@@ -2,10 +2,11 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from typing import Dict, Optional
5
+ from typing import Dict, Optional, Callable
6
6
 
7
7
  from opentrons.hardware_control.modules.types import (
8
8
  ModuleDisconnectedCallback,
9
+ ModuleErrorCallback,
9
10
  TemperatureStatus,
10
11
  )
11
12
  from opentrons.hardware_control.poller import Reader, Poller
@@ -38,12 +39,13 @@ class TempDeck(mod_abc.AbstractModule):
38
39
  port: str,
39
40
  usb_port: USBPort,
40
41
  hw_control_loop: asyncio.AbstractEventLoop,
41
- execution_manager: Optional[ExecutionManager] = None,
42
+ execution_manager: ExecutionManager,
43
+ disconnected_callback: ModuleDisconnectedCallback,
44
+ error_callback: ModuleErrorCallback,
42
45
  poll_interval_seconds: Optional[float] = None,
43
46
  simulating: bool = False,
44
47
  sim_model: Optional[str] = None,
45
48
  sim_serial_number: Optional[str] = None,
46
- disconnected_callback: ModuleDisconnectedCallback = None,
47
49
  ) -> "TempDeck":
48
50
  """
49
51
  Build a TempDeck
@@ -83,6 +85,7 @@ class TempDeck(mod_abc.AbstractModule):
83
85
  device_info=await driver.get_device_info(),
84
86
  hw_control_loop=hw_control_loop,
85
87
  disconnected_callback=disconnected_callback,
88
+ error_callback=error_callback,
86
89
  )
87
90
 
88
91
  try:
@@ -101,8 +104,9 @@ class TempDeck(mod_abc.AbstractModule):
101
104
  poller: Poller,
102
105
  device_info: Dict[str, str],
103
106
  hw_control_loop: asyncio.AbstractEventLoop,
104
- execution_manager: Optional[ExecutionManager] = None,
105
- disconnected_callback: ModuleDisconnectedCallback = None,
107
+ execution_manager: ExecutionManager,
108
+ disconnected_callback: ModuleDisconnectedCallback,
109
+ error_callback: ModuleErrorCallback,
106
110
  ) -> None:
107
111
  """Constructor"""
108
112
  super().__init__(
@@ -111,11 +115,13 @@ class TempDeck(mod_abc.AbstractModule):
111
115
  hw_control_loop=hw_control_loop,
112
116
  execution_manager=execution_manager,
113
117
  disconnected_callback=disconnected_callback,
118
+ error_callback=error_callback,
114
119
  )
115
120
  self._device_info = device_info
116
121
  self._driver = driver
117
122
  self._reader = reader
118
123
  self._poller = poller
124
+ self._reader.set_error_callback(self.error_callback)
119
125
 
120
126
  async def cleanup(self) -> None:
121
127
  """Stop the poller task."""
@@ -293,7 +299,21 @@ class TempDeckReader(Reader):
293
299
  def __init__(self, driver: AbstractTempDeckDriver) -> None:
294
300
  self.temperature = Temperature(current=25, target=None)
295
301
  self._driver = driver
302
+ self._error_callback: Optional[Callable[[Exception], None]] = None
296
303
 
297
304
  async def read(self) -> None:
298
305
  """Read the module's current and target temperatures."""
299
306
  self.temperature = await self._driver.get_temperature()
307
+
308
+ def set_error_callback(
309
+ self, error_callback: Callable[[Exception], None]
310
+ ) -> Callable[[], None]:
311
+ self._error_callback = error_callback
312
+ return self._remove_error_callback
313
+
314
+ def _remove_error_callback(self) -> None:
315
+ self._error_callback = None
316
+
317
+ def on_error(self, exception: Exception) -> None:
318
+ if self._error_callback:
319
+ self._error_callback(exception)
@@ -9,6 +9,7 @@ from opentrons.hardware_control.modules.lid_temp_status import LidTemperatureSta
9
9
  from opentrons.hardware_control.modules.plate_temp_status import PlateTemperatureStatus
10
10
  from opentrons.hardware_control.modules.types import (
11
11
  ModuleDisconnectedCallback,
12
+ ModuleErrorCallback,
12
13
  TemperatureStatus,
13
14
  )
14
15
  from opentrons.hardware_control.poller import Reader, Poller
@@ -21,6 +22,7 @@ from opentrons.drivers.thermocycler import (
21
22
  ThermocyclerDriverV2,
22
23
  ThermocyclerDriverFactory,
23
24
  )
25
+ from opentrons.drivers.asyncio.communication.errors import UnhandledGcode
24
26
 
25
27
 
26
28
  log = logging.getLogger(__name__)
@@ -36,6 +38,8 @@ DFU_PID = "df11"
36
38
  _TC_PLATE_LIFT_OPEN_DEGREES = 20
37
39
  _TC_PLATE_LIFT_RETURN_DEGREES = 23
38
40
 
41
+ _TC_RAMP_RATE_ADDED_VERSION = (1, 0, 8) # v1.0.8
42
+
39
43
 
40
44
  class ThermocyclerError(Exception):
41
45
  pass
@@ -62,12 +66,13 @@ class Thermocycler(mod_abc.AbstractModule):
62
66
  port: str,
63
67
  usb_port: USBPort,
64
68
  hw_control_loop: asyncio.AbstractEventLoop,
65
- execution_manager: Optional[ExecutionManager] = None,
69
+ execution_manager: ExecutionManager,
70
+ disconnected_callback: ModuleDisconnectedCallback,
71
+ error_callback: ModuleErrorCallback,
66
72
  poll_interval_seconds: Optional[float] = None,
67
73
  simulating: bool = False,
68
74
  sim_model: Optional[str] = None,
69
75
  sim_serial_number: Optional[str] = None,
70
- disconnected_callback: ModuleDisconnectedCallback = None,
71
76
  ) -> "Thermocycler":
72
77
  """
73
78
  Build and connect to a Thermocycler
@@ -108,6 +113,7 @@ class Thermocycler(mod_abc.AbstractModule):
108
113
  hw_control_loop=hw_control_loop,
109
114
  execution_manager=execution_manager,
110
115
  disconnected_callback=disconnected_callback,
116
+ error_callback=error_callback,
111
117
  )
112
118
 
113
119
  try:
@@ -126,8 +132,9 @@ class Thermocycler(mod_abc.AbstractModule):
126
132
  poller: Poller,
127
133
  device_info: Dict[str, str],
128
134
  hw_control_loop: asyncio.AbstractEventLoop,
129
- execution_manager: Optional[ExecutionManager] = None,
130
- disconnected_callback: ModuleDisconnectedCallback = None,
135
+ execution_manager: ExecutionManager,
136
+ disconnected_callback: ModuleDisconnectedCallback,
137
+ error_callback: ModuleErrorCallback,
131
138
  ) -> None:
132
139
  """
133
140
  Constructor
@@ -150,6 +157,7 @@ class Thermocycler(mod_abc.AbstractModule):
150
157
  hw_control_loop=hw_control_loop,
151
158
  execution_manager=execution_manager,
152
159
  disconnected_callback=disconnected_callback,
160
+ error_callback=error_callback,
153
161
  )
154
162
  self._device_info = device_info
155
163
  self._reader = reader
@@ -159,10 +167,13 @@ class Thermocycler(mod_abc.AbstractModule):
159
167
  self._total_step_count: Optional[int] = None
160
168
  self._current_step_index: Optional[int] = None
161
169
  self._error: Optional[str] = None
162
- self._reader.register_error_handler(self._enter_error_state)
170
+ self._unsubscribe_reader = self._reader.register_error_handler(
171
+ self._enter_error_state
172
+ )
163
173
 
164
174
  async def cleanup(self) -> None:
165
175
  """Stop the poller task."""
176
+ self._unsubscribe_reader()
166
177
  await self._poller.stop()
167
178
  await self._driver.disconnect()
168
179
 
@@ -265,6 +276,19 @@ class Thermocycler(mod_abc.AbstractModule):
265
276
  await self.open()
266
277
  await self._wait_for_lid_status(ThermocyclerLidStatus.OPEN)
267
278
 
279
+ def can_use_ramp_rate(self) -> bool:
280
+ version_string = self._device_info.get("version", "v")
281
+ if version_string.startswith("v"):
282
+ version_string = version_string[1:]
283
+ try:
284
+ version_tuple = tuple(int(c) for c in version_string.split("."))
285
+ return version_tuple >= _TC_RAMP_RATE_ADDED_VERSION
286
+ except (ValueError, IndexError):
287
+ log.error(
288
+ f"Invalid version from device: {self._device_info.get('version', '')}"
289
+ )
290
+ return False
291
+
268
292
  async def set_temperature(
269
293
  self,
270
294
  temperature: float,
@@ -290,6 +314,11 @@ class Thermocycler(mod_abc.AbstractModule):
290
314
 
291
315
  Returns: None
292
316
  """
317
+ if ramp_rate and not self.can_use_ramp_rate():
318
+ raise ThermocyclerError(
319
+ "Ramp rate is not supported by this thermocycler's firmware version, please update."
320
+ )
321
+
293
322
  await self.wait_for_is_running()
294
323
  await self._set_temperature_no_pause(
295
324
  temperature=temperature,
@@ -312,11 +341,13 @@ class Thermocycler(mod_abc.AbstractModule):
312
341
  total_seconds = seconds + (minutes * 60)
313
342
  hold_time = total_seconds if total_seconds > 0 else 0
314
343
 
315
- if ramp_rate is not None:
316
- await self._driver.set_ramp_rate(ramp_rate=ramp_rate)
344
+ if ramp_rate and not self.can_use_ramp_rate():
345
+ raise ThermocyclerError(
346
+ "Ramp rate is not supported by this thermocycler's firmware version, please update."
347
+ )
317
348
 
318
349
  await self._driver.set_plate_temperature(
319
- temp=temperature, hold_time=hold_time, volume=volume
350
+ temp=temperature, hold_time=hold_time, volume=volume, ramp_rate=ramp_rate
320
351
  )
321
352
 
322
353
  task = self._loop.create_task(self._wait_for_block_target())
@@ -419,6 +450,7 @@ class Thermocycler(mod_abc.AbstractModule):
419
450
  celsius: float,
420
451
  hold_time_seconds: Optional[float] = None,
421
452
  volume: Optional[float] = None,
453
+ ramp_rate: Optional[float] = None,
422
454
  ) -> None:
423
455
  """Set the Thermocycler's target block temperature.
424
456
 
@@ -428,10 +460,17 @@ class Thermocycler(mod_abc.AbstractModule):
428
460
  celsius: The target block temperature, in degrees celsius.
429
461
  """
430
462
  await self.wait_for_is_running()
463
+
464
+ if ramp_rate and not self.can_use_ramp_rate():
465
+ raise ThermocyclerError(
466
+ "Ramp rate is not supported by this thermocycler's firmware version, please update."
467
+ )
468
+
431
469
  await self._driver.set_plate_temperature(
432
470
  temp=celsius,
433
471
  hold_time=hold_time_seconds,
434
472
  volume=volume,
473
+ ramp_rate=ramp_rate,
435
474
  )
436
475
  await self._reader.read_block_temperature()
437
476
 
@@ -596,11 +635,12 @@ class Thermocycler(mod_abc.AbstractModule):
596
635
  temperature = step.get("temperature")
597
636
  hold_time_minutes = step.get("hold_time_minutes", None)
598
637
  hold_time_seconds = step.get("hold_time_seconds", None)
638
+ ramp_rate = step.get("ramp_rate", None)
599
639
  await self._set_temperature_no_pause(
600
640
  temperature=temperature, # type: ignore
601
641
  hold_time_minutes=hold_time_minutes,
602
642
  hold_time_seconds=hold_time_seconds,
603
- ramp_rate=None,
643
+ ramp_rate=ramp_rate,
604
644
  volume=volume,
605
645
  )
606
646
 
@@ -672,7 +712,7 @@ class Thermocycler(mod_abc.AbstractModule):
672
712
  f"https://support.opentrons.com/en/articles/3469797-thermocycler-module"
673
713
  f" for troubleshooting."
674
714
  )
675
- asyncio.run_coroutine_threadsafe(self.cleanup(), self._loop)
715
+ self.error_callback(error)
676
716
 
677
717
 
678
718
  class ThermocyclerReader(Reader):
@@ -710,14 +750,31 @@ class ThermocyclerReader(Reader):
710
750
  if self._handle_error is not None:
711
751
  self._handle_error(exception)
712
752
 
713
- def register_error_handler(self, handle_error: Callable[[Exception], None]) -> None:
753
+ def register_error_handler(
754
+ self, handle_error: Callable[[Exception], None]
755
+ ) -> Callable[[], None]:
714
756
  self._handle_error = handle_error
757
+ return self._unsubscribe_error_handler
758
+
759
+ def _unsubscribe_error_handler(self) -> None:
760
+ self._handle_error = None
715
761
 
716
762
  async def read(self) -> None:
717
763
  """Poll the thermocycler."""
718
764
  await self.read_lid_status()
719
765
  await self.read_lid_temperature()
720
766
  await self.read_block_temperature()
767
+ await self._read_errors()
768
+
769
+ async def _read_errors(self) -> None:
770
+ try:
771
+ await self._driver.get_error_state()
772
+ except UnhandledGcode:
773
+ # This device's firmware cannot accept this command, because it
774
+ # hasn't been updated or because it's a gen1. Ignore the result.
775
+ pass
776
+ # If the error is one we should let pass, raise it so the top level
777
+ # error handler can take it.
721
778
 
722
779
  async def read_lid_status(self) -> None:
723
780
  self.lid_status = await self._driver.get_lid_status()
@@ -11,6 +11,7 @@ from typing import (
11
11
  Awaitable,
12
12
  Union,
13
13
  Optional,
14
+ Protocol,
14
15
  cast,
15
16
  TYPE_CHECKING,
16
17
  TypeGuard,
@@ -44,6 +45,7 @@ class ThermocyclerStepBase(TypedDict):
44
45
  class ThermocyclerStep(ThermocyclerStepBase, total=False):
45
46
  hold_time_seconds: float
46
47
  hold_time_minutes: float
48
+ ramp_rate: Optional[float]
47
49
 
48
50
 
49
51
  class ThermocyclerCycle(TypedDict):
@@ -54,7 +56,24 @@ class ThermocyclerCycle(TypedDict):
54
56
  UploadFunction = Callable[[str, str, Dict[str, Any]], Awaitable[Tuple[bool, str]]]
55
57
 
56
58
 
57
- ModuleDisconnectedCallback = Optional[Callable[[str, str | None], None]]
59
+ class ModuleDisconnectedCallback(Protocol):
60
+ """Protocol for the callback when the module should be disconnected."""
61
+
62
+ def __call__(self, model: str, port: str, serial: str | None) -> None:
63
+ ...
64
+
65
+
66
+ class ModuleErrorCallback(Protocol):
67
+ """Protocol for the callback when the module sees a hardware error."""
68
+
69
+ def __call__(
70
+ self,
71
+ exc: Exception,
72
+ model: str,
73
+ port: str,
74
+ serial: str | None,
75
+ ) -> None:
76
+ ...
58
77
 
59
78
 
60
79
  class MagneticModuleData(TypedDict):
@@ -6,7 +6,12 @@ from opentrons.drivers.rpi_drivers.types import USBPort
6
6
 
7
7
  from ..execution_manager import ExecutionManager
8
8
 
9
- from .types import ModuleDisconnectedCallback, ModuleType, SpeedStatus
9
+ from .types import (
10
+ ModuleDisconnectedCallback,
11
+ ModuleType,
12
+ SpeedStatus,
13
+ ModuleErrorCallback,
14
+ )
10
15
  from .mod_abc import AbstractModule
11
16
  from .tempdeck import TempDeck
12
17
  from .magdeck import MagDeck
@@ -46,10 +51,11 @@ async def build(
46
51
  simulating: bool,
47
52
  usb_port: USBPort,
48
53
  hw_control_loop: asyncio.AbstractEventLoop,
49
- execution_manager: Optional[ExecutionManager] = None,
54
+ execution_manager: ExecutionManager,
55
+ disconnected_callback: ModuleDisconnectedCallback,
56
+ error_callback: ModuleErrorCallback,
50
57
  sim_model: Optional[str] = None,
51
58
  sim_serial_number: Optional[str] = None,
52
- disconnected_callback: ModuleDisconnectedCallback = None,
53
59
  ) -> AbstractModule:
54
60
  return await _MODULE_CLS_BY_TYPE[type].build(
55
61
  port=port,
@@ -57,9 +63,10 @@ async def build(
57
63
  simulating=simulating,
58
64
  hw_control_loop=hw_control_loop,
59
65
  execution_manager=execution_manager,
66
+ disconnected_callback=disconnected_callback,
67
+ error_callback=error_callback,
60
68
  sim_model=sim_model,
61
69
  sim_serial_number=sim_serial_number,
62
- disconnected_callback=disconnected_callback,
63
70
  )
64
71
 
65
72
 
@@ -195,8 +195,7 @@ def target_position_from_plunger(
195
195
  def target_positions_from_plunger_tracking(
196
196
  mount: Union[Mount, OT3Mount],
197
197
  plunger_delta: float,
198
- z_delta: float,
199
- current_position: Dict[Axis, float],
198
+ end_position: OrderedDict[Axis, float],
200
199
  ) -> "OrderedDict[Axis, float]":
201
200
  """Create a target position for machine axes including plungers for dynamic liquid tracking.
202
201
 
@@ -206,10 +205,11 @@ def target_positions_from_plunger_tracking(
206
205
  volume to aspirate/dispense.
207
206
  z_delta: the distance to move the z axis- should be determined based on volume and well geometry.
208
207
  """
209
- all_axes_pos = target_position_from_plunger(mount, plunger_delta, current_position)
210
- z_ax = Axis.by_mount(mount)
211
- all_axes_pos[z_ax] = current_position[z_ax] + z_delta
212
- return all_axes_pos
208
+ plunger_pos = OrderedDict()
209
+ plunger = Axis.of_main_tool_actuator(mount)
210
+ plunger_pos[plunger] = plunger_delta
211
+ end_position.update(plunger_pos)
212
+ return end_position
213
213
 
214
214
 
215
215
  def deck_point_from_machine_point(
@@ -77,6 +77,8 @@ class NozzleMap:
77
77
  #: A map of all of the nozzles of an instrument
78
78
  full_instrument_rows: Dict[str, List[str]]
79
79
  #: A map of all the rows of an instrument
80
+ full_instrument_columns: Dict[str, List[str]]
81
+ #: A map of all the columns of an instrument
80
82
 
81
83
  @classmethod
82
84
  def determine_nozzle_configuration(
@@ -299,6 +301,7 @@ class NozzleMap:
299
301
  rows=rows,
300
302
  full_instrument_map_store=physical_nozzles,
301
303
  full_instrument_rows=physical_rows,
304
+ full_instrument_columns=physical_columns,
302
305
  columns=columns,
303
306
  configuration=cls.determine_nozzle_configuration(
304
307
  physical_rows, rows, physical_columns, columns