opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (190) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +4 -1
  3. opentrons/config/__init__.py +7 -0
  4. opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
  5. opentrons/drivers/heater_shaker/abstract.py +5 -0
  6. opentrons/drivers/heater_shaker/driver.py +10 -0
  7. opentrons/drivers/heater_shaker/simulator.py +4 -0
  8. opentrons/drivers/thermocycler/abstract.py +6 -0
  9. opentrons/drivers/thermocycler/driver.py +61 -10
  10. opentrons/drivers/thermocycler/simulator.py +6 -0
  11. opentrons/drivers/vacuum_module/__init__.py +5 -0
  12. opentrons/drivers/vacuum_module/abstract.py +93 -0
  13. opentrons/drivers/vacuum_module/driver.py +208 -0
  14. opentrons/drivers/vacuum_module/errors.py +39 -0
  15. opentrons/drivers/vacuum_module/simulator.py +85 -0
  16. opentrons/drivers/vacuum_module/types.py +79 -0
  17. opentrons/execute.py +3 -0
  18. opentrons/hardware_control/api.py +24 -5
  19. opentrons/hardware_control/backends/controller.py +8 -2
  20. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  21. opentrons/hardware_control/backends/ot3controller.py +35 -2
  22. opentrons/hardware_control/backends/ot3simulator.py +3 -1
  23. opentrons/hardware_control/backends/ot3utils.py +37 -0
  24. opentrons/hardware_control/backends/simulator.py +2 -1
  25. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  26. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  27. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  28. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  29. opentrons/hardware_control/emulation/settings.py +1 -1
  30. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  31. opentrons/hardware_control/module_control.py +105 -10
  32. opentrons/hardware_control/modules/__init__.py +3 -0
  33. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  34. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  35. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  36. opentrons/hardware_control/modules/magdeck.py +8 -4
  37. opentrons/hardware_control/modules/mod_abc.py +14 -6
  38. opentrons/hardware_control/modules/tempdeck.py +25 -5
  39. opentrons/hardware_control/modules/thermocycler.py +68 -11
  40. opentrons/hardware_control/modules/types.py +20 -1
  41. opentrons/hardware_control/modules/utils.py +11 -4
  42. opentrons/hardware_control/motion_utilities.py +6 -6
  43. opentrons/hardware_control/nozzle_manager.py +3 -0
  44. opentrons/hardware_control/ot3api.py +92 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +12 -4
  47. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  48. opentrons/hardware_control/types.py +43 -2
  49. opentrons/legacy_commands/commands.py +58 -5
  50. opentrons/legacy_commands/module_commands.py +52 -0
  51. opentrons/legacy_commands/protocol_commands.py +53 -1
  52. opentrons/legacy_commands/types.py +155 -1
  53. opentrons/motion_planning/deck_conflict.py +17 -12
  54. opentrons/motion_planning/waypoints.py +15 -29
  55. opentrons/protocol_api/__init__.py +5 -1
  56. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  57. opentrons/protocol_api/_types.py +8 -1
  58. opentrons/protocol_api/core/common.py +3 -1
  59. opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
  60. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  61. opentrons/protocol_api/core/engine/instrument.py +109 -26
  62. opentrons/protocol_api/core/engine/labware.py +8 -1
  63. opentrons/protocol_api/core/engine/module_core.py +95 -4
  64. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +4 -18
  65. opentrons/protocol_api/core/engine/protocol.py +51 -2
  66. opentrons/protocol_api/core/engine/stringify.py +2 -0
  67. opentrons/protocol_api/core/engine/tasks.py +48 -0
  68. opentrons/protocol_api/core/engine/well.py +8 -0
  69. opentrons/protocol_api/core/instrument.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  71. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  72. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  73. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  74. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  75. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  76. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  77. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  78. opentrons/protocol_api/core/module.py +58 -2
  79. opentrons/protocol_api/core/protocol.py +23 -2
  80. opentrons/protocol_api/core/tasks.py +31 -0
  81. opentrons/protocol_api/core/well.py +4 -0
  82. opentrons/protocol_api/instrument_context.py +388 -2
  83. opentrons/protocol_api/labware.py +10 -2
  84. opentrons/protocol_api/module_contexts.py +170 -6
  85. opentrons/protocol_api/protocol_context.py +87 -21
  86. opentrons/protocol_api/robot_context.py +41 -25
  87. opentrons/protocol_api/tasks.py +48 -0
  88. opentrons/protocol_api/validation.py +49 -3
  89. opentrons/protocol_engine/__init__.py +4 -0
  90. opentrons/protocol_engine/actions/__init__.py +6 -2
  91. opentrons/protocol_engine/actions/actions.py +31 -9
  92. opentrons/protocol_engine/clients/sync_client.py +42 -7
  93. opentrons/protocol_engine/commands/__init__.py +56 -0
  94. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  96. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  97. opentrons/protocol_engine/commands/aspirate.py +1 -0
  98. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  99. opentrons/protocol_engine/commands/capture_image.py +302 -0
  100. opentrons/protocol_engine/commands/command.py +2 -0
  101. opentrons/protocol_engine/commands/command_unions.py +62 -0
  102. opentrons/protocol_engine/commands/create_timer.py +83 -0
  103. opentrons/protocol_engine/commands/dispense.py +1 -0
  104. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  105. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  106. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  107. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  108. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  109. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  110. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  111. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  112. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  113. opentrons/protocol_engine/commands/move_labware.py +3 -4
  114. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  115. opentrons/protocol_engine/commands/movement_common.py +31 -2
  116. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  117. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  118. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  119. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  120. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  122. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  123. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  124. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  125. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  126. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  127. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  128. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  129. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  130. opentrons/protocol_engine/engine_support.py +3 -0
  131. opentrons/protocol_engine/errors/__init__.py +12 -0
  132. opentrons/protocol_engine/errors/exceptions.py +119 -0
  133. opentrons/protocol_engine/execution/__init__.py +4 -0
  134. opentrons/protocol_engine/execution/command_executor.py +62 -1
  135. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  136. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  137. opentrons/protocol_engine/execution/movement.py +2 -0
  138. opentrons/protocol_engine/execution/pipetting.py +26 -25
  139. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  140. opentrons/protocol_engine/execution/run_control.py +8 -0
  141. opentrons/protocol_engine/execution/task_handler.py +157 -0
  142. opentrons/protocol_engine/protocol_engine.py +137 -36
  143. opentrons/protocol_engine/resources/__init__.py +4 -0
  144. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  145. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  146. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  147. opentrons/protocol_engine/resources/file_provider.py +133 -58
  148. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  149. opentrons/protocol_engine/slot_standardization.py +2 -0
  150. opentrons/protocol_engine/state/_well_math.py +60 -18
  151. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  152. opentrons/protocol_engine/state/camera.py +54 -0
  153. opentrons/protocol_engine/state/commands.py +37 -14
  154. opentrons/protocol_engine/state/geometry.py +276 -379
  155. opentrons/protocol_engine/state/labware.py +62 -108
  156. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  157. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  158. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  159. opentrons/protocol_engine/state/modules.py +30 -8
  160. opentrons/protocol_engine/state/motion.py +60 -18
  161. opentrons/protocol_engine/state/preconditions.py +59 -0
  162. opentrons/protocol_engine/state/state.py +44 -0
  163. opentrons/protocol_engine/state/state_summary.py +4 -0
  164. opentrons/protocol_engine/state/tasks.py +139 -0
  165. opentrons/protocol_engine/state/tips.py +177 -258
  166. opentrons/protocol_engine/state/update_types.py +26 -9
  167. opentrons/protocol_engine/types/__init__.py +23 -4
  168. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  169. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  170. opentrons/protocol_engine/types/instrument.py +8 -1
  171. opentrons/protocol_engine/types/labware.py +1 -13
  172. opentrons/protocol_engine/types/location.py +26 -2
  173. opentrons/protocol_engine/types/module.py +11 -1
  174. opentrons/protocol_engine/types/tasks.py +38 -0
  175. opentrons/protocol_engine/types/tip.py +9 -0
  176. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  177. opentrons/protocol_runner/protocol_runner.py +14 -1
  178. opentrons/protocol_runner/run_orchestrator.py +49 -2
  179. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  180. opentrons/protocols/api_support/definitions.py +1 -1
  181. opentrons/protocols/api_support/types.py +2 -1
  182. opentrons/simulate.py +51 -15
  183. opentrons/system/camera.py +334 -4
  184. opentrons/system/ffmpeg.py +110 -0
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
  186. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
  187. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
  190. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/licenses/LICENSE +0 -0
@@ -414,7 +414,8 @@ class Simulator:
414
414
 
415
415
  @ensure_yield
416
416
  async def clean_up(self) -> None:
417
- pass
417
+ if hasattr(self, "_module_controls") and self._module_controls is not None:
418
+ await self._module_controls.clean_up()
418
419
 
419
420
  @ensure_yield
420
421
  async def configure_mount(
@@ -91,6 +91,8 @@ class SubsystemManager:
91
91
  self._present_tools = tools.types.ToolSummary(
92
92
  left=None, right=None, gripper=None
93
93
  )
94
+ # This is intended to be an internal variable but is modified in unit tests to avoid long timeouts
95
+ self._check_device_update_timeout = 10.0
94
96
 
95
97
  @property
96
98
  def ok(self) -> bool:
@@ -183,11 +185,12 @@ class SubsystemManager:
183
185
  return self._tool_task_state is True
184
186
 
185
187
  async def _check_devices_after_update(
186
- self, devices: Set[SubSystem], timeout_sec: float = 10.0
188
+ self, devices: Set[SubSystem], timeout_sec: Optional[float] = None
187
189
  ) -> None:
188
190
  try:
189
191
  await asyncio.wait_for(
190
- self._do_check_devices_after_update(devices), timeout=timeout_sec
192
+ self._do_check_devices_after_update(devices),
193
+ timeout=timeout_sec or self._check_device_update_timeout,
191
194
  )
192
195
  except asyncio.TimeoutError:
193
196
  raise RuntimeError("Device failed to come back after firmware update")
@@ -10,12 +10,14 @@ class AbstractEmulator(ABC):
10
10
  """Handle a command and return a response."""
11
11
  ...
12
12
 
13
- @staticmethod
14
- def get_terminator() -> bytes:
13
+ def get_terminator(self) -> bytes:
15
14
  """Get the command terminator for messages coming from PI."""
16
15
  return b"\r\n\r\n"
17
16
 
18
- @staticmethod
19
- def get_ack() -> bytes:
17
+ def get_ack(self) -> bytes:
20
18
  """Get the command ack send to the PI."""
21
19
  return b"ok\r\nok\r\n"
20
+
21
+ def get_autoack(self) -> bool:
22
+ """Should this system automatically acknowledge messages?"""
23
+ return True
@@ -27,12 +27,15 @@ class ConnectionHandler:
27
27
  try:
28
28
  response = self._emulator.handle(line.decode().strip())
29
29
  if response:
30
- response = f"{response}\r\n"
31
- logger.debug("%s Sending: %s", emulator_name, response)
32
- writer.write(response.encode())
30
+ response_bytes = response.encode() + self._emulator.get_terminator()
31
+ logger.debug(f"{emulator_name} Sending: {response_bytes!r}")
32
+ writer.write(response_bytes)
33
33
  except Exception as e:
34
34
  logger.exception("%s exception", emulator_name)
35
- writer.write(f"Error: {str(e)}\r\n".encode())
35
+ writer.write(
36
+ f"Error: {str(e)} ".encode() + self._emulator.get_terminator()
37
+ )
36
38
 
37
- writer.write(self._emulator.get_ack())
39
+ if self._emulator.get_autoack():
40
+ writer.write(self._emulator.get_ack())
38
41
  await writer.drain()
@@ -2,6 +2,7 @@
2
2
 
3
3
  The purpose is to provide a fake backend that responds to GCODE commands.
4
4
  """
5
+
5
6
  import logging
6
7
  from time import sleep
7
8
  from typing import (
@@ -50,6 +51,7 @@ class HeaterShakerEmulator(AbstractEmulator):
50
51
  GCODE.CLOSE_LABWARE_LATCH.value: self._close_labware_latch,
51
52
  GCODE.GET_LABWARE_LATCH_STATE.value: self._get_labware_latch_state,
52
53
  GCODE.DEACTIVATE_HEATER.value: self._deactivate_heater,
54
+ GCODE.GET_ERROR_STATE.value: self._get_error_state,
53
55
  }
54
56
  self.reset()
55
57
 
@@ -60,7 +62,6 @@ class HeaterShakerEmulator(AbstractEmulator):
60
62
  return None if not joined else joined
61
63
 
62
64
  def reset(self) -> None:
63
-
64
65
  self._temperature = Temperature(
65
66
  per_tick=self._settings.temperature.degrees_per_tick,
66
67
  current=self._settings.temperature.starting,
@@ -145,6 +146,14 @@ class HeaterShakerEmulator(AbstractEmulator):
145
146
  self._temperature.deactivate(TEMPERATURE_ROOM)
146
147
  return "M106"
147
148
 
148
- @staticmethod
149
- def get_terminator() -> bytes:
149
+ def _get_error_state(self, command: Command) -> str:
150
+ return f"M411 {HS_ACK}M411"
151
+
152
+ def get_terminator(self) -> bytes:
150
153
  return b"\n"
154
+
155
+ def get_ack(self) -> bytes:
156
+ return HS_ACK.encode()
157
+
158
+ def get_autoack(self) -> bool:
159
+ return False
@@ -90,7 +90,7 @@ class Settings(BaseSettings):
90
90
  )
91
91
  thermocycler: ThermocyclerSettings = ThermocyclerSettings(
92
92
  serial_number="thermocycler_emulator",
93
- model="v02",
93
+ model="thermocyclerModuleV2",
94
94
  version="v1.1.0",
95
95
  lid_temperature=TemperatureModelSettings(),
96
96
  plate_temperature=TemperatureModelSettings(),
@@ -5,8 +5,15 @@ The purpose is to provide a fake backend that responds to GCODE commands.
5
5
 
6
6
  import logging
7
7
  from typing import Optional
8
- from opentrons.drivers.thermocycler.driver import GCODE
8
+ from opentrons.drivers.thermocycler.driver import (
9
+ GCODE,
10
+ TC_GEN2_ACK,
11
+ TC_GEN2_SERIAL_ACK,
12
+ TC_ACK as TC_GEN1_ACK,
13
+ SERIAL_ACK as TC_GEN1_SERIAL_ACK,
14
+ )
9
15
  from opentrons.drivers.types import ThermocyclerLidStatus
16
+ from opentrons.hardware_control.modules.types import ThermocyclerModuleModel
10
17
  from opentrons.hardware_control.emulation.parser import Parser, Command
11
18
  from opentrons.hardware_control.emulation.settings import ThermocyclerSettings
12
19
 
@@ -29,13 +36,31 @@ class ThermocyclerEmulator(AbstractEmulator):
29
36
  def __init__(self, parser: Parser, settings: ThermocyclerSettings) -> None:
30
37
  self._parser = parser
31
38
  self._settings = settings
39
+ # I hate this. These modules do not return anything like this for their actual versions
40
+ # (gen2 returns "Opentrons-thermocycler-gen2" for instance) and this is not what any of
41
+ # the settings anywhere use.
42
+ self._model = (
43
+ ThermocyclerModuleModel.THERMOCYCLER_V1
44
+ if settings.model in ["thermocyclerModuleV1", "v1", "v01"]
45
+ else ThermocyclerModuleModel.THERMOCYCLER_V2
46
+ )
47
+ self._terminator = (
48
+ TC_GEN1_SERIAL_ACK
49
+ if self._model is ThermocyclerModuleModel.THERMOCYCLER_V1
50
+ else TC_GEN2_SERIAL_ACK
51
+ )
52
+ self._ack = (
53
+ TC_GEN1_ACK
54
+ if self._model is ThermocyclerModuleModel.THERMOCYCLER_V1
55
+ else TC_GEN2_ACK
56
+ )
32
57
  self.reset()
33
58
 
34
59
  def handle(self, line: str) -> Optional[str]:
35
60
  """Handle a line"""
36
61
  results = (self._handle(c) for c in self._parser.parse(line))
37
- joined = " ".join(r for r in results if r)
38
- return None if not joined else joined
62
+ joined = " ".join(f"{r} {self._ack}" for r in results if r)
63
+ return self._ack if not joined else joined
39
64
 
40
65
  def reset(self) -> None:
41
66
  self._lid_temperature = Temperature(
@@ -50,6 +75,12 @@ class ThermocyclerEmulator(AbstractEmulator):
50
75
  self.plate_volume = util.OptionalValue[float]()
51
76
  self.plate_ramp_rate = util.OptionalValue[float]()
52
77
 
78
+ def _pref(self, command: Command) -> str:
79
+ if self._model is ThermocyclerModuleModel.THERMOCYCLER_V1:
80
+ return ""
81
+ else:
82
+ return f"{command.gcode} "
83
+
53
84
  def _handle(self, command: Command) -> Optional[str]: # noqa: C901
54
85
  """
55
86
  Handle a command.
@@ -62,7 +93,7 @@ class ThermocyclerEmulator(AbstractEmulator):
62
93
  elif command.gcode == GCODE.CLOSE_LID:
63
94
  self.lid_status = ThermocyclerLidStatus.CLOSED
64
95
  elif command.gcode == GCODE.GET_LID_STATUS:
65
- return f"Lid:{self.lid_status}"
96
+ return self._pref(command) + f"Lid:{self.lid_status}"
66
97
  elif command.gcode == GCODE.SET_LID_TEMP:
67
98
  temperature = command.params["S"]
68
99
  assert isinstance(
@@ -76,7 +107,7 @@ class ThermocyclerEmulator(AbstractEmulator):
76
107
  f"H:none Total_H:none"
77
108
  )
78
109
  self._lid_temperature.tick()
79
- return res
110
+ return self._pref(command) + res
80
111
  elif command.gcode == GCODE.EDIT_PID_PARAMS:
81
112
  pass
82
113
  elif command.gcode == GCODE.SET_PLATE_TEMP:
@@ -105,7 +136,7 @@ class ThermocyclerEmulator(AbstractEmulator):
105
136
  f"Total_H:{plate_total_hold_time} "
106
137
  )
