opentrons 8.7.0a1__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 (119) 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/labware.py +8 -1
  34. opentrons/protocol_api/core/engine/module_core.py +4 -0
  35. opentrons/protocol_api/core/engine/protocol.py +18 -1
  36. opentrons/protocol_api/core/engine/tasks.py +35 -0
  37. opentrons/protocol_api/core/legacy/legacy_module_core.py +2 -0
  38. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
  39. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  40. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  41. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  42. opentrons/protocol_api/core/module.py +1 -0
  43. opentrons/protocol_api/core/protocol.py +11 -2
  44. opentrons/protocol_api/core/tasks.py +31 -0
  45. opentrons/protocol_api/module_contexts.py +1 -0
  46. opentrons/protocol_api/protocol_context.py +26 -4
  47. opentrons/protocol_api/robot_context.py +38 -21
  48. opentrons/protocol_api/tasks.py +48 -0
  49. opentrons/protocol_api/validation.py +6 -1
  50. opentrons/protocol_engine/actions/__init__.py +4 -2
  51. opentrons/protocol_engine/actions/actions.py +22 -9
  52. opentrons/protocol_engine/clients/sync_client.py +6 -7
  53. opentrons/protocol_engine/commands/__init__.py +42 -0
  54. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  55. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  56. opentrons/protocol_engine/commands/aspirate.py +1 -0
  57. opentrons/protocol_engine/commands/command.py +1 -0
  58. opentrons/protocol_engine/commands/command_unions.py +39 -0
  59. opentrons/protocol_engine/commands/create_timer.py +83 -0
  60. opentrons/protocol_engine/commands/dispense.py +1 -0
  61. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  62. opentrons/protocol_engine/commands/movement_common.py +2 -0
  63. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  64. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  65. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  66. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  67. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +17 -1
  68. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  69. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  70. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  71. opentrons/protocol_engine/errors/__init__.py +4 -0
  72. opentrons/protocol_engine/errors/exceptions.py +55 -0
  73. opentrons/protocol_engine/execution/__init__.py +2 -0
  74. opentrons/protocol_engine/execution/command_executor.py +8 -0
  75. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  76. opentrons/protocol_engine/execution/labware_movement.py +9 -12
  77. opentrons/protocol_engine/execution/movement.py +2 -0
  78. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  79. opentrons/protocol_engine/execution/run_control.py +8 -0
  80. opentrons/protocol_engine/execution/task_handler.py +157 -0
  81. opentrons/protocol_engine/protocol_engine.py +67 -33
  82. opentrons/protocol_engine/resources/__init__.py +2 -0
  83. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  84. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  85. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  86. opentrons/protocol_engine/state/_well_math.py +60 -18
  87. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  88. opentrons/protocol_engine/state/commands.py +7 -7
  89. opentrons/protocol_engine/state/geometry.py +204 -374
  90. opentrons/protocol_engine/state/labware.py +52 -102
  91. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  92. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
  93. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  94. opentrons/protocol_engine/state/modules.py +21 -8
  95. opentrons/protocol_engine/state/motion.py +44 -0
  96. opentrons/protocol_engine/state/state.py +14 -0
  97. opentrons/protocol_engine/state/state_summary.py +2 -0
  98. opentrons/protocol_engine/state/tasks.py +139 -0
  99. opentrons/protocol_engine/state/tips.py +177 -258
  100. opentrons/protocol_engine/state/update_types.py +16 -9
  101. opentrons/protocol_engine/types/__init__.py +9 -3
  102. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  103. opentrons/protocol_engine/types/instrument.py +8 -1
  104. opentrons/protocol_engine/types/labware.py +1 -13
  105. opentrons/protocol_engine/types/module.py +10 -0
  106. opentrons/protocol_engine/types/tasks.py +38 -0
  107. opentrons/protocol_engine/types/tip.py +9 -0
  108. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  109. opentrons/protocol_runner/run_orchestrator.py +18 -2
  110. opentrons/protocols/api_support/definitions.py +1 -1
  111. opentrons/protocols/api_support/types.py +2 -1
  112. opentrons/simulate.py +48 -15
  113. opentrons/system/camera.py +1 -1
  114. {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/METADATA +4 -4
  115. {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/RECORD +118 -105
  116. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  117. {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/WHEEL +0 -0
  118. {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/entry_points.txt +0 -0
  119. {opentrons-8.7.0a1.dist-info → opentrons-8.7.0a2.dist-info}/licenses/LICENSE +0 -0
opentrons/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '8.7.0a1'
32
- __version_tuple__ = version_tuple = (8, 7, 0, 'a1')
31
+ __version__ = version = '8.7.0a2'
32
+ __version_tuple__ = version_tuple = (8, 7, 0, 'a2')
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -55,6 +55,7 @@ class AbstractThermocyclerDriver(ABC):
55
55
  temp: float,
56
56
  hold_time: Optional[float] = None,
57
57
  volume: Optional[float] = None,
58
+ ramp_rate: Optional[float] = None,
58
59
  ) -> None:
59
60
  """Send set plate temperature command"""
60
61
  ...
@@ -226,6 +226,7 @@ class ThermocyclerDriver(AbstractThermocyclerDriver):
226
226
  temp: float,
227
227
  hold_time: Optional[float] = None,
228
228
  volume: Optional[float] = None,
229
+ ramp_rate: Optional[float] = None,
229
230
  ) -> None:
230
231
  """Send set plate temperature command"""
231
232
  temp = min(BLOCK_TARGET_MAX, max(BLOCK_TARGET_MIN, temp))
@@ -343,10 +344,38 @@ class ThermocyclerDriverV2(ThermocyclerDriver):
343
344
  """
344
345
  super().__init__(connection)
345
346
 
346
- async def set_ramp_rate(self, ramp_rate: float) -> None:
347
- """Send a set ramp rate command"""
348
- # This command is fully unsupported on TC Gen2
349
- return None
347
+ async def set_plate_temperature(
348
+ self,
349
+ temp: float,
350
+ hold_time: Optional[float] = None,
351
+ volume: Optional[float] = None,
352
+ ramp_rate: Optional[float] = None,
353
+ ) -> None:
354
+ """Send set plate temperature command"""
355
+ temp = min(BLOCK_TARGET_MAX, max(BLOCK_TARGET_MIN, temp))
356
+
357
+ c = (
358
+ CommandBuilder(terminator=TC_COMMAND_TERMINATOR)
359
+ .add_gcode(gcode=GCODE.SET_PLATE_TEMP)
360
+ .add_float(
361
+ prefix="S", value=temp, precision=utils.TC_GCODE_ROUNDING_PRECISION
362
+ )
363
+ )
364
+ if hold_time is not None:
365
+ c = c.add_float(
366
+ prefix="H", value=hold_time, precision=utils.TC_GCODE_ROUNDING_PRECISION
367
+ )
368
+ if volume is not None:
369
+ c = c.add_float(
370
+ prefix="V", value=volume, precision=utils.TC_GCODE_ROUNDING_PRECISION
371
+ )
372
+
373
+ if ramp_rate is not None:
374
+ c = c.add_float(
375
+ prefix="R", value=ramp_rate, precision=utils.TC_GCODE_ROUNDING_PRECISION
376
+ )
377
+
378
+ await self._connection.send_command(command=c, retries=DEFAULT_COMMAND_RETRIES)
350
379
 
351
380
  async def get_device_info(self) -> Dict[str, str]:
352
381
  """Send get device info command"""
@@ -67,10 +67,12 @@ class SimulatingDriver(AbstractThermocyclerDriver):
67
67
  temp: float,
68
68
  hold_time: Optional[float] = None,
69
69
  volume: Optional[float] = None,
70
+ ramp_rate: Optional[float] = None,
70
71
  ) -> None:
71
72
  self._plate_temperature.target = temp
72
73
  self._plate_temperature.current = temp
73
74
  self._plate_temperature.hold = 0
75
+ self._ramp_rate = ramp_rate
74
76
 
75
77
  @ensure_yield
76
78
  async def get_plate_temperature(self) -> PlateTemperature:
@@ -44,6 +44,7 @@ from .execution_manager import ExecutionManagerProvider
44
44
  from .pause_manager import PauseManager
45
45
  from .module_control import AttachedModulesControl
46
46
  from .types import (
47
+ AsynchronousModuleErrorNotification,
47
48
  Axis,
48
49
  CriticalPoint,
49
50
  DoorState,
@@ -51,6 +52,7 @@ from .types import (
51
52
  ErrorMessageNotification,
52
53
  HardwareEventHandler,
53
54
  HardwareAction,
55
+ HardwareEvent,
54
56
  MotionChecks,
55
57
  PauseType,
56
58
  StatusBarState,
@@ -167,6 +169,18 @@ class API(
167
169
  except Exception:
168
170
  mod_log.exception("Errored during door state event callback")
169
171
 
172
+ def _send_module_notification(self, event: HardwareEvent) -> None:
173
+ if not isinstance(event, AsynchronousModuleErrorNotification):
174
+ return
175
+ mod_log.info(
176
+ f"Forwarding module event {event.event} for {event.module_model} {event.module_serial} at {event.port}"
177
+ )
178
+ for cb in self._callbacks:
179
+ try:
180
+ cb(event)
181
+ except Exception:
182
+ mod_log.exception("Errored during module asynchronous callback")
183
+
170
184
  def _reset_last_mount(self) -> None:
171
185
  self._last_moved_mount = None
172
186
 
@@ -247,7 +261,9 @@ class API(
247
261
  )
248
262
  await api_instance.cache_instruments()
249
263
  module_controls = await AttachedModulesControl.build(
250
- api_instance, board_revision=backend.board_revision
264
+ api_instance,
265
+ board_revision=backend.board_revision,
266
+ event_callback=api_instance._send_module_notification,
251
267
  )
252
268
  backend.module_controls = module_controls
253
269
  checked_loop.create_task(backend.watch(loop=checked_loop))
@@ -306,7 +322,9 @@ class API(
306
322
  )
307
323
  await api_instance.cache_instruments()
308
324
  module_controls = await AttachedModulesControl.build(
309
- api_instance, board_revision=backend.board_revision
325
+ api_instance,
326
+ board_revision=backend.board_revision,
327
+ event_callback=api_instance._send_module_notification,
310
328
  )
311
329
  backend.module_controls = module_controls
312
330
  await backend.watch()
@@ -1312,9 +1330,10 @@ class API(
1312
1330
  self.is_simulator
1313
1331
  ), "Cannot build simulating module from non-simulating hardware control API"
1314
1332
 
1315
- return await self._backend.module_controls.build_module(
1316
- port="",
1317
- usb_port=USBPort(name="", port_number=1, port_group=PortGroup.MAIN),
1333
+ return await self._backend.module_controls.register_simulated_module(
1334
+ simulated_usb_port=USBPort(
1335
+ name="", port_number=1, port_group=PortGroup.MAIN
1336
+ ),
1318
1337
  type=modules.ModuleType.from_model(model),
1319
1338
  sim_model=model.value,
1320
1339
  )
@@ -360,13 +360,19 @@ class Controller:
360
360
  """Run a probe and return the new position dict"""
361
361
  return await self._smoothie_driver.probe_axis(axis, distance)
362
362
 
363
- async def clean_up(self) -> None:
363
+ async def clean_up(self) -> None: # noqa: C901
364
364
  try:
365
365
  loop = asyncio.get_event_loop()
366
366
  except RuntimeError:
367
367
  return
368
+ if hasattr(self, "_module_controls") and self._module_controls is not None:
369
+ await self._module_controls.clean_up()
368
370
  if hasattr(self, "_event_watcher"):
369
- if loop.is_running() and self._event_watcher:
371
+ if (
372
+ loop.is_running()
373
+ and self._event_watcher
374
+ and not self._event_watcher.closed
375
+ ):
370
376
  self._event_watcher.close()
371
377
  if hasattr(self, "gpio_chardev"):
372
378
  try:
@@ -1398,6 +1398,9 @@ class OT3Controller(FlexBackend):
1398
1398
  except RuntimeError:
1399
1399
  return
1400
1400
 
1401
+ if hasattr(self, "_module_controls") and self._module_controls is not None:
1402
+ await self._module_controls.clean_up()
1403
+
1401
1404
  if hasattr(self, "_event_watcher"):
1402
1405
  if (
1403
1406
  loop.is_running()
@@ -728,7 +728,8 @@ class OT3Simulator(FlexBackend):
728
728
  @ensure_yield
729
729
  async def clean_up(self) -> None:
730
730
  """Clean up."""
731
- pass
731
+ if hasattr(self, "_module_controls") and self._module_controls is not None:
732
+ await self._module_controls.clean_up()
732
733
 
733
734
  @staticmethod
734
735
  def _get_home_position() -> Dict[Axis, float]:
@@ -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")
@@ -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: