opentrons 8.1.0a0__py2.py3-none-any.whl → 8.2.0__py2.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 (238) hide show
  1. opentrons/cli/analyze.py +71 -7
  2. opentrons/config/__init__.py +9 -0
  3. opentrons/config/advanced_settings.py +22 -0
  4. opentrons/config/defaults_ot3.py +14 -36
  5. opentrons/config/feature_flags.py +4 -0
  6. opentrons/config/types.py +6 -17
  7. opentrons/drivers/absorbance_reader/abstract.py +27 -3
  8. opentrons/drivers/absorbance_reader/async_byonoy.py +208 -154
  9. opentrons/drivers/absorbance_reader/driver.py +24 -15
  10. opentrons/drivers/absorbance_reader/hid_protocol.py +79 -50
  11. opentrons/drivers/absorbance_reader/simulator.py +32 -6
  12. opentrons/drivers/types.py +23 -1
  13. opentrons/execute.py +2 -2
  14. opentrons/hardware_control/api.py +18 -10
  15. opentrons/hardware_control/backends/controller.py +3 -2
  16. opentrons/hardware_control/backends/flex_protocol.py +11 -5
  17. opentrons/hardware_control/backends/ot3controller.py +18 -50
  18. opentrons/hardware_control/backends/ot3simulator.py +7 -6
  19. opentrons/hardware_control/backends/ot3utils.py +1 -0
  20. opentrons/hardware_control/instruments/ot2/pipette_handler.py +22 -82
  21. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -2
  22. opentrons/hardware_control/module_control.py +43 -2
  23. opentrons/hardware_control/modules/__init__.py +7 -1
  24. opentrons/hardware_control/modules/absorbance_reader.py +232 -83
  25. opentrons/hardware_control/modules/errors.py +7 -0
  26. opentrons/hardware_control/modules/heater_shaker.py +8 -3
  27. opentrons/hardware_control/modules/magdeck.py +12 -3
  28. opentrons/hardware_control/modules/mod_abc.py +27 -2
  29. opentrons/hardware_control/modules/tempdeck.py +15 -7
  30. opentrons/hardware_control/modules/thermocycler.py +69 -3
  31. opentrons/hardware_control/modules/types.py +11 -5
  32. opentrons/hardware_control/modules/update.py +11 -5
  33. opentrons/hardware_control/modules/utils.py +3 -1
  34. opentrons/hardware_control/ot3_calibration.py +6 -6
  35. opentrons/hardware_control/ot3api.py +131 -94
  36. opentrons/hardware_control/poller.py +15 -11
  37. opentrons/hardware_control/protocols/__init__.py +1 -7
  38. opentrons/hardware_control/protocols/instrument_configurer.py +14 -2
  39. opentrons/hardware_control/protocols/liquid_handler.py +5 -0
  40. opentrons/hardware_control/protocols/position_estimator.py +3 -1
  41. opentrons/hardware_control/types.py +2 -0
  42. opentrons/legacy_commands/helpers.py +8 -2
  43. opentrons/motion_planning/__init__.py +2 -0
  44. opentrons/motion_planning/waypoints.py +32 -0
  45. opentrons/protocol_api/__init__.py +2 -1
  46. opentrons/protocol_api/_liquid.py +87 -1
  47. opentrons/protocol_api/_parameter_context.py +10 -1
  48. opentrons/protocol_api/core/engine/deck_conflict.py +0 -297
  49. opentrons/protocol_api/core/engine/instrument.py +29 -25
  50. opentrons/protocol_api/core/engine/labware.py +20 -4
  51. opentrons/protocol_api/core/engine/module_core.py +166 -17
  52. opentrons/protocol_api/core/engine/pipette_movement_conflict.py +362 -0
  53. opentrons/protocol_api/core/engine/protocol.py +30 -2
  54. opentrons/protocol_api/core/instrument.py +2 -0
  55. opentrons/protocol_api/core/labware.py +4 -0
  56. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +2 -0
  57. opentrons/protocol_api/core/legacy/legacy_labware_core.py +5 -0
  58. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +6 -2
  59. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +2 -0
  60. opentrons/protocol_api/core/module.py +22 -4
  61. opentrons/protocol_api/core/protocol.py +6 -2
  62. opentrons/protocol_api/instrument_context.py +52 -20
  63. opentrons/protocol_api/labware.py +13 -1
  64. opentrons/protocol_api/module_contexts.py +115 -17
  65. opentrons/protocol_api/protocol_context.py +49 -5
  66. opentrons/protocol_api/validation.py +5 -3
  67. opentrons/protocol_engine/__init__.py +10 -9
  68. opentrons/protocol_engine/actions/__init__.py +3 -0
  69. opentrons/protocol_engine/actions/actions.py +30 -25
  70. opentrons/protocol_engine/actions/get_state_update.py +38 -0
  71. opentrons/protocol_engine/clients/sync_client.py +1 -1
  72. opentrons/protocol_engine/clients/transports.py +1 -1
  73. opentrons/protocol_engine/commands/__init__.py +0 -4
  74. opentrons/protocol_engine/commands/absorbance_reader/__init__.py +41 -11
  75. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +148 -0
  76. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +65 -9
  77. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +148 -0
  78. opentrons/protocol_engine/commands/absorbance_reader/read.py +200 -0
  79. opentrons/protocol_engine/commands/aspirate.py +29 -16
  80. opentrons/protocol_engine/commands/aspirate_in_place.py +33 -16
  81. opentrons/protocol_engine/commands/blow_out.py +63 -14
  82. opentrons/protocol_engine/commands/blow_out_in_place.py +55 -13
  83. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +2 -5
  84. opentrons/protocol_engine/commands/calibration/calibrate_module.py +3 -4
  85. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +2 -5
  86. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +6 -4
  87. opentrons/protocol_engine/commands/command.py +31 -18
  88. opentrons/protocol_engine/commands/command_unions.py +37 -24
  89. opentrons/protocol_engine/commands/comment.py +5 -3
  90. opentrons/protocol_engine/commands/configure_for_volume.py +11 -14
  91. opentrons/protocol_engine/commands/configure_nozzle_layout.py +9 -15
  92. opentrons/protocol_engine/commands/custom.py +5 -3
  93. opentrons/protocol_engine/commands/dispense.py +42 -20
  94. opentrons/protocol_engine/commands/dispense_in_place.py +32 -14
  95. opentrons/protocol_engine/commands/drop_tip.py +70 -16
  96. opentrons/protocol_engine/commands/drop_tip_in_place.py +59 -13
  97. opentrons/protocol_engine/commands/get_tip_presence.py +5 -3
  98. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +6 -6
  99. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +6 -6
  100. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +6 -6
  101. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +8 -6
  102. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +8 -4
  103. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +6 -4
  104. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +6 -6
  105. opentrons/protocol_engine/commands/home.py +11 -5
  106. opentrons/protocol_engine/commands/liquid_probe.py +146 -88
  107. opentrons/protocol_engine/commands/load_labware.py +28 -5
  108. opentrons/protocol_engine/commands/load_liquid.py +18 -7
  109. opentrons/protocol_engine/commands/load_module.py +4 -6
  110. opentrons/protocol_engine/commands/load_pipette.py +18 -17
  111. opentrons/protocol_engine/commands/magnetic_module/disengage.py +6 -6
  112. opentrons/protocol_engine/commands/magnetic_module/engage.py +6 -4
  113. opentrons/protocol_engine/commands/move_labware.py +155 -23
  114. opentrons/protocol_engine/commands/move_relative.py +15 -3
  115. opentrons/protocol_engine/commands/move_to_addressable_area.py +29 -4
  116. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +13 -4
  117. opentrons/protocol_engine/commands/move_to_coordinates.py +11 -5
  118. opentrons/protocol_engine/commands/move_to_well.py +37 -10
  119. opentrons/protocol_engine/commands/pick_up_tip.py +51 -30
  120. opentrons/protocol_engine/commands/pipetting_common.py +47 -16
  121. opentrons/protocol_engine/commands/prepare_to_aspirate.py +62 -15
  122. opentrons/protocol_engine/commands/reload_labware.py +13 -4
  123. opentrons/protocol_engine/commands/retract_axis.py +6 -3
  124. opentrons/protocol_engine/commands/save_position.py +2 -3
  125. opentrons/protocol_engine/commands/set_rail_lights.py +5 -3
  126. opentrons/protocol_engine/commands/set_status_bar.py +5 -3
  127. opentrons/protocol_engine/commands/temperature_module/deactivate.py +6 -4
  128. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +3 -4
  129. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +6 -6
  130. opentrons/protocol_engine/commands/thermocycler/__init__.py +19 -0
  131. opentrons/protocol_engine/commands/thermocycler/close_lid.py +8 -8
  132. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +6 -4
  133. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +6 -4
  134. opentrons/protocol_engine/commands/thermocycler/open_lid.py +8 -4
  135. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +165 -0
  136. opentrons/protocol_engine/commands/thermocycler/run_profile.py +6 -6
  137. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +3 -4
  138. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +3 -4
  139. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +6 -4
  140. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +6 -4
  141. opentrons/protocol_engine/commands/touch_tip.py +19 -7
  142. opentrons/protocol_engine/commands/unsafe/__init__.py +30 -0
  143. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +6 -4
  144. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +12 -4
  145. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +5 -3
  146. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +208 -0
  147. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +77 -0
  148. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +10 -4
  149. opentrons/protocol_engine/commands/verify_tip_presence.py +5 -5
  150. opentrons/protocol_engine/commands/wait_for_duration.py +5 -3
  151. opentrons/protocol_engine/commands/wait_for_resume.py +5 -3
  152. opentrons/protocol_engine/create_protocol_engine.py +60 -10
  153. opentrons/protocol_engine/engine_support.py +2 -1
  154. opentrons/protocol_engine/error_recovery_policy.py +14 -3
  155. opentrons/protocol_engine/errors/__init__.py +20 -0
  156. opentrons/protocol_engine/errors/error_occurrence.py +8 -3
  157. opentrons/protocol_engine/errors/exceptions.py +127 -2
  158. opentrons/protocol_engine/execution/__init__.py +2 -0
  159. opentrons/protocol_engine/execution/command_executor.py +22 -13
  160. opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
  161. opentrons/protocol_engine/execution/door_watcher.py +1 -1
  162. opentrons/protocol_engine/execution/equipment.py +2 -1
  163. opentrons/protocol_engine/execution/error_recovery_hardware_state_synchronizer.py +101 -0
  164. opentrons/protocol_engine/execution/gantry_mover.py +4 -2
  165. opentrons/protocol_engine/execution/hardware_stopper.py +3 -3
  166. opentrons/protocol_engine/execution/heater_shaker_movement_flagger.py +1 -4
  167. opentrons/protocol_engine/execution/labware_movement.py +73 -22
  168. opentrons/protocol_engine/execution/movement.py +17 -7
  169. opentrons/protocol_engine/execution/pipetting.py +7 -4
  170. opentrons/protocol_engine/execution/queue_worker.py +6 -2
  171. opentrons/protocol_engine/execution/run_control.py +1 -1
  172. opentrons/protocol_engine/execution/thermocycler_movement_flagger.py +1 -1
  173. opentrons/protocol_engine/execution/thermocycler_plate_lifter.py +2 -1
  174. opentrons/protocol_engine/execution/tip_handler.py +77 -43
  175. opentrons/protocol_engine/notes/__init__.py +14 -2
  176. opentrons/protocol_engine/notes/notes.py +18 -1
  177. opentrons/protocol_engine/plugins.py +1 -1
  178. opentrons/protocol_engine/protocol_engine.py +47 -31
  179. opentrons/protocol_engine/resources/__init__.py +2 -0
  180. opentrons/protocol_engine/resources/deck_data_provider.py +19 -5
  181. opentrons/protocol_engine/resources/file_provider.py +161 -0
  182. opentrons/protocol_engine/resources/fixture_validation.py +11 -1
  183. opentrons/protocol_engine/resources/labware_validation.py +10 -0
  184. opentrons/protocol_engine/state/__init__.py +0 -70
  185. opentrons/protocol_engine/state/addressable_areas.py +1 -1
  186. opentrons/protocol_engine/state/command_history.py +21 -2
  187. opentrons/protocol_engine/state/commands.py +110 -31
  188. opentrons/protocol_engine/state/files.py +59 -0
  189. opentrons/protocol_engine/state/frustum_helpers.py +440 -0
  190. opentrons/protocol_engine/state/geometry.py +445 -59
  191. opentrons/protocol_engine/state/labware.py +264 -84
  192. opentrons/protocol_engine/state/liquids.py +1 -1
  193. opentrons/protocol_engine/state/module_substates/absorbance_reader_substate.py +21 -3
  194. opentrons/protocol_engine/state/modules.py +145 -90
  195. opentrons/protocol_engine/state/motion.py +33 -14
  196. opentrons/protocol_engine/state/pipettes.py +157 -317
  197. opentrons/protocol_engine/state/state.py +30 -1
  198. opentrons/protocol_engine/state/state_summary.py +3 -0
  199. opentrons/protocol_engine/state/tips.py +69 -114
  200. opentrons/protocol_engine/state/update_types.py +424 -0
  201. opentrons/protocol_engine/state/wells.py +236 -0
  202. opentrons/protocol_engine/types.py +90 -0
  203. opentrons/protocol_reader/file_format_validator.py +83 -15
  204. opentrons/protocol_runner/json_translator.py +21 -5
  205. opentrons/protocol_runner/legacy_command_mapper.py +27 -6
  206. opentrons/protocol_runner/legacy_context_plugin.py +27 -71
  207. opentrons/protocol_runner/protocol_runner.py +6 -3
  208. opentrons/protocol_runner/run_orchestrator.py +41 -6
  209. opentrons/protocols/advanced_control/mix.py +3 -5
  210. opentrons/protocols/advanced_control/transfers.py +125 -56
  211. opentrons/protocols/api_support/constants.py +1 -1
  212. opentrons/protocols/api_support/definitions.py +1 -1
  213. opentrons/protocols/api_support/labware_like.py +4 -4
  214. opentrons/protocols/api_support/tip_tracker.py +2 -2
  215. opentrons/protocols/api_support/types.py +15 -2
  216. opentrons/protocols/api_support/util.py +30 -42
  217. opentrons/protocols/duration/errors.py +1 -1
  218. opentrons/protocols/duration/estimator.py +50 -29
  219. opentrons/protocols/execution/dev_types.py +2 -2
  220. opentrons/protocols/execution/execute_json_v4.py +15 -10
  221. opentrons/protocols/execution/execute_python.py +8 -3
  222. opentrons/protocols/geometry/planning.py +12 -12
  223. opentrons/protocols/labware.py +17 -33
  224. opentrons/protocols/parameters/csv_parameter_interface.py +3 -1
  225. opentrons/simulate.py +3 -3
  226. opentrons/types.py +30 -3
  227. opentrons/util/logging_config.py +34 -0
  228. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/METADATA +5 -4
  229. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/RECORD +235 -223
  230. opentrons/protocol_engine/commands/absorbance_reader/measure.py +0 -94
  231. opentrons/protocol_engine/commands/configuring_common.py +0 -26
  232. opentrons/protocol_runner/thread_async_queue.py +0 -174
  233. /opentrons/protocol_engine/state/{abstract_store.py → _abstract_store.py} +0 -0
  234. /opentrons/protocol_engine/state/{move_types.py → _move_types.py} +0 -0
  235. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/LICENSE +0 -0
  236. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/WHEEL +0 -0
  237. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.1.0a0.dist-info → opentrons-8.2.0.dist-info}/top_level.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  import asyncio
2
- from typing import Optional, Mapping, List, Dict, Any, Tuple
2
+ import logging
3
+ from typing import Any, Callable, Dict, Optional, Mapping, List, Tuple
3
4
 
4
5
  from opentrons.drivers.rpi_drivers.types import USBPort
5
6
  from opentrons.drivers.absorbance_reader import (
@@ -11,23 +12,84 @@ from opentrons.drivers.types import (
11
12
  AbsorbanceReaderLidStatus,
12
13
  AbsorbanceReaderPlatePresence,
13
14
  AbsorbanceReaderDeviceState,
15
+ ABSMeasurementMode,
16
+ ABSMeasurementConfig,
14
17
  )
15
18
 
16
19
  from opentrons.hardware_control.execution_manager import ExecutionManager
17
- from opentrons.hardware_control.poller import Reader
20
+ from opentrons.hardware_control.poller import Poller, Reader
18
21
  from opentrons.hardware_control.modules import mod_abc
19
22
  from opentrons.hardware_control.modules.types import (
23
+ ModuleDisconnectedCallback,
20
24
  ModuleType,
21
25
  AbsorbanceReaderStatus,
22
26
  LiveData,
23
27
  UploadFunction,
24
28
  )
29
+ from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnectedError
25
30
 
31
+ log = logging.getLogger(__name__)
26
32
 
27
- async def upload_func_placeholder(
28
- dfu_serial: str, firmware_file_path: str, kwargs: Dict[str, Any]
29
- ) -> Tuple[bool, str]:
30
- return False, "Not implemented"
33
+
34
+ POLLING_FREQUENCY_SEC = 2.0
35
+ SIM_POLLING_FREQUENCY_SEC = POLLING_FREQUENCY_SEC / 50.0
36
+
37
+
38
+ class AbsorbanceReaderReader(Reader):
39
+ """Read data from the Absorbance Reader.
40
+
41
+ Args:
42
+ driver: A connected Absorbance Reader driver.
43
+ """
44
+
45
+ device_state: AbsorbanceReaderDeviceState
46
+ lid_status: AbsorbanceReaderLidStatus
47
+ plate_presence: AbsorbanceReaderPlatePresence
48
+ supported_wavelengths: List[int]
49
+ error: Optional[str]
50
+
51
+ def __init__(self, driver: AbstractAbsorbanceReaderDriver) -> None:
52
+ self.device_state = AbsorbanceReaderDeviceState.UNKNOWN
53
+ self.lid_status = AbsorbanceReaderLidStatus.UNKNOWN
54
+ self.plate_presence = AbsorbanceReaderPlatePresence.UNKNOWN
55
+ self.supported_wavelengths = []
56
+ self.uptime = 0
57
+ self._driver = driver
58
+ self._handle_error: Optional[Callable[[Exception], None]] = None
59
+
60
+ async def read(self) -> None:
61
+ await self.get_lid_status()
62
+ await self.get_device_status()
63
+ await self.get_plate_presence()
64
+ if not self.supported_wavelengths:
65
+ await self.get_supported_wavelengths()
66
+
67
+ async def get_device_status(self) -> None:
68
+ """Get the Absorbance Reader's current status."""
69
+ self.device_state = await self._driver.get_status()
70
+
71
+ async def get_device_uptime(self) -> None:
72
+ """Get the device uptime in seconds."""
73
+ self.uptime = await self._driver.get_uptime()
74
+
75
+ async def get_supported_wavelengths(self) -> None:
76
+ """Get the Absorbance Reader's supported wavelengths."""
77
+ self.supported_wavelengths = await self._driver.get_available_wavelengths()
78
+
79
+ async def get_lid_status(self) -> None:
80
+ """Get the Absorbance Reader's lid status."""
81
+ self.lid_status = await self._driver.get_lid_status()
82
+
83
+ async def get_plate_presence(self) -> None:
84
+ """Get the Absorbance Reader's plate presence."""
85
+ self.plate_presence = await self._driver.get_plate_presence()
86
+
87
+ def on_error(self, exception: Exception) -> None:
88
+ if self._handle_error is not None:
89
+ self._handle_error(exception)
90
+
91
+ def register_error_handler(self, handle_error: Callable[[Exception], None]) -> None:
92
+ self._handle_error = handle_error
31
93
 
32
94
 
33
95
  class AbsorbanceReader(mod_abc.AbstractModule):
@@ -46,24 +108,56 @@ class AbsorbanceReader(mod_abc.AbstractModule):
46
108
  simulating: bool = False,
47
109
  sim_model: Optional[str] = None,
48
110
  sim_serial_number: Optional[str] = None,
111
+ disconnected_callback: ModuleDisconnectedCallback = None,
49
112
  ) -> "AbsorbanceReader":
50
- """Create an AbsorbanceReader."""
113
+ """
114
+ Build and connect to an AbsorbanceReader
115
+
116
+ Args:
117
+ port: The port to connect to
118
+ usb_port: USB Port
119
+ execution_manager: Execution manager.
120
+ hw_control_loop: The event loop running in the hardware control thread.
121
+ poll_interval_seconds: Poll interval override.
122
+ simulating: whether to build a simulating driver
123
+ sim_model: The model name used by simulator
124
+ sim_serial_number: The serial number used by simulator
125
+ disconnected_callback: Callback to inform the module controller that the device was disconnected
126
+
127
+ Returns:
128
+ AbsorbanceReader instance.
129
+
130
+ """
51
131
  driver: AbstractAbsorbanceReaderDriver
52
132
  if not simulating:
53
133
  driver = await AbsorbanceReaderDriver.create(
54
134
  port, usb_port, hw_control_loop
55
135
  )
136
+ await driver.connect()
137
+ poll_interval_seconds = poll_interval_seconds or POLLING_FREQUENCY_SEC
56
138
  else:
57
- driver = SimulatingDriver(serial_number=sim_serial_number)
139
+ driver = SimulatingDriver(model=sim_model, serial_number=sim_serial_number)
140
+ poll_interval_seconds = poll_interval_seconds or SIM_POLLING_FREQUENCY_SEC
141
+
142
+ reader = AbsorbanceReaderReader(driver=driver)
143
+ poller = Poller(reader=reader, interval=poll_interval_seconds)
58
144
  module = cls(
59
145
  port=port,
60
146
  usb_port=usb_port,
61
- device_info=await driver.get_device_info(),
62
- execution_manager=execution_manager,
63
147
  driver=driver,
148
+ reader=reader,
149
+ poller=poller,
150
+ device_info=await driver.get_device_info(),
64
151
  hw_control_loop=hw_control_loop,
152
+ execution_manager=execution_manager,
153
+ disconnected_callback=disconnected_callback,
65
154
  )
66
- await module.setup()
155
+
156
+ try:
157
+ await poller.start()
158
+ except Exception:
159
+ log.exception(f"First read of AbsorbanceReader on port {port} failed")
160
+
67
161
  return module
68
162
 
69
163
  def __init__(
@@ -71,33 +165,74 @@ class AbsorbanceReader(mod_abc.AbstractModule):
71
165
  port: str,
72
166
  usb_port: USBPort,
73
167
  driver: AbstractAbsorbanceReaderDriver,
168
+ reader: AbsorbanceReaderReader,
169
+ poller: Poller,
74
170
  device_info: Mapping[str, str],
75
171
  execution_manager: ExecutionManager,
76
172
  hw_control_loop: asyncio.AbstractEventLoop,
173
+ disconnected_callback: ModuleDisconnectedCallback = None,
77
174
  ) -> None:
78
- super().__init__(port, usb_port, execution_manager, hw_control_loop)
175
+ """
176
+ Constructor
177
+
178
+ Args:
179
+ port: The port the absorbance is connected to.
180
+ usb_port: The USB port.
181
+ execution_manager: The hardware execution manager.
182
+ driver: The Absorbance driver.
183
+ reader: An interface to read data from the Absorbance Reader.
184
+ poller: A poll controller for reads.
185
+ device_info: The Absorbance device info.
186
+ hw_control_loop: The event loop running in the hardware control thread.
187
+ """
79
188
  self._driver = driver
189
+ super().__init__(
190
+ port=port,
191
+ usb_port=usb_port,
192
+ hw_control_loop=hw_control_loop,
193
+ execution_manager=execution_manager,
194
+ disconnected_callback=disconnected_callback,
195
+ )
80
196
  self._device_info = device_info
81
-
82
- async def deactivate(self, must_be_running: bool = True) -> None:
83
- """Deactivate the module.
84
-
85
- Contains an override to the `wait_for_is_running` step in cases where the
86
- module must be deactivated regardless of context."""
87
- await self._driver.disconnect()
197
+ self._reader = reader
198
+ self._poller = poller
199
+ self._measurement_config: Optional[ABSMeasurementConfig] = None
200
+ self._device_status = AbsorbanceReaderStatus.IDLE
201
+ self._error: Optional[str] = None
202
+ self._reader.register_error_handler(self._enter_error_state)
88
203
 
89
204
  @property
90
205
  def status(self) -> AbsorbanceReaderStatus:
91
- """Return some string describing status."""
92
- return AbsorbanceReaderStatus.IDLE
206
+ """Return some string describing the device status."""
207
+ state = self._reader.device_state
208
+ if state not in [
209
+ AbsorbanceReaderDeviceState.UNKNOWN,
210
+ AbsorbanceReaderDeviceState.OK,
211
+ ]:
212
+ return AbsorbanceReaderStatus.ERROR
213
+ return self._device_status
93
214
 
94
215
  @property
95
216
  def lid_status(self) -> AbsorbanceReaderLidStatus:
96
- return AbsorbanceReaderLidStatus.UNKNOWN
217
+ return self._reader.lid_status
97
218
 
98
219
  @property
99
220
  def plate_presence(self) -> AbsorbanceReaderPlatePresence:
100
- return AbsorbanceReaderPlatePresence.UNKNOWN
221
+ return self._reader.plate_presence
222
+
223
+ @property
224
+ def uptime(self) -> int:
225
+ """Time in ms this device has been running for."""
226
+ return self._reader.uptime
227
+
228
+ @property
229
+ def supported_wavelengths(self) -> List[int]:
230
+ """The wavelengths in nm this plate reader supports."""
231
+ return self._reader.supported_wavelengths
232
+
233
+ @property
234
+ def measurement_config(self) -> Optional[ABSMeasurementConfig]:
235
+ return self._measurement_config
101
236
 
102
237
  @property
103
238
  def device_info(self) -> Mapping[str, str]:
@@ -107,12 +242,17 @@ class AbsorbanceReader(mod_abc.AbstractModule):
107
242
  @property
108
243
  def live_data(self) -> LiveData:
109
244
  """Return a dict of the module's dynamic information"""
245
+ conf = self._measurement_config.data if self._measurement_config else dict()
110
246
  return {
111
247
  "status": self.status.value,
112
248
  "data": {
249
+ "uptime": self.uptime,
250
+ "deviceStatus": self.status.value,
113
251
  "lidStatus": self.lid_status.value,
114
252
  "platePresence": self.plate_presence.value,
115
- "sampleWavelength": 400,
253
+ "measureMode": conf.get("measureMode", ""),
254
+ "sampleWavelengths": conf.get("sampleWavelengths", []),
255
+ "referenceWavelength": conf.get("referenceWavelength", 0),
116
256
  },
117
257
  }
118
258
 
@@ -131,6 +271,10 @@ class AbsorbanceReader(mod_abc.AbstractModule):
131
271
  """The physical port where the module is connected."""
132
272
  return self._usb_port
133
273
 
274
+ async def deactivate(self, must_be_running: bool = True) -> None:
275
+ """Deactivate the module."""
276
+ pass
277
+
134
278
  async def wait_for_is_running(self) -> None:
135
279
  if not self.is_simulated:
136
280
  await self._execution_manager.wait_for_is_running()
@@ -145,25 +289,44 @@ class AbsorbanceReader(mod_abc.AbstractModule):
145
289
 
146
290
  :returns str: The port we're running on.
147
291
  """
148
- return ""
149
-
150
- def model(self) -> str:
151
- """A name for this specific module, matching module defs"""
152
- return "absorbanceReaderV1"
292
+ await self._poller.stop()
293
+ return self.port
153
294
 
154
295
  @classmethod
155
296
  def name(cls) -> str:
156
297
  """A shortname used for matching usb ports, among other things"""
157
298
  return "absorbancereader"
158
299
 
300
+ def model(self) -> str:
301
+ """A name for this specific module, matching module defs"""
302
+ return "absorbanceReaderV1"
303
+
159
304
  def firmware_prefix(self) -> str:
160
305
  """The prefix used for looking up firmware"""
161
- # TODO: (AA) This is a placeholder
162
- return ""
306
+ return "absorbance-96"
307
+
308
+ async def update_device(self, firmware_file_path: str) -> Tuple[bool, str]:
309
+ """Updates the firmware on the device."""
310
+ if self._updating:
311
+ return False, f"Device {self.serial_number} already updating."
312
+ log.debug(f"Updating {self.name}: {self.port} with {firmware_file_path}")
313
+ self._updating = True
314
+ success, res = await self._driver.update_firmware(firmware_file_path)
315
+ # it takes time for the plate reader to re-init after an update.
316
+ await asyncio.sleep(10)
317
+ self._device_info = await self._driver.get_device_info()
318
+ await self._poller.start()
319
+ self._updating = False
320
+ return success, res
163
321
 
164
322
  def bootloader(self) -> UploadFunction:
165
- """Bootloader mode"""
166
- return upload_func_placeholder
323
+ async def _update_function(
324
+ port: str, firmware_file_path: str, kwargs: Dict[str, Any]
325
+ ) -> Tuple[bool, str]:
326
+ module: AbsorbanceReader = kwargs["module"]
327
+ return await module.update_device(firmware_file_path)
328
+
329
+ return _update_function
167
330
 
168
331
  async def cleanup(self) -> None:
169
332
  """Clean up the module instance.
@@ -171,56 +334,42 @@ class AbsorbanceReader(mod_abc.AbstractModule):
171
334
  Clean up, i.e. stop pollers, disconnect serial, etc in preparation for
172
335
  object destruction.
173
336
  """
337
+ await self._poller.stop()
174
338
  await self._driver.disconnect()
175
339
 
176
- async def set_sample_wavelength(self, wavelength: int) -> None:
177
- """Set the Absorbance Reader's active wavelength."""
178
- await self._driver.initialize_measurement(wavelength)
179
-
180
- async def start_measure(self, wavelength: int) -> List[float]:
181
- """Initiate a single measurement."""
182
- return await self._driver.get_single_measurement(wavelength)
183
-
184
- async def setup(self) -> None:
185
- """Setup the Absorbance Reader."""
186
- is_open = await self._driver.is_connected()
187
- if not is_open:
188
- await self._driver.connect()
189
-
190
- async def get_current_wavelength(self) -> None:
191
- """Get the Absorbance Reader's current active wavelength."""
192
- pass
193
-
194
-
195
- class AbsorbanceReaderReader(Reader):
196
- device_state: AbsorbanceReaderDeviceState
197
- lid_status: AbsorbanceReaderLidStatus
198
- plate_presence: AbsorbanceReaderPlatePresence
199
- supported_wavelengths: List[int]
200
-
201
- def __init__(self, driver: AbsorbanceReaderDriver) -> None:
202
- self.device_state = AbsorbanceReaderDeviceState.UNKNOWN
203
- self.lid_status = AbsorbanceReaderLidStatus.UNKNOWN
204
- self.plate_presence = AbsorbanceReaderPlatePresence.UNKNOWN
205
- self.supported_wavelengths = []
206
- self._driver = driver
207
-
208
- async def read(self) -> None:
209
- await self.get_device_status()
210
- await self.get_supported_wavelengths()
211
-
212
- async def get_device_status(self) -> None:
213
- """Get the Absorbance Reader's current status."""
214
- self.device_state = await self._driver.get_status()
215
-
216
- async def get_supported_wavelengths(self) -> None:
217
- """Get the Absorbance Reader's supported wavelengths."""
218
- self.supported_wavelengths = await self._driver.get_available_wavelengths()
219
-
220
- async def get_lid_status(self) -> None:
221
- """Get the Absorbance Reader's lid status."""
222
- self.lid_status = await self._driver.get_lid_status()
340
+ async def set_sample_wavelength(
341
+ self,
342
+ mode: ABSMeasurementMode,
343
+ wavelengths: List[int],
344
+ reference_wavelength: Optional[int] = None,
345
+ ) -> None:
346
+ """Set the Absorbance Reader's measurement mode and active wavelength."""
347
+ if mode == ABSMeasurementMode.SINGLE:
348
+ assert (
349
+ len(wavelengths) == 1
350
+ ), "Cannot initialize single read mode with more than 1 wavelength."
351
+
352
+ await self._driver.initialize_measurement(wavelengths, mode)
353
+ self._measurement_config = ABSMeasurementConfig(
354
+ measure_mode=mode,
355
+ sample_wavelengths=wavelengths,
356
+ reference_wavelength=reference_wavelength,
357
+ )
223
358
 