107
138
  self._plate_temperature.tick()
108
- return res
139
+ return self._pref(command) + res
109
140
  elif command.gcode == GCODE.SET_RAMP_RATE:
110
141
  self.plate_ramp_rate.val = command.params["S"]
111
142
  elif command.gcode == GCODE.DEACTIVATE_ALL:
@@ -116,13 +147,34 @@ class ThermocyclerEmulator(AbstractEmulator):
116
147
  elif command.gcode == GCODE.DEACTIVATE_BLOCK:
117
148
  self._plate_temperature.deactivate(temperature=util.TEMPERATURE_ROOM)
118
149
  elif command.gcode == GCODE.DEVICE_INFO:
119
- return (
120
- f"serial:{self._settings.serial_number} "
121
- f"model:{self._settings.model} "
122
- f"version:{self._settings.version}"
123
- )
124
- return None
150
+ # the gen2 returns a completely different device info format than the
151
+ # gen1 which is pretty cool
152
+ if self._model == ThermocyclerModuleModel.THERMOCYCLER_V1:
153
+ return (
154
+ f"serial:{self._settings.serial_number} "
155
+ f"model:{self._settings.model} "
156
+ f"version:{self._settings.version}"
157
+ )
158
+ else:
159
+ return (
160
+ command.gcode
161
+ + " "
162
+ + (
163
+ f"FW:{self._settings.version} "
164
+ f"HW:{self._settings.model} "
165
+ f"SerialNo:{self._settings.serial_number}"
166
+ )
167
+ )
168
+ elif command.gcode == GCODE.GET_ERROR_STATE:
169
+ if self._model is ThermocyclerModuleModel.THERMOCYCLER_V2:
170
+ return self._pref(command) + self._ack + self._pref(command)
171
+ return self._pref(command)
172
+
173
+ def get_terminator(self) -> bytes:
174
+ return self._terminator.encode()
175
+
176
+ def get_ack(self) -> bytes:
177
+ return self._ack.encode()
125
178
 
