opentrons 8.2.0a4__py2.py3-none-any.whl → 8.3.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.

Potentially problematic release.


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

Files changed (238) hide show
  1. opentrons/calibration_storage/deck_configuration.py +3 -3
  2. opentrons/calibration_storage/file_operators.py +3 -3
  3. opentrons/calibration_storage/helpers.py +3 -1
  4. opentrons/calibration_storage/ot2/models/v1.py +16 -29
  5. opentrons/calibration_storage/ot2/tip_length.py +7 -4
  6. opentrons/calibration_storage/ot3/models/v1.py +14 -23
  7. opentrons/cli/analyze.py +18 -6
  8. opentrons/config/defaults_ot3.py +1 -0
  9. opentrons/drivers/asyncio/communication/__init__.py +2 -0
  10. opentrons/drivers/asyncio/communication/errors.py +16 -3
  11. opentrons/drivers/asyncio/communication/serial_connection.py +24 -9
  12. opentrons/drivers/command_builder.py +2 -2
  13. opentrons/drivers/flex_stacker/__init__.py +9 -0
  14. opentrons/drivers/flex_stacker/abstract.py +89 -0
  15. opentrons/drivers/flex_stacker/driver.py +260 -0
  16. opentrons/drivers/flex_stacker/simulator.py +109 -0
  17. opentrons/drivers/flex_stacker/types.py +138 -0
  18. opentrons/drivers/heater_shaker/driver.py +18 -3
  19. opentrons/drivers/temp_deck/driver.py +13 -3
  20. opentrons/drivers/thermocycler/driver.py +17 -3
  21. opentrons/execute.py +3 -1
  22. opentrons/hardware_control/__init__.py +1 -2
  23. opentrons/hardware_control/api.py +33 -21
  24. opentrons/hardware_control/backends/flex_protocol.py +17 -7
  25. opentrons/hardware_control/backends/ot3controller.py +213 -63
  26. opentrons/hardware_control/backends/ot3simulator.py +18 -9
  27. opentrons/hardware_control/backends/ot3utils.py +43 -15
  28. opentrons/hardware_control/dev_types.py +4 -0
  29. opentrons/hardware_control/emulation/heater_shaker.py +4 -0
  30. opentrons/hardware_control/emulation/module_server/client.py +1 -1
  31. opentrons/hardware_control/emulation/module_server/server.py +5 -3
  32. opentrons/hardware_control/emulation/settings.py +3 -4
  33. opentrons/hardware_control/instruments/ot2/instrument_calibration.py +2 -1
  34. opentrons/hardware_control/instruments/ot2/pipette.py +15 -22
  35. opentrons/hardware_control/instruments/ot2/pipette_handler.py +8 -1
  36. opentrons/hardware_control/instruments/ot3/gripper.py +2 -2
  37. opentrons/hardware_control/instruments/ot3/pipette.py +23 -22
  38. opentrons/hardware_control/instruments/ot3/pipette_handler.py +10 -1
  39. opentrons/hardware_control/modules/mod_abc.py +2 -2
  40. opentrons/hardware_control/motion_utilities.py +68 -0
  41. opentrons/hardware_control/nozzle_manager.py +39 -41
  42. opentrons/hardware_control/ot3_calibration.py +1 -1
  43. opentrons/hardware_control/ot3api.py +78 -31
  44. opentrons/hardware_control/protocols/gripper_controller.py +3 -0
  45. opentrons/hardware_control/protocols/hardware_manager.py +5 -1
  46. opentrons/hardware_control/protocols/liquid_handler.py +22 -1
  47. opentrons/hardware_control/protocols/motion_controller.py +7 -0
  48. opentrons/hardware_control/robot_calibration.py +1 -1
  49. opentrons/hardware_control/types.py +61 -0
  50. opentrons/legacy_commands/commands.py +37 -0
  51. opentrons/legacy_commands/types.py +39 -0
  52. opentrons/protocol_api/__init__.py +20 -1
  53. opentrons/protocol_api/_liquid.py +24 -49
  54. opentrons/protocol_api/_liquid_properties.py +754 -0
  55. opentrons/protocol_api/_types.py +24 -0
  56. opentrons/protocol_api/core/common.py +2 -0
  57. opentrons/protocol_api/core/engine/instrument.py +191 -10
  58. opentrons/protocol_api/core/engine/labware.py +29 -7
  59. opentrons/protocol_api/core/engine/protocol.py +130 -5
  60. opentrons/protocol_api/core/engine/robot.py +139 -0
  61. opentrons/protocol_api/core/engine/well.py +4 -1
  62. opentrons/protocol_api/core/instrument.py +73 -4
  63. opentrons/protocol_api/core/labware.py +13 -4
  64. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +87 -3
  65. opentrons/protocol_api/core/legacy/legacy_labware_core.py +13 -4
  66. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +32 -1
  67. opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
  68. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +61 -3
  69. opentrons/protocol_api/core/protocol.py +34 -1
  70. opentrons/protocol_api/core/robot.py +51 -0
  71. opentrons/protocol_api/instrument_context.py +299 -44
  72. opentrons/protocol_api/labware.py +248 -9
  73. opentrons/protocol_api/module_contexts.py +21 -17
  74. opentrons/protocol_api/protocol_context.py +125 -4
  75. opentrons/protocol_api/robot_context.py +204 -32
  76. opentrons/protocol_api/validation.py +262 -3
  77. opentrons/protocol_engine/__init__.py +4 -0
  78. opentrons/protocol_engine/actions/actions.py +2 -3
  79. opentrons/protocol_engine/clients/sync_client.py +18 -0
  80. opentrons/protocol_engine/commands/__init__.py +121 -0
  81. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +1 -3
  82. opentrons/protocol_engine/commands/absorbance_reader/initialize.py +20 -6
  83. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +1 -2
  84. opentrons/protocol_engine/commands/absorbance_reader/read.py +36 -10
  85. opentrons/protocol_engine/commands/air_gap_in_place.py +160 -0
  86. opentrons/protocol_engine/commands/aspirate.py +103 -53
  87. opentrons/protocol_engine/commands/aspirate_in_place.py +55 -51
  88. opentrons/protocol_engine/commands/blow_out.py +44 -39
  89. opentrons/protocol_engine/commands/blow_out_in_place.py +21 -32
  90. opentrons/protocol_engine/commands/calibration/calibrate_gripper.py +13 -6
  91. opentrons/protocol_engine/commands/calibration/calibrate_module.py +1 -1
  92. opentrons/protocol_engine/commands/calibration/calibrate_pipette.py +3 -3
  93. opentrons/protocol_engine/commands/calibration/move_to_maintenance_position.py +1 -1
  94. opentrons/protocol_engine/commands/command.py +73 -66
  95. opentrons/protocol_engine/commands/command_unions.py +140 -1
  96. opentrons/protocol_engine/commands/comment.py +1 -1
  97. opentrons/protocol_engine/commands/configure_for_volume.py +10 -3
  98. opentrons/protocol_engine/commands/configure_nozzle_layout.py +6 -4
  99. opentrons/protocol_engine/commands/custom.py +6 -12
  100. opentrons/protocol_engine/commands/dispense.py +82 -48
  101. opentrons/protocol_engine/commands/dispense_in_place.py +71 -51
  102. opentrons/protocol_engine/commands/drop_tip.py +52 -31
  103. opentrons/protocol_engine/commands/drop_tip_in_place.py +79 -8
  104. opentrons/protocol_engine/commands/evotip_dispense.py +156 -0
  105. opentrons/protocol_engine/commands/evotip_seal_pipette.py +331 -0
  106. opentrons/protocol_engine/commands/evotip_unseal_pipette.py +160 -0
  107. opentrons/protocol_engine/commands/generate_command_schema.py +4 -11
  108. opentrons/protocol_engine/commands/get_next_tip.py +134 -0
  109. opentrons/protocol_engine/commands/get_tip_presence.py +1 -1
  110. opentrons/protocol_engine/commands/heater_shaker/close_labware_latch.py +1 -1
  111. opentrons/protocol_engine/commands/heater_shaker/deactivate_heater.py +1 -1
  112. opentrons/protocol_engine/commands/heater_shaker/deactivate_shaker.py +1 -1
  113. opentrons/protocol_engine/commands/heater_shaker/open_labware_latch.py +1 -1
  114. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +1 -1
  115. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  116. opentrons/protocol_engine/commands/heater_shaker/wait_for_temperature.py +10 -4
  117. opentrons/protocol_engine/commands/home.py +13 -4
  118. opentrons/protocol_engine/commands/liquid_probe.py +125 -31
  119. opentrons/protocol_engine/commands/load_labware.py +33 -6
  120. opentrons/protocol_engine/commands/load_lid.py +146 -0
  121. opentrons/protocol_engine/commands/load_lid_stack.py +189 -0
  122. opentrons/protocol_engine/commands/load_liquid.py +12 -4
  123. opentrons/protocol_engine/commands/load_liquid_class.py +144 -0
  124. opentrons/protocol_engine/commands/load_module.py +31 -10
  125. opentrons/protocol_engine/commands/load_pipette.py +19 -8
  126. opentrons/protocol_engine/commands/magnetic_module/disengage.py +1 -1
  127. opentrons/protocol_engine/commands/magnetic_module/engage.py +1 -1
  128. opentrons/protocol_engine/commands/move_labware.py +28 -6
  129. opentrons/protocol_engine/commands/move_relative.py +35 -25
  130. opentrons/protocol_engine/commands/move_to_addressable_area.py +40 -27
  131. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +53 -32
  132. opentrons/protocol_engine/commands/move_to_coordinates.py +36 -22
  133. opentrons/protocol_engine/commands/move_to_well.py +40 -24
  134. opentrons/protocol_engine/commands/movement_common.py +338 -0
  135. opentrons/protocol_engine/commands/pick_up_tip.py +49 -27
  136. opentrons/protocol_engine/commands/pipetting_common.py +169 -87
  137. opentrons/protocol_engine/commands/prepare_to_aspirate.py +24 -33
  138. opentrons/protocol_engine/commands/reload_labware.py +1 -1
  139. opentrons/protocol_engine/commands/retract_axis.py +1 -1
  140. opentrons/protocol_engine/commands/robot/__init__.py +69 -0
  141. opentrons/protocol_engine/commands/robot/close_gripper_jaw.py +86 -0
  142. opentrons/protocol_engine/commands/robot/common.py +18 -0
  143. opentrons/protocol_engine/commands/robot/move_axes_relative.py +101 -0
  144. opentrons/protocol_engine/commands/robot/move_axes_to.py +100 -0
  145. opentrons/protocol_engine/commands/robot/move_to.py +94 -0
  146. opentrons/protocol_engine/commands/robot/open_gripper_jaw.py +77 -0
  147. opentrons/protocol_engine/commands/save_position.py +14 -5
  148. opentrons/protocol_engine/commands/set_rail_lights.py +1 -1
  149. opentrons/protocol_engine/commands/set_status_bar.py +1 -1
  150. opentrons/protocol_engine/commands/temperature_module/deactivate.py +1 -1
  151. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +1 -1
  152. opentrons/protocol_engine/commands/temperature_module/wait_for_temperature.py +10 -4
  153. opentrons/protocol_engine/commands/thermocycler/close_lid.py +1 -1
  154. opentrons/protocol_engine/commands/thermocycler/deactivate_block.py +1 -1
  155. opentrons/protocol_engine/commands/thermocycler/deactivate_lid.py +1 -1
  156. opentrons/protocol_engine/commands/thermocycler/open_lid.py +1 -1
  157. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +9 -3
  158. opentrons/protocol_engine/commands/thermocycler/run_profile.py +9 -3
  159. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +11 -4
  160. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +1 -1
  161. opentrons/protocol_engine/commands/thermocycler/wait_for_block_temperature.py +1 -1
  162. opentrons/protocol_engine/commands/thermocycler/wait_for_lid_temperature.py +1 -1
  163. opentrons/protocol_engine/commands/touch_tip.py +65 -16
  164. opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +5 -2
  165. opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +13 -4
  166. opentrons/protocol_engine/commands/unsafe/unsafe_engage_axes.py +2 -5
  167. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +1 -1
  168. opentrons/protocol_engine/commands/unsafe/unsafe_ungrip_labware.py +1 -1
  169. opentrons/protocol_engine/commands/unsafe/update_position_estimators.py +2 -5
  170. opentrons/protocol_engine/commands/verify_tip_presence.py +11 -4
  171. opentrons/protocol_engine/commands/wait_for_duration.py +10 -3
  172. opentrons/protocol_engine/commands/wait_for_resume.py +10 -3
  173. opentrons/protocol_engine/errors/__init__.py +12 -0
  174. opentrons/protocol_engine/errors/error_occurrence.py +19 -20
  175. opentrons/protocol_engine/errors/exceptions.py +76 -0
  176. opentrons/protocol_engine/execution/command_executor.py +1 -1
  177. opentrons/protocol_engine/execution/equipment.py +73 -5
  178. opentrons/protocol_engine/execution/gantry_mover.py +369 -8
  179. opentrons/protocol_engine/execution/hardware_stopper.py +7 -7
  180. opentrons/protocol_engine/execution/movement.py +27 -0
  181. opentrons/protocol_engine/execution/pipetting.py +5 -1
  182. opentrons/protocol_engine/execution/tip_handler.py +34 -15
  183. opentrons/protocol_engine/notes/notes.py +1 -1
  184. opentrons/protocol_engine/protocol_engine.py +7 -6
  185. opentrons/protocol_engine/resources/labware_data_provider.py +1 -1
  186. opentrons/protocol_engine/resources/labware_validation.py +18 -0
  187. opentrons/protocol_engine/resources/module_data_provider.py +1 -1
  188. opentrons/protocol_engine/resources/pipette_data_provider.py +26 -0
  189. opentrons/protocol_engine/slot_standardization.py +9 -9
  190. opentrons/protocol_engine/state/_move_types.py +9 -5
  191. opentrons/protocol_engine/state/_well_math.py +193 -0
  192. opentrons/protocol_engine/state/addressable_areas.py +25 -61
  193. opentrons/protocol_engine/state/command_history.py +12 -0
  194. opentrons/protocol_engine/state/commands.py +22 -14
  195. opentrons/protocol_engine/state/files.py +10 -12
  196. opentrons/protocol_engine/state/fluid_stack.py +138 -0
  197. opentrons/protocol_engine/state/frustum_helpers.py +63 -69
  198. opentrons/protocol_engine/state/geometry.py +47 -1
  199. opentrons/protocol_engine/state/labware.py +92 -26
  200. opentrons/protocol_engine/state/liquid_classes.py +82 -0
  201. opentrons/protocol_engine/state/liquids.py +16 -4
  202. opentrons/protocol_engine/state/modules.py +52 -70
  203. opentrons/protocol_engine/state/motion.py +6 -1
  204. opentrons/protocol_engine/state/pipettes.py +149 -58
  205. opentrons/protocol_engine/state/state.py +21 -2
  206. opentrons/protocol_engine/state/state_summary.py +4 -2
  207. opentrons/protocol_engine/state/tips.py +11 -44
  208. opentrons/protocol_engine/state/update_types.py +343 -48
  209. opentrons/protocol_engine/state/wells.py +19 -11
  210. opentrons/protocol_engine/types.py +176 -28
  211. opentrons/protocol_reader/extract_labware_definitions.py +5 -2
  212. opentrons/protocol_reader/file_format_validator.py +5 -5
  213. opentrons/protocol_runner/json_file_reader.py +9 -3
  214. opentrons/protocol_runner/json_translator.py +51 -25
  215. opentrons/protocol_runner/legacy_command_mapper.py +66 -64
  216. opentrons/protocol_runner/protocol_runner.py +35 -4
  217. opentrons/protocol_runner/python_protocol_wrappers.py +1 -1
  218. opentrons/protocol_runner/run_orchestrator.py +13 -3
  219. opentrons/protocols/advanced_control/common.py +38 -0
  220. opentrons/protocols/advanced_control/mix.py +1 -1
  221. opentrons/protocols/advanced_control/transfers/__init__.py +0 -0
  222. opentrons/protocols/advanced_control/transfers/common.py +56 -0
  223. opentrons/protocols/advanced_control/{transfers.py → transfers/transfer.py} +10 -85
  224. opentrons/protocols/api_support/definitions.py +1 -1
  225. opentrons/protocols/api_support/instrument.py +1 -1
  226. opentrons/protocols/api_support/util.py +10 -0
  227. opentrons/protocols/labware.py +70 -8
  228. opentrons/protocols/models/json_protocol.py +5 -9
  229. opentrons/simulate.py +3 -1
  230. opentrons/types.py +162 -2
  231. opentrons/util/entrypoint_util.py +2 -5
  232. opentrons/util/logging_config.py +1 -1
  233. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/METADATA +16 -15
  234. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/RECORD +238 -208
  235. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/WHEEL +1 -1
  236. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/LICENSE +0 -0
  237. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/entry_points.txt +0 -0
  238. {opentrons-8.2.0a4.dist-info → opentrons-8.3.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,89 @@
1
+ from typing import Protocol
2
+
3
+ from .types import (
4
+ StackerAxis,
5
+ PlatformStatus,
6
+ Direction,
7
+ MoveParams,
8
+ StackerInfo,
9
+ LEDColor,
10
+ )
11
+
12
+
13
+ class AbstractStackerDriver(Protocol):
14
+ """Protocol for the Stacker driver."""
15
+
16
+ async def connect(self) -> None:
17
+ """Connect to stacker."""
18
+ ...
19
+
20
+ async def disconnect(self) -> None:
21
+ """Disconnect from stacker."""
22
+ ...
23
+
24
+ async def is_connected(self) -> bool:
25
+ """Check connection to stacker."""
26
+ ...
27
+
28
+ async def update_firmware(self, firmware_file_path: str) -> None:
29
+ """Updates the firmware on the device."""
30
+ ...
31
+
32
+ async def get_device_info(self) -> StackerInfo:
33
+ """Get Device Info."""
34
+ ...
35
+
36
+ async def set_serial_number(self, sn: str) -> bool:
37
+ """Set Serial Number."""
38
+ ...
39
+
40
+ async def stop_motors(self) -> bool:
41
+ """Stop all motor movement."""
42
+ ...
43
+
44
+ async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool:
45
+ """Get limit switch status.
46
+
47
+ :return: True if limit switch is triggered, False otherwise
48
+ """
49
+ ...
50
+
51
+ async def get_platform_sensor(self, direction: Direction) -> bool:
52
+ """Get platform sensor status.
53
+
54
+ :return: True if platform is present, False otherwise
55
+ """
56
+ ...
57
+
58
+ async def get_platform_status(self) -> PlatformStatus:
59
+ """Get platform status."""
60
+ ...
61
+
62
+ async def get_hopper_door_closed(self) -> bool:
63
+ """Get whether or not door is closed.
64
+
65
+ :return: True if door is closed, False otherwise
66
+ """
67
+ ...
68
+
69
+ async def move_in_mm(
70
+ self, axis: StackerAxis, distance: float, params: MoveParams | None = None
71
+ ) -> bool:
72
+ """Move axis."""
73
+ ...
74
+
75
+ async def move_to_limit_switch(
76
+ self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None
77
+ ) -> bool:
78
+ """Move until limit switch is triggered."""
79
+ ...
80
+
81
+ async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool:
82
+ """Home axis."""
83
+ ...
84
+
85
+ async def set_led(
86
+ self, power: float, color: LEDColor | None = None, external: bool | None = None
87
+ ) -> bool:
88
+ """Set LED color of status bar."""
89
+ ...
@@ -0,0 +1,260 @@
1
+ import asyncio
2
+ import re
3
+ from typing import Optional
4
+
5
+ from opentrons.drivers.command_builder import CommandBuilder
6
+ from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection
7
+
8
+ from .abstract import AbstractStackerDriver
9
+ from .types import (
10
+ GCODE,
11
+ StackerAxis,
12
+ PlatformStatus,
13
+ Direction,
14
+ StackerInfo,
15
+ HardwareRevision,
16
+ MoveParams,
17
+ LimitSwitchStatus,
18
+ LEDColor,
19
+ )
20
+
21
+
22
+ FS_BAUDRATE = 115200
23
+ DEFAULT_FS_TIMEOUT = 40
24
+ FS_ACK = "OK\n"
25
+ FS_ERROR_KEYWORD = "err"
26
+ FS_ASYNC_ERROR_ACK = "async"
27
+ DEFAULT_COMMAND_RETRIES = 0
28
+ GCODE_ROUNDING_PRECISION = 2
29
+
30
+
31
+ class FlexStackerDriver(AbstractStackerDriver):
32
+ """FLEX Stacker driver."""
33
+
34
+ @classmethod
35
+ def parse_device_info(cls, response: str) -> StackerInfo:
36
+ """Parse stacker info."""
37
+ # TODO: Validate serial number format once established
38
+ _RE = re.compile(
39
+ f"^{GCODE.DEVICE_INFO} FW:(?P<fw>\\S+) HW:Opentrons-flex-stacker-(?P<hw>\\S+) SerialNo:(?P<sn>\\S+)$"
40
+ )
41
+ m = _RE.match(response)
42
+ if not m:
43
+ raise ValueError(f"Incorrect Response for device info: {response}")
44
+ return StackerInfo(
45
+ m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn")
46
+ )
47
+
48
+ @classmethod
49
+ def parse_limit_switch_status(cls, response: str) -> LimitSwitchStatus:
50
+ """Parse limit switch statuses."""
51
+ field_names = LimitSwitchStatus.get_fields()
52
+ pattern = r"\s".join([rf"{name}:(?P<{name}>\d)" for name in field_names])
53
+ _RE = re.compile(f"^{GCODE.GET_LIMIT_SWITCH} {pattern}$")
54
+ m = _RE.match(response)
55
+ if not m:
56
+ raise ValueError(f"Incorrect Response for limit switch status: {response}")
57
+ return LimitSwitchStatus(*(bool(int(m.group(name))) for name in field_names))
58
+
59
+ @classmethod
60
+ def parse_platform_sensor_status(cls, response: str) -> PlatformStatus:
61
+ """Parse platform statuses."""
62
+ field_names = PlatformStatus.get_fields()
63
+ pattern = r"\s".join([rf"{name}:(?P<{name}>\d)" for name in field_names])
64
+ _RE = re.compile(f"^{GCODE.GET_PLATFORM_SENSOR} {pattern}$")
65
+ m = _RE.match(response)
66
+ if not m:
67
+ raise ValueError(f"Incorrect Response for platform status: {response}")
68
+ return PlatformStatus(*(bool(int(m.group(name))) for name in field_names))
69
+
70
+ @classmethod
71
+ def parse_door_closed(cls, response: str) -> bool:
72
+ """Parse door closed."""
73
+ _RE = re.compile(r"^M122 D:(\d)$")
74
+ match = _RE.match(response)
75
+ if not match:
76
+ raise ValueError(f"Incorrect Response for door closed: {response}")
77
+ return bool(int(match.group(1)))
78
+
79
+ @classmethod
80
+ def append_move_params(
81
+ cls, command: CommandBuilder, params: MoveParams | None
82
+ ) -> CommandBuilder:
83
+ """Append move params."""
84
+ if params is not None:
85
+ if params.max_speed is not None:
86
+ command.add_float("V", params.max_speed, GCODE_ROUNDING_PRECISION)
87
+ if params.acceleration is not None:
88
+ command.add_float("A", params.acceleration, GCODE_ROUNDING_PRECISION)
89
+ if params.max_speed_discont is not None:
90
+ command.add_float(
91
+ "D", params.max_speed_discont, GCODE_ROUNDING_PRECISION
92
+ )
93
+ return command
94
+
95
+ @classmethod
96
+ async def create(
97
+ cls, port: str, loop: Optional[asyncio.AbstractEventLoop]
98
+ ) -> "FlexStackerDriver":
99
+ """Create a FLEX Stacker driver."""
100
+ connection = await AsyncResponseSerialConnection.create(
101
+ port=port,
102
+ baud_rate=FS_BAUDRATE,
103
+ timeout=DEFAULT_FS_TIMEOUT,
104
+ number_of_retries=DEFAULT_COMMAND_RETRIES,
105
+ ack=FS_ACK,
106
+ loop=loop,
107
+ error_keyword=FS_ERROR_KEYWORD,
108
+ async_error_ack=FS_ASYNC_ERROR_ACK,
109
+ )
110
+ return cls(connection)
111
+
112
+ def __init__(self, connection: AsyncResponseSerialConnection) -> None:
113
+ """
114
+ Constructor
115
+
116
+ Args:
117
+ connection: Connection to the FLEX Stacker
118
+ """
119
+ self._connection = connection
120
+
121
+ async def connect(self) -> None:
122
+ """Connect to stacker."""
123
+ await self._connection.open()
124
+
125
+ async def disconnect(self) -> None:
126
+ """Disconnect from stacker."""
127
+ await self._connection.close()
128
+
129
+ async def is_connected(self) -> bool:
130
+ """Check connection to stacker."""
131
+ return await self._connection.is_open()
132
+
133
+ async def get_device_info(self) -> StackerInfo:
134
+ """Get Device Info."""
135
+ response = await self._connection.send_command(
136
+ GCODE.DEVICE_INFO.build_command()
137
+ )
138
+ await self._connection.send_command(GCODE.GET_RESET_REASON.build_command())
139
+ return self.parse_device_info(response)
140
+
141
+ async def set_serial_number(self, sn: str) -> bool:
142
+ """Set Serial Number."""
143
+ # TODO: validate the serial number format
144
+ resp = await self._connection.send_command(
145
+ GCODE.SET_SERIAL_NUMBER.build_command().add_element(sn)
146
+ )
147
+ if not re.match(rf"^{GCODE.SET_SERIAL_NUMBER}$", resp):
148
+ raise ValueError(f"Incorrect Response for set serial number: {resp}")
149
+ return True
150
+
151
+ async def stop_motors(self) -> bool:
152
+ """Stop all motor movement."""
153
+ resp = await self._connection.send_command(GCODE.STOP_MOTORS.build_command())
154
+ if not re.match(rf"^{GCODE.STOP_MOTORS}$", resp):
155
+ raise ValueError(f"Incorrect Response for stop motors: {resp}")
156
+ return True
157
+
158
+ async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool:
159
+ """Get limit switch status.
160
+
161
+ :return: True if limit switch is triggered, False otherwise
162
+ """
163
+ response = await self.get_limit_switches_status()
164
+ return response.get(axis, direction)
165
+
166
+ async def get_limit_switches_status(self) -> LimitSwitchStatus:
167
+ """Get limit switch statuses for all axes."""
168
+ response = await self._connection.send_command(
169
+ GCODE.GET_LIMIT_SWITCH.build_command()
170
+ )
171
+ return self.parse_limit_switch_status(response)
172
+
173
+ async def get_platform_sensor(self, direction: Direction) -> bool:
174
+ """Get platform sensor at one direction."""
175
+ response = await self.get_platform_status()
176
+ return response.get(direction)
177
+
178
+ async def get_platform_status(self) -> PlatformStatus:
179
+ """Get platform sensor status.
180
+
181
+ :return: True if platform is detected, False otherwise
182
+ """
183
+ response = await self._connection.send_command(
184
+ GCODE.GET_PLATFORM_SENSOR.build_command()
185
+ )
186
+ return self.parse_platform_sensor_status(response)
187
+
188
+ async def get_hopper_door_closed(self) -> bool:
189
+ """Get whether or not door is closed.
190
+
191
+ :return: True if door is closed, False otherwise
192
+ """
193
+ response = await self._connection.send_command(
194
+ GCODE.GET_DOOR_SWITCH.build_command()
195
+ )
196
+ return self.parse_door_closed(response)
197
+
198
+ async def move_in_mm(
199
+ self, axis: StackerAxis, distance: float, params: MoveParams | None = None
200
+ ) -> bool:
201
+ """Move axis."""
202
+ command = self.append_move_params(
203
+ GCODE.MOVE_TO.build_command().add_float(
204
+ axis.name, distance, GCODE_ROUNDING_PRECISION
205
+ ),
206
+ params,
207
+ )
208
+ resp = await self._connection.send_command(command)
209
+ if not re.match(rf"^{GCODE.MOVE_TO}$", resp):
210
+ raise ValueError(f"Incorrect Response for move to: {resp}")
211
+ return True
212
+
213
+ async def move_to_limit_switch(
214
+ self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None
215
+ ) -> bool:
216
+ """Move until limit switch is triggered."""
217
+ command = self.append_move_params(
218
+ GCODE.MOVE_TO_SWITCH.build_command().add_int(axis.name, direction.value),
219
+ params,
220
+ )
221
+ resp = await self._connection.send_command(command)
222
+ if not re.match(rf"^{GCODE.MOVE_TO_SWITCH}$", resp):
223
+ raise ValueError(f"Incorrect Response for move to switch: {resp}")
224
+ return True
225
+
226
+ async def home_axis(self, axis: StackerAxis, direction: Direction) -> bool:
227
+ """Home axis."""
228
+ resp = await self._connection.send_command(
229
+ GCODE.HOME_AXIS.build_command().add_int(axis.name, direction.value)
230
+ )
231
+ if not re.match(rf"^{GCODE.HOME_AXIS}$", resp):
232
+ raise ValueError(f"Incorrect Response for home axis: {resp}")
233
+ return True
234
+
235
+ async def set_led(
236
+ self, power: float, color: LEDColor | None = None, external: bool | None = None
237
+ ) -> bool:
238
+ """Set LED color.
239
+
240
+ :param power: Power of the LED (0-1.0), 0 is off, 1 is full power
241
+ :param color: Color of the LED
242
+ :param external: True if external LED, False if internal LED
243
+ """
244
+ power = max(0, min(power, 1.0))
245
+ command = GCODE.SET_LED.build_command().add_float(
246
+ "P", power, GCODE_ROUNDING_PRECISION
247
+ )
248
+ if color is not None:
249
+ command.add_int("C", color.value)
250
+ if external is not None:
251
+ command.add_int("E", external)
252
+ resp = await self._connection.send_command(command)
253
+ if not re.match(rf"^{GCODE.SET_LED}$", resp):
254
+ raise ValueError(f"Incorrect Response for set led: {resp}")
255
+ return True
256
+
257
+ async def update_firmware(self, firmware_file_path: str) -> None:
258
+ """Updates the firmware on the device."""
259
+ # TODO: Implement firmware update
260
+ pass
@@ -0,0 +1,109 @@
1
+ from typing import Optional
2
+
3
+ from opentrons.util.async_helpers import ensure_yield
4
+
5
+ from .abstract import AbstractStackerDriver
6
+ from .types import (
7
+ StackerAxis,
8
+ PlatformStatus,
9
+ Direction,
10
+ StackerInfo,
11
+ HardwareRevision,
12
+ MoveParams,
13
+ LimitSwitchStatus,
14
+ )
15
+
16
+
17
+ class SimulatingDriver(AbstractStackerDriver):
18
+ """FLEX Stacker driver simulator."""
19
+
20
+ def __init__(self, serial_number: Optional[str] = None) -> None:
21
+ self._sn = serial_number or "dummySerialFS"
22
+ self._limit_switch_status = LimitSwitchStatus(False, False, False, False, False)
23
+ self._platform_sensor_status = PlatformStatus(False, False)
24
+ self._door_closed = True
25
+
26
+ def set_limit_switch(self, status: LimitSwitchStatus) -> bool:
27
+ self._limit_switch_status = status
28
+ return True
29
+
30
+ def set_platform_sensor(self, status: PlatformStatus) -> bool:
31
+ self._platform_sensor_status = status
32
+ return True
33
+
34
+ def set_door_closed(self, door_closed: bool) -> bool:
35
+ self._door_closed = door_closed
36
+ return True
37
+
38
+ @ensure_yield
39
+ async def connect(self) -> None:
40
+ """Connect to stacker."""
41
+ pass
42
+
43
+ @ensure_yield
44
+ async def disconnect(self) -> None:
45
+ """Disconnect from stacker."""
46
+ pass
47
+
48
+ @ensure_yield
49
+ async def is_connected(self) -> bool:
50
+ """Check connection to stacker."""
51
+ return True
52
+
53
+ @ensure_yield
54
+ async def get_device_info(self) -> StackerInfo:
55
+ """Get Device Info."""
56
+ return StackerInfo(fw="stacker-fw", hw=HardwareRevision.EVT, sn=self._sn)
57
+
58
+ @ensure_yield
59
+ async def set_serial_number(self, sn: str) -> bool:
60
+ """Set Serial Number."""
61
+ return True
62
+
63
+ @ensure_yield
64
+ async def stop_motor(self) -> bool:
65
+ """Stop motor movement."""
66
+ return True
67
+
68
+ @ensure_yield
69
+ async def get_limit_switch(self, axis: StackerAxis, direction: Direction) -> bool:
70
+ """Get limit switch status.
71
+
72
+ :return: True if limit switch is triggered, False otherwise
73
+ """
74
+ return self._limit_switch_status.get(axis, direction)
75
+
76
+ @ensure_yield
77
+ async def get_limit_switches_status(self) -> LimitSwitchStatus:
78
+ """Get limit switch statuses for all axes."""
79
+ return self._limit_switch_status
80
+
81
+ @ensure_yield
82
+ async def get_platform_sensor_status(self) -> PlatformStatus:
83
+ """Get platform sensor status.
84
+
85
+ :return: True if platform is detected, False otherwise
86
+ """
87
+ return self._platform_sensor_status
88
+
89
+ @ensure_yield
90
+ async def get_hopper_door_closed(self) -> bool:
91
+ """Get whether or not door is closed.
92
+
93
+ :return: True if door is closed, False otherwise
94
+ """
95
+ return self._door_closed
96
+
97
+ @ensure_yield
98
+ async def move_in_mm(
99
+ self, axis: StackerAxis, distance: float, params: MoveParams | None = None
100
+ ) -> bool:
101
+ """Move axis."""
102
+ return True
103
+
104
+ @ensure_yield
105
+ async def move_to_limit_switch(
106
+ self, axis: StackerAxis, direction: Direction, params: MoveParams | None = None
107
+ ) -> bool:
108
+ """Move until limit switch is triggered."""
109
+ return True
@@ -0,0 +1,138 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass, fields
3
+ from typing import List
4
+
5
+ from opentrons.drivers.command_builder import CommandBuilder
6
+
7
+
8
+ class GCODE(str, Enum):
9
+
10
+ MOVE_TO = "G0"
11
+ MOVE_TO_SWITCH = "G5"
12
+ HOME_AXIS = "G28"
13
+ STOP_MOTORS = "M0"
14
+ GET_RESET_REASON = "M114"
15
+ DEVICE_INFO = "M115"
16
+ GET_LIMIT_SWITCH = "M119"
17
+ SET_LED = "M200"
18
+ GET_PLATFORM_SENSOR = "M121"
19
+ GET_DOOR_SWITCH = "M122"
20
+ SET_SERIAL_NUMBER = "M996"
21
+ ENTER_BOOTLOADER = "dfu"
22
+
23
+ def build_command(self) -> CommandBuilder:
24
+ """Build command."""
25
+ return CommandBuilder().add_gcode(self)
26
+
27
+
28
+ STACKER_VID = 0x483
29
+ STACKER_PID = 0xEF24
30
+ STACKER_FREQ = 115200
31
+
32
+
33
+ class HardwareRevision(Enum):
34
+ """Hardware Revision."""
35
+
36
+ NFF = "nff"
37
+ EVT = "a1"
38
+
39
+
40
+ @dataclass
41
+ class StackerInfo:
42
+ """Stacker Info."""
43
+
44
+ fw: str
45
+ hw: HardwareRevision
46
+ sn: str
47
+
48
+
49
+ class StackerAxis(Enum):
50
+ """Stacker Axis."""
51
+
52
+ X = "X"
53
+ Z = "Z"
54
+ L = "L"
55
+
56
+ def __str__(self) -> str:
57
+ """Name."""
58
+ return self.name
59
+
60
+
61
+ class LEDColor(Enum):
62
+ """Stacker LED Color."""
63
+
64
+ WHITE = 0
65
+ RED = 1
66
+ GREEN = 2
67
+ BLUE = 3
68
+
69
+
70
+ class Direction(Enum):
71
+ """Direction."""
72
+
73
+ RETRACT = 0 # negative
74
+ EXTENT = 1 # positive
75
+
76
+ def __str__(self) -> str:
77
+ """Convert to tag for clear logging."""
78
+ return "negative" if self == Direction.RETRACT else "positive"
79
+
80
+ def opposite(self) -> "Direction":
81
+ """Get opposite direction."""
82
+ return Direction.EXTENT if self == Direction.RETRACT else Direction.RETRACT
83
+
84
+ def distance(self, distance: float) -> float:
85
+ """Get signed distance, where retract direction is negative."""
86
+ return distance * -1 if self == Direction.RETRACT else distance
87
+
88
+
89
+ @dataclass
90
+ class LimitSwitchStatus:
91
+ """Stacker Limit Switch Statuses."""
92
+
93
+ XE: bool
94
+ XR: bool
95
+ ZE: bool
96
+ ZR: bool
97
+ LR: bool
98
+
99
+ @classmethod
100
+ def get_fields(cls) -> List[str]:
101
+ """Get fields."""
102
+ return [f.name for f in fields(cls)]
103
+
104
+ def get(self, axis: StackerAxis, direction: Direction) -> bool:
105
+ """Get limit switch status."""
106
+ if axis == StackerAxis.X:
107
+ return self.XE if direction == Direction.EXTENT else self.XR
108
+ if axis == StackerAxis.Z:
109
+ return self.ZE if direction == Direction.EXTENT else self.ZR
110
+ if direction == Direction.EXTENT:
111
+ raise ValueError("Latch does not have extent limit switch")
112
+ return self.LR
113
+
114
+
115
+ @dataclass
116
+ class PlatformStatus:
117
+ """Stacker Platform Statuses."""
118
+
119
+ E: bool
120
+ R: bool
121
+
122
+ @classmethod
123
+ def get_fields(cls) -> List[str]:
124
+ """Get fields."""
125
+ return [f.name for f in fields(cls)]
126
+
127
+ def get(self, direction: Direction) -> bool:
128
+ """Get platform status."""
129
+ return self.E if direction == Direction.EXTENT else self.R
130
+
131
+
132
+ @dataclass
133
+ class MoveParams:
134
+ """Move Parameters."""
135
+
136
+ max_speed: float | None = None
137
+ acceleration: float | None = None
138
+ max_speed_discont: float | None = None
@@ -6,7 +6,10 @@ import asyncio
6
6
  from typing import Optional, Dict
7
7
  from opentrons.drivers import utils
8
8
  from opentrons.drivers.command_builder import CommandBuilder
9
- from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection
9
+ from opentrons.drivers.asyncio.communication import (
10
+ AsyncResponseSerialConnection,
11
+ UnhandledGcode,
12
+ )
10
13
  from opentrons.drivers.heater_shaker.abstract import AbstractHeaterShakerDriver
11
14
  from opentrons.drivers.types import Temperature, RPM, HeaterShakerLabwareLatchStatus
12
15
 
@@ -23,6 +26,7 @@ class GCODE(str, Enum):
23
26
  CLOSE_LABWARE_LATCH = "M243"
24
27
  GET_LABWARE_LATCH_STATE = "M241"
25
28
  DEACTIVATE_HEATER = "M106"
29
+ GET_RESET_REASON = "M114"
26
30
 
27
31
 
28
32
  HS_BAUDRATE = 115200
@@ -166,12 +170,23 @@ class HeaterShakerDriver(AbstractHeaterShakerDriver):
166
170
 
167
171
  async def get_device_info(self) -> Dict[str, str]:
168
172
  """Send get-device-info command"""
169
- c = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode(
173
+ device_info = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode(
170
174
  gcode=GCODE.GET_VERSION
171
175
  )
172
176
  response = await self._connection.send_command(
173
- command=c, retries=DEFAULT_COMMAND_RETRIES
177
+ command=device_info, retries=DEFAULT_COMMAND_RETRIES
178
+ )
179
+
180
+ reset_reason = CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode(
181
+ gcode=GCODE.GET_RESET_REASON
174
182
  )
183
+ try:
184
+ await self._connection.send_command(
185
+ command=reset_reason, retries=DEFAULT_COMMAND_RETRIES
186
+ )
187
+ except UnhandledGcode:
188
+ pass
189
+
175
190
  return utils.parse_hs_device_information(device_info_string=response)
176
191
 
177
192
  async def enter_programming_mode(self) -> None:
@@ -17,7 +17,7 @@ from enum import Enum
17
17
  from opentrons.drivers import utils
18
18
  from opentrons.drivers.types import Temperature
19
19
  from opentrons.drivers.command_builder import CommandBuilder
20
- from opentrons.drivers.asyncio.communication import SerialConnection
20
+ from opentrons.drivers.asyncio.communication import SerialConnection, UnhandledGcode
21
21
  from opentrons.drivers.temp_deck.abstract import AbstractTempDeckDriver
22
22
 
23
23
  log = logging.getLogger(__name__)
@@ -31,6 +31,7 @@ class GCODE(str, Enum):
31
31
  GET_TEMP = "M105"
32
32
  SET_TEMP = "M104"
33
33
  DEVICE_INFO = "M115"
34
+ GET_RESET_REASON = "M114"
34
35
  DISENGAGE = "M18"
35
36
  PROGRAMMING_MODE = "dfu"
36
37
 
@@ -154,10 +155,19 @@ class TempDeckDriver(AbstractTempDeckDriver):
154
155
  Example input from Temp-Deck's serial response:
155
156
  "serial:aa11bb22 model:aa11bb22 version:aa11bb22"
156
157
  """
157
- c = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode(
158
+ device_info = CommandBuilder(terminator=TEMP_DECK_COMMAND_TERMINATOR).add_gcode(
158
159
  gcode=GCODE.DEVICE_INFO
159
160
  )
160
- response = await self._send_command(command=c)
161
+ response = await self._send_command(command=device_info)
162
+
163
+ reset_reason = CommandBuilder(
164
+ terminator=TEMP_DECK_COMMAND_TERMINATOR
165
+ ).add_gcode(gcode=GCODE.GET_RESET_REASON)
166
+ try:
167
+ await self._send_command(command=reset_reason)
168
+ except UnhandledGcode:
169
+ pass
170
+
161
171
  return utils.parse_device_information(device_info_string=response)
162
172
 
163
173
  async def enter_programming_mode(self) -> None: