opentrons 8.7.0a0__py3-none-any.whl → 8.7.0a2__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.

Potentially problematic release.


This version of opentrons might be problematic. Click here for more details.

Files changed (121) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/drivers/thermocycler/abstract.py +1 -0
  3. opentrons/drivers/thermocycler/driver.py +33 -4
  4. opentrons/drivers/thermocycler/simulator.py +2 -0
  5. opentrons/hardware_control/api.py +24 -5
  6. opentrons/hardware_control/backends/controller.py +8 -2
  7. opentrons/hardware_control/backends/ot3controller.py +3 -0
  8. opentrons/hardware_control/backends/ot3simulator.py +2 -1
  9. opentrons/hardware_control/backends/simulator.py +2 -1
  10. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  11. opentrons/hardware_control/module_control.py +82 -8
  12. opentrons/hardware_control/modules/__init__.py +3 -0
  13. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  14. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  15. opentrons/hardware_control/modules/heater_shaker.py +30 -5
  16. opentrons/hardware_control/modules/magdeck.py +8 -4
  17. opentrons/hardware_control/modules/mod_abc.py +13 -5
  18. opentrons/hardware_control/modules/tempdeck.py +25 -5
  19. opentrons/hardware_control/modules/thermocycler.py +56 -10
  20. opentrons/hardware_control/modules/types.py +20 -1
  21. opentrons/hardware_control/modules/utils.py +11 -4
  22. opentrons/hardware_control/nozzle_manager.py +3 -0
  23. opentrons/hardware_control/ot3api.py +26 -5
  24. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  25. opentrons/hardware_control/types.py +31 -2
  26. opentrons/legacy_commands/protocol_commands.py +20 -0
  27. opentrons/legacy_commands/types.py +42 -0
  28. opentrons/motion_planning/waypoints.py +15 -29
  29. opentrons/protocol_api/__init__.py +5 -0
  30. opentrons/protocol_api/_types.py +6 -1
  31. opentrons/protocol_api/core/common.py +3 -1
  32. opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
  33. opentrons/protocol_api/core/engine/_default_liquid_class_versions.py +2 -0
  34. opentrons/protocol_api/core/engine/labware.py +8 -1
  35. opentrons/protocol_api/core/engine/module_core.py +4 -0
  36. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +77 -17
  37. opentrons/protocol_api/core/engine/protocol.py +18 -1
  38. opentrons/protocol_api/core/engine/tasks.py +35 -0
  39. opentrons/protocol_api/core/legacy/legacy_module_core.py +2 -0
  40. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
  41. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  42. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  43. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  44. opentrons/protocol_api/core/module.py +1 -0
  45. opentrons/protocol_api/core/protocol.py +11 -2
  46. opentrons/protocol_api/core/tasks.py +31 -0
  47. opentrons/protocol_api/module_contexts.py +1 -0
  48. opentrons/protocol_api/protocol_context.py +26 -4
  49. opentrons/protocol_api/robot_context.py +38 -21
  50. opentrons/protocol_api/tasks.py +48 -0
  51. opentrons/protocol_api/validation.py +6 -1
  52. opentrons/protocol_engine/actions/__init__.py +4 -2
  53. opentrons/protocol_engine/actions/actions.py +22 -9
  54. opentrons/protocol_engine/clients/sync_client.py +6 -7
  55. opentrons/protocol_engine/commands/__init__.py +42 -0
  56. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  57. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  58. opentrons/protocol_engine/commands/aspirate.py +1 -0
  59. opentrons/protocol_engine/commands/command.py +1 -0
  60. opentrons/protocol_engine/commands/command_unions.py +39 -0
  61. opentrons/protocol_engine/commands/create_timer.py +83 -0
  62. opentrons/protocol_engine/commands/dispense.py +1 -0
  63. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  64. opentrons/protocol_engine/commands/movement_common.py +2 -0
  65. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  66. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  67. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  68. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  69. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +17 -1
  70. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  71. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  72. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  73. opentrons/protocol_engine/errors/__init__.py +4 -0
  74. opentrons/protocol_engine/errors/exceptions.py +55 -0
  75. opentrons/protocol_engine/execution/__init__.py +2 -0
  76. opentrons/protocol_engine/execution/command_executor.py +8 -0
  77. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  78. opentrons/protocol_engine/execution/labware_movement.py +9 -12
  79. opentrons/protocol_engine/execution/movement.py +2 -0
  80. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  81. opentrons/protocol_engine/execution/run_control.py +8 -0
  82. opentrons/protocol_engine/execution/task_handler.py +157 -0
  83. opentrons/protocol_engine/protocol_engine.py +67 -33
  84. opentrons/protocol_engine/resources/__init__.py +2 -0
  85. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  86. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  87. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  88. opentrons/protocol_engine/state/_well_math.py +60 -18
  89. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  90. opentrons/protocol_engine/state/commands.py +7 -7
  91. opentrons/protocol_engine/state/geometry.py +237 -379
  92. opentrons/protocol_engine/state/labware.py +52 -102
  93. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  94. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
  95. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  96. opentrons/protocol_engine/state/modules.py +26 -7
  97. opentrons/protocol_engine/state/motion.py +44 -0
  98. opentrons/protocol_engine/state/state.py +14 -0
  99. opentrons/protocol_engine/state/state_summary.py +2 -0
  100. opentrons/protocol_engine/state/tasks.py +139 -0
  101. opentrons/protocol_engine/state/tips.py +177 -258
  102. opentrons/protocol_engine/state/update_types.py +16 -9
  103. opentrons/protocol_engine/types/__init__.py +9 -3
  104. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  105. opentrons/protocol_engine/types/instrument.py +8 -1
  106. opentrons/protocol_engine/types/labware.py +1 -13
  107. opentrons/protocol_engine/types/module.py +10 -0
  108. opentrons/protocol_engine/types/tasks.py +38 -0
  109. opentrons/protocol_engine/types/tip.py +9 -0
  110. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  111. opentrons/protocol_runner/run_orchestrator.py +18 -2
  112. opentrons/protocols/api_support/definitions.py +1 -1
  113. opentrons/protocols/api_support/types.py +2 -1
  114. opentrons/simulate.py +48 -15
  115. opentrons/system/camera.py +1 -1
  116. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/METADATA +4 -4
  117. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/RECORD +120 -107
  118. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  119. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/WHEEL +0 -0
  120. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/entry_points.txt +0 -0
  121. {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/licenses/LICENSE +0 -0
@@ -2,7 +2,7 @@ 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
@@ -15,6 +15,7 @@ from opentrons.hardware_control.poller import Reader, Poller
15
15
  from opentrons.hardware_control.modules import mod_abc, update
16
16
  from opentrons.hardware_control.modules.types import (
17
17
  ModuleDisconnectedCallback,
18
+ ModuleErrorCallback,
18
19
  ModuleType,
19
20
  TemperatureStatus,
20
21
  SpeedStatus,
@@ -47,12 +48,13 @@ class HeaterShaker(mod_abc.AbstractModule):
47
48
  port: str,
48
49
  usb_port: USBPort,
49
50
  hw_control_loop: asyncio.AbstractEventLoop,
50
- execution_manager: Optional[ExecutionManager] = None,
51
+ execution_manager: ExecutionManager,
52
+ disconnected_callback: ModuleDisconnectedCallback,
53
+ error_callback: ModuleErrorCallback,
51
54
  poll_interval_seconds: Optional[float] = 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
  ) -> "HeaterShaker":
57
59
  """
58
60
  Build a HeaterShaker
@@ -67,6 +69,7 @@ class HeaterShaker(mod_abc.AbstractModule):
67
69
  loop: Loop
68
70
  sim_model: The model name used by simulator
69
71
  disconnected_callback: Callback to inform the module controller that the device was disconnected
72
+ error_callback: Callback to inform the module controller of an asynchronous error
70
73
 
71
74
  Returns:
72
75
  HeaterShaker instance
@@ -91,6 +94,7 @@ class HeaterShaker(mod_abc.AbstractModule):
91
94
  hw_control_loop=hw_control_loop,
92
95
  execution_manager=execution_manager,
93
96
  disconnected_callback=disconnected_callback,
97
+ error_callback=error_callback,
94
98
  )
95
99
 
96
100
  try:
@@ -109,8 +113,9 @@ class HeaterShaker(mod_abc.AbstractModule):
109
113
  poller: Poller,
110
114
  device_info: Mapping[str, str],
111
115
  hw_control_loop: asyncio.AbstractEventLoop,
112
- execution_manager: Optional[ExecutionManager] = None,
113
- disconnected_callback: ModuleDisconnectedCallback = None,
116
+ execution_manager: ExecutionManager,
117
+ disconnected_callback: ModuleDisconnectedCallback,
118
+ error_callback: ModuleErrorCallback,
114
119
  ):
115
120
  super().__init__(
116
121
  port=port,
@@ -118,14 +123,22 @@ class HeaterShaker(mod_abc.AbstractModule):
118
123
  hw_control_loop=hw_control_loop,
119
124
  execution_manager=execution_manager,
120
125
  disconnected_callback=disconnected_callback,
126
+ error_callback=error_callback,
121
127
  )
122
128
  self._device_info = device_info
123
129
  self._driver = driver
124
130
  self._reader = reader
125
131
  self._poller = poller
132
+ self._unsubscribe_reader = self._reader.register_error_handler(
133
+ self._handle_error
134
+ )
135
+
136
+ def _handle_error(self, error: Exception) -> None:
137
+ self.error_callback(error)
126
138
 
127
139
  async def cleanup(self) -> None:
128
140
  """Stop the poller task"""
141
+ self._unsubscribe_reader()
129
142
  await self._poller.stop()
130
143
  await self._driver.disconnect()
131
144
 
@@ -397,6 +410,16 @@ class HeaterShakerReader(Reader):
397
410
  self.labware_latch = HeaterShakerLabwareLatchStatus.IDLE_UNKNOWN
398
411
  self.error: Optional[str] = None
399
412
  self._driver = driver
413
+ self._handle_error: Callable[[Exception], None] | None = None
414
+
415
+ def register_error_handler(
416
+ self, handle_error: Callable[[Exception], None]
417
+ ) -> Callable[[], None]:
418
+ self._handle_error = handle_error
419
+ return self._unsubscribe_error_handler
420
+
421
+ def _unsubscribe_error_handler(self) -> None:
422
+ self._handle_error = None
400
423
 
401
424
  async def read(self) -> None:
402
425
  await self.read_temperature()
@@ -420,6 +443,8 @@ class HeaterShakerReader(Reader):
420
443
  if exception is None:
421
444
  self.error = None
422
445
  else:
446
+ if self._handle_error:
447
+ self._handle_error(exception)
423
448
  try:
424
449
  self.error = str(exception.args[0])
425
450
  except Exception:
@@ -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:
@@ -103,6 +107,10 @@ class AbstractModule(abc.ABC):
103
107
  if self._disconnected_callback is not None:
104
108
  self._disconnected_callback(self.port, self.serial_number)
105
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)
113
+
106
114
  def get_bundled_fw(self) -> Optional[BundledFirmware]:
107
115
  """Get absolute path to bundled version of module fw if available."""
108
116
  if not IS_ROBOT:
@@ -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
@@ -36,6 +37,8 @@ DFU_PID = "df11"
36
37
  _TC_PLATE_LIFT_OPEN_DEGREES = 20
37
38
  _TC_PLATE_LIFT_RETURN_DEGREES = 23
38
39
 
40
+ _TC_RAMP_RATE_ADDED_VERSION = (1, 0, 8) # v1.0.8
41
+
39
42
 
40
43
  class ThermocyclerError(Exception):
41
44
  pass
@@ -62,12 +65,13 @@ class Thermocycler(mod_abc.AbstractModule):
62
65
  port: str,
63
66
  usb_port: USBPort,
64
67
  hw_control_loop: asyncio.AbstractEventLoop,
65
- execution_manager: Optional[ExecutionManager] = None,
68
+ execution_manager: ExecutionManager,
69
+ disconnected_callback: ModuleDisconnectedCallback,
70
+ error_callback: ModuleErrorCallback,
66
71
  poll_interval_seconds: Optional[float] = None,
67
72
  simulating: bool = False,
68
73
  sim_model: Optional[str] = None,
69
74
  sim_serial_number: Optional[str] = None,
70
- disconnected_callback: ModuleDisconnectedCallback = None,
71
75
  ) -> "Thermocycler":
72
76
  """
73
77
  Build and connect to a Thermocycler
@@ -108,6 +112,7 @@ class Thermocycler(mod_abc.AbstractModule):
108
112
  hw_control_loop=hw_control_loop,
109
113
  execution_manager=execution_manager,
110
114
  disconnected_callback=disconnected_callback,
115
+ error_callback=error_callback,
111
116
  )
112
117
 
113
118
  try:
@@ -126,8 +131,9 @@ class Thermocycler(mod_abc.AbstractModule):
126
131
  poller: Poller,
127
132
  device_info: Dict[str, str],
128
133
  hw_control_loop: asyncio.AbstractEventLoop,
129
- execution_manager: Optional[ExecutionManager] = None,
130
- disconnected_callback: ModuleDisconnectedCallback = None,
134
+ execution_manager: ExecutionManager,
135
+ disconnected_callback: ModuleDisconnectedCallback,
136
+ error_callback: ModuleErrorCallback,
131
137
  ) -> None:
132
138
  """
133
139
  Constructor
@@ -150,6 +156,7 @@ class Thermocycler(mod_abc.AbstractModule):
150
156
  hw_control_loop=hw_control_loop,
151
157
  execution_manager=execution_manager,
152
158
  disconnected_callback=disconnected_callback,
159
+ error_callback=error_callback,
153
160
  )
154
161
  self._device_info = device_info
155
162
  self._reader = reader
@@ -159,10 +166,13 @@ class Thermocycler(mod_abc.AbstractModule):
159
166
  self._total_step_count: Optional[int] = None
160
167
  self._current_step_index: Optional[int] = None
161
168
  self._error: Optional[str] = None
162
- self._reader.register_error_handler(self._enter_error_state)
169
+ self._unsubscribe_reader = self._reader.register_error_handler(
170
+ self._enter_error_state
171
+ )
163
172
 
164
173
  async def cleanup(self) -> None:
165
174
  """Stop the poller task."""
175
+ self._unsubscribe_reader()
166
176
  await self._poller.stop()
167
177
  await self._driver.disconnect()
168
178
 
@@ -265,6 +275,19 @@ class Thermocycler(mod_abc.AbstractModule):
265
275
  await self.open()
266
276
  await self._wait_for_lid_status(ThermocyclerLidStatus.OPEN)
267
277
 
278
+ def can_use_ramp_rate(self) -> bool:
279
+ version_string = self._device_info.get("version", "v")
280
+ if version_string.startswith("v"):
281
+ version_string = version_string[1:]
282
+ try:
283
+ version_tuple = tuple(int(c) for c in version_string.split("."))
284
+ return version_tuple >= _TC_RAMP_RATE_ADDED_VERSION
285
+ except (ValueError, IndexError):
286
+ log.error(
287
+ f"Invalid version from device: {self._device_info.get('version', '')}"
288
+ )
289
+ return False
290
+
268
291
  async def set_temperature(
269
292
  self,
270
293
  temperature: float,
@@ -290,6 +313,11 @@ class Thermocycler(mod_abc.AbstractModule):
290
313
 
291
314
  Returns: None
292
315
  """
316
+ if ramp_rate and not self.can_use_ramp_rate():
317
+ raise ThermocyclerError(
318
+ "Ramp rate is not supported by this thermocycler's firmware version, please update."
319
+ )
320
+
293
321
  await self.wait_for_is_running()
294
322
  await self._set_temperature_no_pause(
295
323
  temperature=temperature,
@@ -312,11 +340,13 @@ class Thermocycler(mod_abc.AbstractModule):
312
340
  total_seconds = seconds + (minutes * 60)
313
341
  hold_time = total_seconds if total_seconds > 0 else 0
314
342
 
315
- if ramp_rate is not None:
316
- await self._driver.set_ramp_rate(ramp_rate=ramp_rate)
343
+ if ramp_rate and not self.can_use_ramp_rate():
344
+ raise ThermocyclerError(
345
+ "Ramp rate is not supported by this thermocycler's firmware version, please update."
346
+ )
317
347
 
318
348
  await self._driver.set_plate_temperature(
319
- temp=temperature, hold_time=hold_time, volume=volume
349
+ temp=temperature, hold_time=hold_time, volume=volume, ramp_rate=ramp_rate
320
350
  )
321
351
 
322
352
  task = self._loop.create_task(self._wait_for_block_target())
@@ -419,6 +449,7 @@ class Thermocycler(mod_abc.AbstractModule):
419
449
  celsius: float,
420
450
  hold_time_seconds: Optional[float] = None,
421
451
  volume: Optional[float] = None,
452
+ ramp_rate: Optional[float] = None,
422
453
  ) -> None:
423
454
  """Set the Thermocycler's target block temperature.
424
455
 
@@ -428,10 +459,17 @@ class Thermocycler(mod_abc.AbstractModule):
428
459
  celsius: The target block temperature, in degrees celsius.
429
460
  """
430
461
  await self.wait_for_is_running()
462
+
463
+ if ramp_rate and not self.can_use_ramp_rate():
464
+ raise ThermocyclerError(
465
+ "Ramp rate is not supported by this thermocycler's firmware version, please update."
466
+ )
467
+
431
468
  await self._driver.set_plate_temperature(
432
469
  temp=celsius,
433
470
  hold_time=hold_time_seconds,
434
471
  volume=volume,
472
+ ramp_rate=ramp_rate,
435
473
  )
436
474
  await self._reader.read_block_temperature()
437
475
 
@@ -596,11 +634,12 @@ class Thermocycler(mod_abc.AbstractModule):
596
634
  temperature = step.get("temperature")
597
635
  hold_time_minutes = step.get("hold_time_minutes", None)
598
636
  hold_time_seconds = step.get("hold_time_seconds", None)
637
+ ramp_rate = step.get("ramp_rate", None)
599
638
  await self._set_temperature_no_pause(
600
639
  temperature=temperature, # type: ignore
601
640
  hold_time_minutes=hold_time_minutes,
602
641
  hold_time_seconds=hold_time_seconds,
603
- ramp_rate=None,
642
+ ramp_rate=ramp_rate,
604
643
  volume=volume,
605
644
  )
606
645
 
@@ -673,6 +712,7 @@ class Thermocycler(mod_abc.AbstractModule):
673
712
  f" for troubleshooting."
674
713
  )
675
714
  asyncio.run_coroutine_threadsafe(self.cleanup(), self._loop)
715
+ self.error_callback(error)
676
716
 
677
717
 
678
718
  class ThermocyclerReader(Reader):
@@ -710,8 +750,14 @@ 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."""
@@ -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, 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
 
@@ -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
@@ -74,6 +74,7 @@ from .types import (
74
74
  DoorStateNotification,
75
75
  ErrorMessageNotification,
76
76
  HardwareEvent,
77
+ AsynchronousModuleErrorNotification,
77
78
  HardwareEventHandler,
78
79
  HardwareAction,
79
80
  HepaFanState,
@@ -367,6 +368,21 @@ class OT3API(
367
368
 
368
369
  return futures
369
370
 
371
+ def _send_module_notification(self, event: HardwareEvent) -> None:
372
+ if not isinstance(
373
+ event,
374
+ AsynchronousModuleErrorNotification,
375
+ ):
376
+ return
377
+ mod_log.info(
378
+ f"Forwarding module event {event.event} for {event.module_model} {event.module_serial} at {event.port}"
379
+ )
380
+ for cb in self._callbacks:
381
+ try:
382
+ cb(event)
383
+ except Exception:
384
+ mod_log.exception("Errored during module asynchronous callback")
385
+
370
386
  def _reset_last_mount(self) -> None:
371
387
  self._last_moved_mount = None
372
388
 
@@ -422,7 +438,9 @@ class OT3API(
422
438
 
423
439
  await api_instance.set_status_bar_enabled(status_bar_enabled)
424
440
  module_controls = await AttachedModulesControl.build(
425
- api_instance, board_revision=backend.board_revision
441
+ api_instance,
442
+ board_revision=backend.board_revision,
443
+ event_callback=api_instance._send_module_notification,
426
444
  )
427
445
  backend.module_controls = module_controls
428
446
  await backend.build_estop_detector()
@@ -484,7 +502,9 @@ class OT3API(
484
502
  )
485
503
  await api_instance.cache_instruments()
486
504
  module_controls = await AttachedModulesControl.build(
487
- api_instance, board_revision=backend.board_revision
505
+ api_instance,
506
+ board_revision=backend.board_revision,
507
+ event_callback=api_instance._send_module_notification,
488
508
  )
489
509
  backend.module_controls = module_controls
490
510
  await backend.watch(api_instance.loop)
@@ -627,9 +647,10 @@ class OT3API(
627
647
  self.is_simulator
628
648
  ), "Cannot build simulating module from non-simulating hardware control API"
629
649
 
630
- return await self._backend.module_controls.build_module(
631
- port="",
632
- usb_port=USBPort(name="", port_number=1, port_group=PortGroup.LEFT),
650
+ return await self._backend.module_controls.register_simulated_module(
651
+ simulated_usb_port=USBPort(
652
+ name="", port_number=1, port_group=PortGroup.LEFT
653
+ ),
633
654
  type=modules.ModuleType.from_model(model),
634
655
  sim_model=model.value,
635
656
  )
@@ -1,4 +1,5 @@
1
1
  """Module Firmware update script."""
2
+
2
3
  import argparse
3
4
  import asyncio
4
5
  from glob import glob
@@ -14,6 +15,7 @@ from opentrons.hardware_control import modules
14
15
  from opentrons.hardware_control.modules.mod_abc import AbstractModule
15
16
  from opentrons.hardware_control.modules.update import update_firmware
16
17
  from opentrons.hardware_control.types import BoardRevision
18
+ from opentrons.hardware_control.execution_manager import ExecutionManager
17
19
 
18
20
 
19
21
  # Constants for checking if module is back online
@@ -84,6 +86,9 @@ async def build_module(
84
86
  port=port,
85
87
  usb_port=mod.usb_port,
86
88
  type=modules.MODULE_TYPE_BY_NAME[mod.name],
89
+ execution_manager=ExecutionManager(),
90
+ disconnected_callback=lambda *args: None,
91
+ error_callback=lambda *args: None,
87
92
  simulating=False,
88
93
  hw_control_loop=loop,
89
94
  )