224
- async def get_plate_presence(self) -> None:
225
- """Get the Absorbance Reader's plate presence."""
226
- self.plate_presence = await self._driver.get_plate_presence()
359
+ async def start_measure(self) -> List[List[float]]:
360
+ """Initiate a measurement depending on the measurement mode."""
361
+ try:
362
+ self._device_status = AbsorbanceReaderStatus.MEASURING
363
+ return await self._driver.get_measurement()
364
+ finally:
365
+ self._device_status = AbsorbanceReaderStatus.IDLE
366
+
367
+ async def get_current_lid_status(self) -> AbsorbanceReaderLidStatus:
368
+ """Get the Absorbance Reader's current lid status."""
369
+ await self._reader.get_lid_status()
370
+ return self._reader.lid_status
371
+
372
+ def _enter_error_state(self, error: Exception) -> None:
373
+ self._error = str(error)
374
+ if isinstance(error, AbsorbanceReaderDisconnectedError):
375
+ self.disconnected_callback()
@@ -0,0 +1,7 @@
1
+ class UpdateError(RuntimeError):
2
+ pass
3
+
4
+
5
+ class AbsorbanceReaderDisconnectedError(RuntimeError):
6
+ def __init__(self, serial: str):
7
+ self.serial = serial
@@ -14,6 +14,7 @@ from opentrons.hardware_control.execution_manager import ExecutionManager
14
14
  from opentrons.hardware_control.poller import Reader, Poller
15
15
  from opentrons.hardware_control.modules import mod_abc, update
16
16
  from opentrons.hardware_control.modules.types import (
17
+ ModuleDisconnectedCallback,
17
18
  ModuleType,
18
19
  TemperatureStatus,
19
20
  SpeedStatus,
@@ -50,6 +51,7 @@ class HeaterShaker(mod_abc.AbstractModule):
50
51
  simulating: bool = False,
51
52
  sim_model: Optional[str] = None,
52
53
  sim_serial_number: Optional[str] = None,
54
+ disconnected_callback: ModuleDisconnectedCallback = None,
53
55
  ) -> "HeaterShaker":
54
56
  """
55
57
  Build a HeaterShaker
@@ -63,6 +65,7 @@ class HeaterShaker(mod_abc.AbstractModule):
63
65
  simulating: whether to build a simulating driver
64
66
  loop: Loop
65
67
  sim_model: The model name used by simulator
68
+ disconnected_callback: Callback to inform the module controller that the device was disconnected
66
69
 
67
70
  Returns:
68
71
  HeaterShaker instance
@@ -80,12 +83,13 @@ class HeaterShaker(mod_abc.AbstractModule):
80
83
  module = cls(
81
84
  port=port,
82
85
  usb_port=usb_port,
83
- device_info=await driver.get_device_info(),
84
- execution_manager=execution_manager,
85
86
  driver=driver,
86
87
  reader=reader,
87
88
  poller=poller,
89
+ device_info=await driver.get_device_info(),
88
90
  hw_control_loop=hw_control_loop,
91
+ execution_manager=execution_manager,
92
+ disconnected_callback=disconnected_callback,
89
93
  )
90
94
 
91
95
  try:
@@ -105,12 +109,14 @@ class HeaterShaker(mod_abc.AbstractModule):
105
109
  poller: Poller,
106
110
  device_info: Mapping[str, str],
107
111
  hw_control_loop: asyncio.AbstractEventLoop,
112
+ disconnected_callback: ModuleDisconnectedCallback = None,
108
113
  ):
109
114
  super().__init__(
110
115
  port=port,
111
116
  usb_port=usb_port,
112
117
  hw_control_loop=hw_control_loop,
113
118
  execution_manager=execution_manager,
119
+ disconnected_callback=disconnected_callback,
114
120
  )
115
121
  self._device_info = device_info
116
122
  self._driver = driver
@@ -195,7 +201,6 @@ class HeaterShaker(mod_abc.AbstractModule):
195
201
  @property
196
202
  def live_data(self) -> LiveData:
197
203
  return {
198
- # TODO (spp, 2022-2-22): Revise what status includes
199
204
  "status": self.status.value,
200
205
  "data": {
201
206
  "temperatureStatus": self.temperature_status.value,
@@ -1,6 +1,6 @@
1
1
  import asyncio
2
2
  import logging
3
- from typing import Mapping, Optional
3
+ from typing import Dict, Optional
4
4
  from opentrons.drivers.mag_deck import (
5
5
  SimulatingDriver,
6
6
  MagDeckDriver,
@@ -54,6 +54,7 @@ class MagDeck(mod_abc.AbstractModule):
54
54
  simulating: bool = False,
55
55
  sim_model: Optional[str] = None,
56
56
  sim_serial_number: Optional[str] = None,
57
+ disconnected_callback: types.ModuleDisconnectedCallback = None,
57
58
  ) -> "MagDeck":
58
59
  """Factory function."""
59
60
  driver: AbstractMagDeckDriver
@@ -71,6 +72,7 @@ class MagDeck(mod_abc.AbstractModule):
71
72
  hw_control_loop=hw_control_loop,
72
73
  device_info=await driver.get_device_info(),
73
74
  driver=driver,
75
+ disconnected_callback=disconnected_callback,
74
76
  )
75
77
  return mod
76
78
 
@@ -81,7 +83,8 @@ class MagDeck(mod_abc.AbstractModule):
81
83
  execution_manager: ExecutionManager,
82
84
  hw_control_loop: asyncio.AbstractEventLoop,
83
85
  driver: AbstractMagDeckDriver,
84
- device_info: Mapping[str, str],
86
+ device_info: Dict[str, str],
87
+ disconnected_callback: types.ModuleDisconnectedCallback = None,
85
88
  ) -> None:
86
89
  """Constructor"""
87
90
  super().__init__(
@@ -89,6 +92,7 @@ class MagDeck(mod_abc.AbstractModule):
89
92
  usb_port=usb_port,
90
93
  hw_control_loop=hw_control_loop,
91
94
  execution_manager=execution_manager,
95
+ disconnected_callback=disconnected_callback,
92
96
  )
93
97
  self._device_info = device_info
94
98
  self._driver = driver
@@ -162,7 +166,7 @@ class MagDeck(mod_abc.AbstractModule):
162
166
  return self._current_height
163
167
 
164
168
  @property
165
- def device_info(self) -> Mapping[str, str]:
169
+ def device_info(self) -> Dict[str, str]:
166
170
  """
167
171
 
168
172
  Returns: a dict
@@ -171,6 +175,11 @@ class MagDeck(mod_abc.AbstractModule):
171
175
  """
172
176
  return self._device_info
173
177
 
178
+ @property
179
+ def serial_number(self) -> Optional[str]:
180
+ """The usb serial number of this device"""
181
+ return self._device_info.get("serial")
182
+
174
183
  @property
175
184
  def status(self) -> types.MagneticStatus:
176
185
  if self.current_height > 0:
@@ -8,7 +8,13 @@ from opentrons.config import IS_ROBOT, ROBOT_FIRMWARE_DIR
8
8
  from opentrons.drivers.rpi_drivers.types import USBPort
9
9
 
10
10
  from ..execution_manager import ExecutionManager
11
- from .types import BundledFirmware, UploadFunction, LiveData, ModuleType
11
+ from .types import (
12
+ BundledFirmware,
13
+ ModuleDisconnectedCallback,
14
+ UploadFunction,
15
+ LiveData,
16
+ ModuleType,
17
+ )
12
18
 
13
19
  mod_log = logging.getLogger(__name__)
14
20
 
@@ -45,6 +51,7 @@ class AbstractModule(abc.ABC):
45
51
  simulating: bool = False,
46
52
  sim_model: Optional[str] = None,
47
53
  sim_serial_number: Optional[str] = None,
54
+ disconnected_callback: ModuleDisconnectedCallback = None,
48
55
  ) -> "AbstractModule":
49
56
  """Modules should always be created using this factory.
50
57
 
@@ -58,12 +65,15 @@ class AbstractModule(abc.ABC):
58
65
  usb_port: USBPort,
59
66
  execution_manager: ExecutionManager,
60
67
  hw_control_loop: asyncio.AbstractEventLoop,
68
+ disconnected_callback: ModuleDisconnectedCallback = None,
61
69
  ) -> None:
62
70
  self._port = port
63
71
  self._usb_port = usb_port
64
72
  self._loop = hw_control_loop
65
73
  self._execution_manager = execution_manager
66
74
  self._bundled_fw: Optional[BundledFirmware] = self.get_bundled_fw()
75
+ self._disconnected_callback = disconnected_callback
76
+ self._updating = False
67
77
 
68
78
  @staticmethod
69
79
  def sort_key(inst: "AbstractModule") -> int:
@@ -82,13 +92,23 @@ class AbstractModule(abc.ABC):
82
92
  def loop(self) -> asyncio.AbstractEventLoop:
83
93
  return self._loop
84
94
 
95
+ @property
96
+ def updating(self) -> bool:
97
+ """The device is updating is True."""
98
+ return self._updating
99
+
100
+ def disconnected_callback(self) -> None:
101
+ """Called from within the module object to signify the object is no longer connected"""
102
+ if self._disconnected_callback is not None:
103
+ self._disconnected_callback(self.port, self.serial_number)
104
+
85
105
  def get_bundled_fw(self) -> Optional[BundledFirmware]:
86
106
  """Get absolute path to bundled version of module fw if available."""
87
107
  if not IS_ROBOT:
88
108
  return None
89
109
  file_prefix = self.firmware_prefix()
90
110
 
91
- MODULE_FW_RE = re.compile(f"^{file_prefix}@v(.*)[.](hex|bin)$")
111
+ MODULE_FW_RE = re.compile(f"^{file_prefix}@v(.*)[.](hex|bin|byoup)$")
92
112
  for fw_resource in ROBOT_FIRMWARE_DIR.iterdir(): # type: ignore
93
113
  matches = MODULE_FW_RE.search(fw_resource.name)
94
114
  if matches:
@@ -154,6 +174,11 @@ class AbstractModule(abc.ABC):
154
174
  """The physical port where the module is connected."""
155
175
  return self._usb_port
156
176
 
177
+ @property
178
+ def serial_number(self) -> Optional[str]:
179
+ """The usb serial number of this device."""
180
+ return self.device_info.get("serial")
181
+
157
182
  @abc.abstractmethod
158
183
  async def prep_for_update(self) -> str:
159
184
  """Prepare for an update.