126
- @staticmethod
127
- def get_terminator() -> bytes:
128
- return b"\r\n"
179
+ def get_autoack(self) -> bool:
180
+ return False
@@ -2,9 +2,11 @@ from __future__ import annotations
2
2
  import asyncio
3
3
  import logging
4
4
  import re
5
- from typing import TYPE_CHECKING, List, Optional, Union
5
+ from typing import TYPE_CHECKING, List, Optional, Union, Callable
6
6
  from glob import glob
7
7
 
8
+ from opentrons_shared_data.errors.exceptions import EnumeratedError
9
+
8
10
  from opentrons.config import IS_ROBOT, IS_LINUX
9
11
  from opentrons.drivers.rpi_drivers import types, interfaces, usb, usb_simulator
10
12
  from opentrons.hardware_control.emulation.module_server.helpers import (
@@ -19,8 +21,17 @@ from opentrons.hardware_control.modules.module_calibration import (
19
21
  from opentrons.hardware_control.modules.types import ModuleAtPort, ModuleType
20
22
  from opentrons.hardware_control.modules import SimulatingModuleAtPort
21
23
 
24
+
22
25
  from opentrons.types import Point
23
- from .types import AionotifyEvent, BoardRevision, OT3Mount, StatusBarUpdateEvent
26
+ from .types import (
27
+ AionotifyEvent,
28
+ BoardRevision,
29
+ OT3Mount,
30
+ StatusBarUpdateEvent,
31
+ HardwareEvent,
32
+ AsynchronousModuleErrorNotification,
33
+ ModuleDisconnectedNotification,
34
+ )
24
35
  from . import modules
25
36
 
26
37
  if TYPE_CHECKING:
@@ -51,10 +62,21 @@ class AttachedModulesControl:
51
62
  self,
52
63
  api: Union["API", "OT3API"],
53
64
  usb: interfaces.USBDriverInterface,
65
+ event_callback: Callable[[HardwareEvent], None],
54
66
  ) -> None:
55
67
  self._available_modules: List[modules.AbstractModule] = []
56
68
  self._api = api
57
69
  self._usb = usb
70
+ self._event_callback = event_callback
71
+ if not IS_ROBOT and not api.is_simulator:
72
+ # Start task that registers emulated modules.
73
+ self._emulation_listen_task: asyncio.Task[
74
+ None
75
+ ] | None = api.loop.create_task(
76
+ listen_module_connection(self.register_modules)
77
+ )
78
+ else:
79
+ self._emulation_listen_task = None
58
80
 
59
81
  def subscribe_to_api_event(self, module: modules.AbstractModule) -> None:
60
82
  self._api.add_status_bar_listener(module.event_listener)
@@ -64,22 +86,20 @@ class AttachedModulesControl:
64
86
  cls,
65
87
  api_instance: Union["API", "OT3API"],
66
88
  board_revision: BoardRevision,
89
+ event_callback: Callable[[HardwareEvent], None],
67
90
  ) -> AttachedModulesControl:
