opentrons 8.7.0a6__py3-none-any.whl → 8.7.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.

Potentially problematic release.


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

Files changed (144) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/drivers/asyncio/communication/serial_connection.py +129 -52
  3. opentrons/drivers/heater_shaker/abstract.py +5 -0
  4. opentrons/drivers/heater_shaker/driver.py +10 -0
  5. opentrons/drivers/heater_shaker/simulator.py +4 -0
  6. opentrons/drivers/thermocycler/abstract.py +6 -0
  7. opentrons/drivers/thermocycler/driver.py +61 -10
  8. opentrons/drivers/thermocycler/simulator.py +6 -0
  9. opentrons/hardware_control/api.py +24 -5
  10. opentrons/hardware_control/backends/controller.py +8 -2
  11. opentrons/hardware_control/backends/ot3controller.py +3 -0
  12. opentrons/hardware_control/backends/ot3simulator.py +2 -1
  13. opentrons/hardware_control/backends/simulator.py +2 -1
  14. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  15. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  16. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  17. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  18. opentrons/hardware_control/emulation/settings.py +1 -1
  19. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  20. opentrons/hardware_control/module_control.py +82 -8
  21. opentrons/hardware_control/modules/__init__.py +3 -0
  22. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  23. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  24. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  25. opentrons/hardware_control/modules/magdeck.py +8 -4
  26. opentrons/hardware_control/modules/mod_abc.py +13 -5
  27. opentrons/hardware_control/modules/tempdeck.py +25 -5
  28. opentrons/hardware_control/modules/thermocycler.py +68 -11
  29. opentrons/hardware_control/modules/types.py +20 -1
  30. opentrons/hardware_control/modules/utils.py +11 -4
  31. opentrons/hardware_control/nozzle_manager.py +3 -0
  32. opentrons/hardware_control/ot3api.py +26 -5
  33. opentrons/hardware_control/poller.py +22 -8
  34. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  35. opentrons/hardware_control/types.py +31 -2
  36. opentrons/legacy_commands/module_commands.py +23 -0
  37. opentrons/legacy_commands/protocol_commands.py +20 -0
  38. opentrons/legacy_commands/types.py +80 -0
  39. opentrons/motion_planning/deck_conflict.py +17 -12
  40. opentrons/motion_planning/waypoints.py +15 -29
  41. opentrons/protocol_api/__init__.py +5 -1
  42. opentrons/protocol_api/_types.py +6 -1
  43. opentrons/protocol_api/core/common.py +3 -1
  44. opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
  45. opentrons/protocol_api/core/engine/labware.py +8 -1
  46. opentrons/protocol_api/core/engine/module_core.py +75 -8
  47. opentrons/protocol_api/core/engine/protocol.py +18 -1
  48. opentrons/protocol_api/core/engine/tasks.py +48 -0
  49. opentrons/protocol_api/core/engine/well.py +8 -0
  50. opentrons/protocol_api/core/legacy/legacy_module_core.py +24 -4
  51. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
  52. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  53. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  54. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  55. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  56. opentrons/protocol_api/core/module.py +37 -4
  57. opentrons/protocol_api/core/protocol.py +11 -2
  58. opentrons/protocol_api/core/tasks.py +31 -0
  59. opentrons/protocol_api/core/well.py +4 -0
  60. opentrons/protocol_api/labware.py +5 -0
  61. opentrons/protocol_api/module_contexts.py +117 -11
  62. opentrons/protocol_api/protocol_context.py +26 -4
  63. opentrons/protocol_api/robot_context.py +38 -21
  64. opentrons/protocol_api/tasks.py +48 -0
  65. opentrons/protocol_api/validation.py +6 -1
  66. opentrons/protocol_engine/actions/__init__.py +4 -2
  67. opentrons/protocol_engine/actions/actions.py +22 -9
  68. opentrons/protocol_engine/clients/sync_client.py +42 -7
  69. opentrons/protocol_engine/commands/__init__.py +42 -0
  70. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  71. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  72. opentrons/protocol_engine/commands/aspirate.py +1 -0
  73. opentrons/protocol_engine/commands/command.py +1 -0
  74. opentrons/protocol_engine/commands/command_unions.py +49 -0
  75. opentrons/protocol_engine/commands/create_timer.py +83 -0
  76. opentrons/protocol_engine/commands/dispense.py +1 -0
  77. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  78. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  79. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  80. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  81. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  82. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  83. opentrons/protocol_engine/commands/movement_common.py +2 -0
  84. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  85. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  86. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  87. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  88. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  89. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  90. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +40 -6
  91. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +29 -5
  92. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  93. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  94. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  95. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  96. opentrons/protocol_engine/errors/__init__.py +4 -0
  97. opentrons/protocol_engine/errors/exceptions.py +55 -0
  98. opentrons/protocol_engine/execution/__init__.py +2 -0
  99. opentrons/protocol_engine/execution/command_executor.py +8 -0
  100. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  101. opentrons/protocol_engine/execution/labware_movement.py +9 -12
  102. opentrons/protocol_engine/execution/movement.py +2 -0
  103. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  104. opentrons/protocol_engine/execution/run_control.py +8 -0
  105. opentrons/protocol_engine/execution/task_handler.py +157 -0
  106. opentrons/protocol_engine/protocol_engine.py +75 -34
  107. opentrons/protocol_engine/resources/__init__.py +2 -0
  108. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  109. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  110. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  111. opentrons/protocol_engine/state/_well_math.py +60 -18
  112. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  113. opentrons/protocol_engine/state/commands.py +14 -11
  114. opentrons/protocol_engine/state/geometry.py +213 -374
  115. opentrons/protocol_engine/state/labware.py +52 -102
  116. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  117. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
  118. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  119. opentrons/protocol_engine/state/modules.py +21 -8
  120. opentrons/protocol_engine/state/motion.py +44 -0
  121. opentrons/protocol_engine/state/state.py +14 -0
  122. opentrons/protocol_engine/state/state_summary.py +2 -0
  123. opentrons/protocol_engine/state/tasks.py +139 -0
  124. opentrons/protocol_engine/state/tips.py +177 -258
  125. opentrons/protocol_engine/state/update_types.py +16 -9
  126. opentrons/protocol_engine/types/__init__.py +9 -3
  127. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  128. opentrons/protocol_engine/types/instrument.py +8 -1
  129. opentrons/protocol_engine/types/labware.py +1 -13
  130. opentrons/protocol_engine/types/module.py +10 -0
  131. opentrons/protocol_engine/types/tasks.py +38 -0
  132. opentrons/protocol_engine/types/tip.py +9 -0
  133. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  134. opentrons/protocol_runner/run_orchestrator.py +18 -2
  135. opentrons/protocols/api_support/definitions.py +1 -1
  136. opentrons/protocols/api_support/types.py +2 -1
  137. opentrons/simulate.py +48 -15
  138. opentrons/system/camera.py +1 -1
  139. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/METADATA +4 -4
  140. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/RECORD +143 -127
  141. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  142. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/WHEEL +0 -0
  143. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/entry_points.txt +0 -0
  144. {opentrons-8.7.0a6.dist-info → opentrons-8.7.0a7.dist-info}/licenses/LICENSE +0 -0
@@ -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,16 @@ 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
+ )
24
34
  from . import modules
25
35
 
26
36
  if TYPE_CHECKING:
@@ -51,10 +61,21 @@ class AttachedModulesControl:
51
61
  self,
52
62
  api: Union["API", "OT3API"],
53
63
  usb: interfaces.USBDriverInterface,
64
+ event_callback: Callable[[HardwareEvent], None],
54
65
  ) -> None:
55
66
  self._available_modules: List[modules.AbstractModule] = []
56
67
  self._api = api
57
68
  self._usb = usb
69
+ self._event_callback = event_callback
70
+ if not IS_ROBOT and not api.is_simulator:
71
+ # Start task that registers emulated modules.
72
+ self._emulation_listen_task: asyncio.Task[
73
+ None
74
+ ] | None = api.loop.create_task(
75
+ listen_module_connection(self.register_modules)
76
+ )
77
+ else:
78
+ self._emulation_listen_task = None
58
79
 
59
80
  def subscribe_to_api_event(self, module: modules.AbstractModule) -> None:
60
81
  self._api.add_status_bar_listener(module.event_listener)
@@ -64,22 +85,20 @@ class AttachedModulesControl:
64
85
  cls,
65
86
  api_instance: Union["API", "OT3API"],
66
87
  board_revision: BoardRevision,
88
+ event_callback: Callable[[HardwareEvent], None],
67
89
  ) -> AttachedModulesControl:
68
90
  usb_instance = (
69
91
  usb.USBBus(board_revision)
70
92
  if not api_instance.is_simulator and IS_ROBOT
71
93
  else usb_simulator.USBBusSimulator()
72
94
  )
73
- mc_instance = cls(api=api_instance, usb=usb_instance)
95
+ mc_instance = cls(
96
+ api=api_instance, usb=usb_instance, event_callback=event_callback
97
+ )
74
98
 
75
99
  if not api_instance.is_simulator:
76
100
  # Do an initial scan of modules.
77
101
  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
102
 
84
103
  return mc_instance
85
104
 
@@ -87,6 +106,37 @@ class AttachedModulesControl:
87
106
  def available_modules(self) -> List[modules.AbstractModule]:
88
107
  return self._available_modules
89
108
 
109
+ async def clean_up(self) -> None:
110
+ """Clean up all registered modules and emulator scanning tasks (if any)."""
111
+ for module in self._available_modules:
112
+ await module.cleanup()
113
+ if self._emulation_listen_task is not None:
114
+ self._emulation_listen_task.cancel("cleanup")
115
+ try:
116
+ await self._emulation_listen_task
117
+ except asyncio.CancelledError:
118
+ pass
119
+ except Exception:
120
+ log.exception("Exception cleaning up emulation listen task")
121
+ finally:
122
+ self._emulation_listen_task = None
123
+
124
+ async def register_simulated_module(
125
+ self,
126
+ simulated_usb_port: types.USBPort,
127
+ type: modules.ModuleType,
128
+ sim_model: str,
129
+ ) -> modules.AbstractModule:
130
+ """Register a simulated module."""
131
+ module = await self.build_module(
132
+ "", simulated_usb_port, type, sim_model, sim_serial_number=None
133
+ )
134
+ self._available_modules.append(module)
135
+ self._available_modules = sorted(
136
+ self._available_modules, key=modules.AbstractModule.sort_key
137
+ )
138
+ return module
139
+
90
140
  async def build_module(
91
141
  self,
92
142
  port: str,
@@ -105,6 +155,7 @@ class AttachedModulesControl:
105
155
  sim_model=sim_model,
106
156
  sim_serial_number=sim_serial_number,
107
157
  disconnected_callback=self._disconnected_callback,
158
+ error_callback=self._async_error_callback,
108
159
  )
109
160
  last_event = StatusBarUpdateEvent(
110
161
  self._api.get_status_bar_state(), self._api.get_status_bar_enabled()
@@ -121,6 +172,29 @@ class AttachedModulesControl:
121
172
  self._api.loop,
122
173
  )
123
174
 
175
+ def _async_error_callback(
176
+ self,
177
+ exc: Exception,
178
+ model: str,
179
+ port: str,
180
+ serial: str | None,
181
+ ) -> None:
182
+ """Used by the module to indicate it saw an error from its data poller."""
183
+ try:
184
+ self._api.loop.call_soon(
185
+ self._event_callback,
186
+ AsynchronousModuleErrorNotification(
187
+ exception=EnumeratedError.ensure(exc),
188
+ module_serial=serial,
189
+ module_model=modules.module_model_from_string(model),
190
+ port=port,
191
+ ),
192
+ )
193
+ except Exception:
194
+ log.exception(
195
+ f"Async error callback for module {model} {serial} at {port} for exc {exc} failed"
196
+ )
197
+
124
198
  async def unregister_modules(
125
199
  self,
126
200
  mods_at_ports: Union[
@@ -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:
@@ -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.