68
91
  usb_instance = (
69
92
  usb.USBBus(board_revision)
70
93
  if not api_instance.is_simulator and IS_ROBOT
71
94
  else usb_simulator.USBBusSimulator()
72
95
  )
73
- mc_instance = cls(api=api_instance, usb=usb_instance)
96
+ mc_instance = cls(
97
+ api=api_instance, usb=usb_instance, event_callback=event_callback
98
+ )
74
99
 
75
100
  if not api_instance.is_simulator:
76
101
  # Do an initial scan of modules.
77
102
  await mc_instance.register_modules(mc_instance.scan())
78
- if not IS_ROBOT:
79
- # Start task that registers emulated modules.
80
- api_instance.loop.create_task(
81
- listen_module_connection(mc_instance.register_modules)
82
- )
83
103
 
84
104
  return mc_instance
85
105
 
@@ -87,6 +107,37 @@ class AttachedModulesControl:
87
107
  def available_modules(self) -> List[modules.AbstractModule]:
88
108
  return self._available_modules
89
109
 
110
+ async def clean_up(self) -> None:
111
+ """Clean up all registered modules and emulator scanning tasks (if any)."""
112
+ for module in self._available_modules:
113
+ await module.cleanup()
114
+ if self._emulation_listen_task is not None:
115
+ self._emulation_listen_task.cancel("cleanup")
116
+ try:
117
+ await self._emulation_listen_task
118
+ except asyncio.CancelledError:
119
+ pass
120
+ except Exception:
121
+ log.exception("Exception cleaning up emulation listen task")
122
+ finally:
123
+ self._emulation_listen_task = None
124
+
125
+ async def register_simulated_module(
126
+ self,
127
+ simulated_usb_port: types.USBPort,
128
+ type: modules.ModuleType,
129
+ sim_model: str,
130
+ ) -> modules.AbstractModule:
131
+ """Register a simulated module."""
132
+ module = await self.build_module(
133
+ "", simulated_usb_port, type, sim_model, sim_serial_number=None
134
+ )
135
+ self._available_modules.append(module)
136
+ self._available_modules = sorted(
137
+ self._available_modules, key=modules.AbstractModule.sort_key
138
+ )
139
+ return module
140
+
90
141
  async def build_module(
91
142
  self,
92
143
  port: str,
@@ -105,6 +156,7 @@ class AttachedModulesControl:
105
156
  sim_model=sim_model,
106
157
  sim_serial_number=sim_serial_number,
107
158
  disconnected_callback=self._disconnected_callback,
159
+ error_callback=self._async_error_callback,
108
160
  )
109
161
  last_event = StatusBarUpdateEvent(
110
162
  self._api.get_status_bar_state(), self._api.get_status_bar_enabled()
@@ -113,13 +165,51 @@ class AttachedModulesControl:
113
165
  self.subscribe_to_api_event(mod)
114
166
  return mod
115
167
 
116
- def _disconnected_callback(self, port: str, serial: Optional[str]) -> None:
168
+ def _disconnected_callback(
169
+ self, model: str, port: str, serial: Optional[str]
170
+ ) -> None:
117
171
  """Used by the module to indicate that it was disconnected and should be deleted."""
118
172
  mod = ModuleAtPort(port=port, serial=serial, name="")
119
173
  asyncio.run_coroutine_threadsafe(
120
174
  self.unregister_modules([mod]),
121
175
  self._api.loop,
122
176
  )
177
+ try:
178
+ self._api.loop.call_soon(
179
+ self._event_callback,
180
+ ModuleDisconnectedNotification(
181
+ module_serial=serial,
182
+ module_model=modules.module_model_from_string(model),
183
+ port=port,
184
+ ),
185
+ )
186
+ except Exception:
187
+ log.exception(
188
+ f"Module disconnect callback for module {model} {serial} at {port} failed"
189
+ )
190
+
191
+ def _async_error_callback(
192
+ self,
193
+ exc: Exception,
194
+ model: str,
195
+ port: str,
196
+ serial: str | None,
197
+ ) -> None:
198
+ """Used by the module to indicate it saw an error from its data poller."""
199
+ try:
200
+ self._api.loop.call_soon(
201
+ self._event_callback,
202
+ AsynchronousModuleErrorNotification(
203
+ exception=EnumeratedError.ensure(exc),
204
+ module_serial=serial,
205
+ module_model=modules.module_model_from_string(model),
206
+ port=port,
207
+ ),
208
+ )
209
+ except Exception:
210
+ log.exception(
211
+ f"Async error callback for module {model} {serial} at {port} for exc {exc} failed"
212
+ )
123
213
 
124
214
  async def unregister_modules(
125
215
  self,
@@ -144,10 +234,15 @@ class AttachedModulesControl:
144
234
  for removed_mod in removed_modules:
145
235
  try:
146
236
  self._available_modules.remove(removed_mod)
237
+ # Important: this wants to be after the remove because this may trigger
238
+ # recursion back to here; we therefore want the module to already be
239
+ # removed so that the recursion terminates next loop
240
+ removed_mod.disconnected_callback()
147
241
  except ValueError:
148
- log.exception(
242
+ log.warning(
149
243
  f"Removed Module {removed_mod} not found in attached modules"
150
244
  )
245
+
151
246
  for removed_mod in removed_modules:
152
247
  log.info(
153
248
  f"Module {removed_mod.name()} detached from port {removed_mod.port}"
@@ -27,7 +27,9 @@ from .types import (
27
27
  LiveData,
28
28
  ModuleData,
29
29
  ModuleDataValidator,
30
+ module_model_from_string,
30
31
  )
32
+
31
33
  from .errors import (
32
34
  UpdateError,
33
35
  AbsorbanceReaderDisconnectedError,
@@ -66,4 +68,5 @@ __all__ = [
66
68
  "FlexStackerStatus",
67
69
  "PlatformState",
68
70
  "StackerAxisState",
71
+ "module_model_from_string",
69
72
  ]
@@ -21,6 +21,7 @@ from opentrons.hardware_control.poller import Poller, Reader
21
21
  from opentrons.hardware_control.modules import mod_abc
22
22
  from opentrons.hardware_control.modules.types import (
23
23
  ModuleDisconnectedCallback,
24
+ ModuleErrorCallback,
24
25
  ModuleType,
25
26
  AbsorbanceReaderStatus,
26
27
  LiveData,
@@ -104,12 +105,13 @@ class AbsorbanceReader(mod_abc.AbstractModule):
104
105
  port: str,
105
106
  usb_port: USBPort,
106
107
  hw_control_loop: asyncio.AbstractEventLoop,
107
- execution_manager: Optional[ExecutionManager] = None,
108
+ execution_manager: ExecutionManager,
109
+ disconnected_callback: ModuleDisconnectedCallback,
110
+ error_callback: ModuleErrorCallback,
108
111
  poll_interval_seconds: Optional[float] = None,
109
112
  simulating: bool = False,
110
113
  sim_model: Optional[str] = None,
111
114
  sim_serial_number: Optional[str] = None,
112
- disconnected_callback: ModuleDisconnectedCallback = None,
113
115
  ) -> "AbsorbanceReader":
114
116
  """
115
117
  Build and connect to an AbsorbanceReader
@@ -152,6 +154,7 @@ class AbsorbanceReader(mod_abc.AbstractModule):
152
154
  hw_control_loop=hw_control_loop,
153
155
  execution_manager=execution_manager,
154
156
  disconnected_callback=disconnected_callback,
157
+ error_callback=error_callback,
155
158
  )
156
159
 
157
160
  try:
@@ -170,8 +173,9 @@ class AbsorbanceReader(mod_abc.AbstractModule):
170
173
  poller: Poller,
171
174
  device_info: Mapping[str, str],
172
175
  hw_control_loop: asyncio.AbstractEventLoop,
173
- execution_manager: Optional[ExecutionManager] = None,
174
- disconnected_callback: ModuleDisconnectedCallback = None,
176
+ execution_manager: ExecutionManager,
177
+ disconnected_callback: ModuleDisconnectedCallback,
178
+ error_callback: ModuleErrorCallback,
175
179
  ) -> None:
176
180
  """
177
181
  Constructor
@@ -193,6 +197,7 @@ class AbsorbanceReader(mod_abc.AbstractModule):
193
197
  hw_control_loop=hw_control_loop,
194
198
  execution_manager=execution_manager,
195
199
  disconnected_callback=disconnected_callback,
200
+ error_callback=error_callback,
196
201
  )
197
202
  self._device_info = device_info
198
203
  self._reader = reader
@@ -371,3 +376,5 @@ class AbsorbanceReader(mod_abc.AbstractModule):
371
376
  self._error = str(error)
372
377
  if isinstance(error, AbsorbanceReaderDisconnectedError):
373
378
  self.disconnected_callback()
379
+ else:
380
+ self.error_callback(error)
@@ -44,6 +44,7 @@ from opentrons.hardware_control.modules.types import (
44
44
  FlexStackerStatus,
45
45
  HopperDoorState,
46
46
  LatchState,
47
+ ModuleErrorCallback,
47
48
  ModuleDisconnectedCallback,
48
49
  ModuleType,
49
50
  PlatformState,
@@ -215,12 +216,13 @@ class FlexStacker(mod_abc.AbstractModule):
215
216
  port: str,
216
217
  usb_port: USBPort,
217
218
  hw_control_loop: asyncio.AbstractEventLoop,
218
- execution_manager: Optional[ExecutionManager] = None,
219
- poll_interval_seconds: Optional[float] = None,
219
+ execution_manager: ExecutionManager,
220
+ disconnected_callback: ModuleDisconnectedCallback,
221
+ error_callback: ModuleErrorCallback,
222
+ poll_interval_seconds: float | None = None,
220
223
  simulating: bool = False,
221
224
  sim_model: Optional[str] = None,
222
225
  sim_serial_number: Optional[str] = None,
223
- disconnected_callback: ModuleDisconnectedCallback = None,
224
226
  ) -> "FlexStacker":
225
227
  """
226
228
  Build a FlexStacker
@@ -259,11 +261,9 @@ class FlexStacker(mod_abc.AbstractModule):
259
261
  hw_control_loop=hw_control_loop,
260
262
  execution_manager=execution_manager,
261
263
  disconnected_callback=disconnected_callback,
264
+ error_callback=error_callback,
262
265
  )
263
266
 
264
- # Set initialized callback
265
- reader.set_initialized_callback(module._initialized_callback)
266
-
267
267
  # Enable stallguard
268
268
  for axis, config in STALLGUARD_CONFIG.items():
269
269
  await driver.set_stallguard_threshold(
@@ -286,8 +286,9 @@ class FlexStacker(mod_abc.AbstractModule):
286
286
  poller: Poller,
287
287
  device_info: Mapping[str, str],
288
288
  hw_control_loop: asyncio.AbstractEventLoop,
289
- execution_manager: Optional[ExecutionManager] = None,
290
- disconnected_callback: ModuleDisconnectedCallback = None,
289
+ execution_manager: ExecutionManager,
290
+ disconnected_callback: ModuleDisconnectedCallback,
291
+ error_callback: ModuleErrorCallback,
291
292
  ):
292
293
  super().__init__(
293
294
  port=port,
@@ -295,6 +296,7 @@ class FlexStacker(mod_abc.AbstractModule):
295
296
  hw_control_loop=hw_control_loop,
296
297
  execution_manager=execution_manager,
297
298
  disconnected_callback=disconnected_callback,
299
+ error_callback=error_callback,
298
300
  )
299
301
  self._device_info = device_info
300
302
  self._driver = driver
@@ -304,12 +306,20 @@ class FlexStacker(mod_abc.AbstractModule):
304
306
  self._stacker_status = FlexStackerStatus.IDLE
305
307
  self._last_status_bar_event: Optional[StatusBarUpdateEvent] = None
306
308
  self._should_identify = False
309
+ # Set initialized callback
310
+ self._unsubscribe_init = reader.set_initialized_callback(
311
+ self._initialized_callback
312
+ )
313
+ self._unsubscribe_error = reader.set_error_callback(self._async_error_callback)
307
314
 
308
315
  async def _initialized_callback(self) -> None:
309
316
  """Called by the reader once the module is initialized."""
310
317
  if self._last_status_bar_event:
311
318
  await self._handle_status_bar_event(self._last_status_bar_event)
312
319
 
320
+ def _async_error_callback(self, exception: Exception) -> None:
321
+ self.error_callback(exception)
322
+
313
323
  async def cleanup(self) -> None:
314
324
  """Stop the poller task"""
315
325
  await self._poller.stop()
@@ -864,10 +874,27 @@ class FlexStackerReader(Reader):
864
874
  self.installation_detected = False
865
875
  self._refresh_state = False
866
876
  self._initialized_callback: Optional[Callable[[], Awaitable[None]]] = None
877
+ self._error_callback: Optional[Callable[[Exception], None]] = None
867
878
 
868
- def set_initialized_callback(self, callback: Callable[[], Awaitable[None]]) -> None:
879
+ def set_initialized_callback(
880
+ self, callback: Callable[[], Awaitable[None]]
881
+ ) -> Callable[[], None]:
869
882
  """Sets the callback used when done initializing the module."""
870
883
  self._initialized_callback = callback
884
+ return self._remove_init_callback
885
+
886
+ def _remove_init_callback(self) -> None:
887
+ self._initialized_callback = None
888
+
889
+ def set_error_callback(
890
+ self, error_callback: Callable[[Exception], None]
891
+ ) -> Callable[[], None]:
892
+ """Register a handler for asynchronous hardware errors."""
893
+ self._error_callback = error_callback
894
+ return self._remove_error_callback
895
+
896
+ def _remove_error_callback(self) -> None:
897
+ self._error_callback = None
871
898
 
872
899
  async def read(self) -> None:
873
900
  await self.get_door_closed()
@@ -942,6 +969,8 @@ class FlexStackerReader(Reader):
942
969
  if exception is None:
943
970
  self.error = None
944
971
  else:
972
+ if self._error_callback:
973
+ self._error_callback(exception)
945
974
  try:
946
975
  self.error = str(exception.args[0])
947
976
  except Exception: