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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (189) hide show
  1. opentrons/_version.py +2 -2
  2. opentrons/cli/analyze.py +4 -1
  3. opentrons/config/__init__.py +7 -0
  4. opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
  5. opentrons/drivers/heater_shaker/abstract.py +5 -0
  6. opentrons/drivers/heater_shaker/driver.py +10 -0
  7. opentrons/drivers/heater_shaker/simulator.py +4 -0
  8. opentrons/drivers/thermocycler/abstract.py +6 -0
  9. opentrons/drivers/thermocycler/driver.py +61 -10
  10. opentrons/drivers/thermocycler/simulator.py +6 -0
  11. opentrons/drivers/vacuum_module/__init__.py +5 -0
  12. opentrons/drivers/vacuum_module/abstract.py +93 -0
  13. opentrons/drivers/vacuum_module/driver.py +208 -0
  14. opentrons/drivers/vacuum_module/errors.py +39 -0
  15. opentrons/drivers/vacuum_module/simulator.py +85 -0
  16. opentrons/drivers/vacuum_module/types.py +79 -0
  17. opentrons/execute.py +3 -0
  18. opentrons/hardware_control/api.py +24 -5
  19. opentrons/hardware_control/backends/controller.py +8 -2
  20. opentrons/hardware_control/backends/flex_protocol.py +1 -0
  21. opentrons/hardware_control/backends/ot3controller.py +35 -2
  22. opentrons/hardware_control/backends/ot3simulator.py +3 -1
  23. opentrons/hardware_control/backends/ot3utils.py +37 -0
  24. opentrons/hardware_control/backends/simulator.py +2 -1
  25. opentrons/hardware_control/backends/subsystem_manager.py +5 -2
  26. opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
  27. opentrons/hardware_control/emulation/connection_handler.py +8 -5
  28. opentrons/hardware_control/emulation/heater_shaker.py +12 -3
  29. opentrons/hardware_control/emulation/settings.py +1 -1
  30. opentrons/hardware_control/emulation/thermocycler.py +67 -15
  31. opentrons/hardware_control/module_control.py +105 -10
  32. opentrons/hardware_control/modules/__init__.py +3 -0
  33. opentrons/hardware_control/modules/absorbance_reader.py +11 -4
  34. opentrons/hardware_control/modules/flex_stacker.py +38 -9
  35. opentrons/hardware_control/modules/heater_shaker.py +42 -5
  36. opentrons/hardware_control/modules/magdeck.py +8 -4
  37. opentrons/hardware_control/modules/mod_abc.py +14 -6
  38. opentrons/hardware_control/modules/tempdeck.py +25 -5
  39. opentrons/hardware_control/modules/thermocycler.py +68 -11
  40. opentrons/hardware_control/modules/types.py +20 -1
  41. opentrons/hardware_control/modules/utils.py +11 -4
  42. opentrons/hardware_control/motion_utilities.py +6 -6
  43. opentrons/hardware_control/nozzle_manager.py +3 -0
  44. opentrons/hardware_control/ot3api.py +85 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +6 -2
  47. opentrons/hardware_control/scripts/update_module_fw.py +5 -0
  48. opentrons/hardware_control/types.py +43 -2
  49. opentrons/legacy_commands/commands.py +58 -5
  50. opentrons/legacy_commands/module_commands.py +52 -0
  51. opentrons/legacy_commands/protocol_commands.py +53 -1
  52. opentrons/legacy_commands/types.py +155 -1
  53. opentrons/motion_planning/deck_conflict.py +17 -12
  54. opentrons/motion_planning/waypoints.py +15 -29
  55. opentrons/protocol_api/__init__.py +5 -1
  56. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  57. opentrons/protocol_api/_types.py +8 -1
  58. opentrons/protocol_api/core/common.py +3 -1
  59. opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
  60. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  61. opentrons/protocol_api/core/engine/instrument.py +109 -26
  62. opentrons/protocol_api/core/engine/labware.py +8 -1
  63. opentrons/protocol_api/core/engine/module_core.py +95 -4
  64. opentrons/protocol_api/core/engine/protocol.py +51 -2
  65. opentrons/protocol_api/core/engine/stringify.py +2 -0
  66. opentrons/protocol_api/core/engine/tasks.py +48 -0
  67. opentrons/protocol_api/core/engine/well.py +8 -0
  68. opentrons/protocol_api/core/instrument.py +19 -2
  69. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  71. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  72. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  73. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  74. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  75. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  76. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  77. opentrons/protocol_api/core/module.py +58 -2
  78. opentrons/protocol_api/core/protocol.py +23 -2
  79. opentrons/protocol_api/core/tasks.py +31 -0
  80. opentrons/protocol_api/core/well.py +4 -0
  81. opentrons/protocol_api/instrument_context.py +388 -2
  82. opentrons/protocol_api/labware.py +10 -2
  83. opentrons/protocol_api/module_contexts.py +170 -6
  84. opentrons/protocol_api/protocol_context.py +87 -21
  85. opentrons/protocol_api/robot_context.py +41 -25
  86. opentrons/protocol_api/tasks.py +48 -0
  87. opentrons/protocol_api/validation.py +49 -3
  88. opentrons/protocol_engine/__init__.py +4 -0
  89. opentrons/protocol_engine/actions/__init__.py +6 -2
  90. opentrons/protocol_engine/actions/actions.py +31 -9
  91. opentrons/protocol_engine/clients/sync_client.py +42 -7
  92. opentrons/protocol_engine/commands/__init__.py +56 -0
  93. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  94. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  96. opentrons/protocol_engine/commands/aspirate.py +1 -0
  97. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  98. opentrons/protocol_engine/commands/capture_image.py +302 -0
  99. opentrons/protocol_engine/commands/command.py +2 -0
  100. opentrons/protocol_engine/commands/command_unions.py +62 -0
  101. opentrons/protocol_engine/commands/create_timer.py +83 -0
  102. opentrons/protocol_engine/commands/dispense.py +1 -0
  103. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  104. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  105. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  106. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  107. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  108. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  109. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  110. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  111. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  112. opentrons/protocol_engine/commands/move_labware.py +3 -4
  113. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  114. opentrons/protocol_engine/commands/movement_common.py +31 -2
  115. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  116. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  117. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  118. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  119. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  120. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  122. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  123. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  124. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  125. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  126. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  127. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  128. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  129. opentrons/protocol_engine/engine_support.py +3 -0
  130. opentrons/protocol_engine/errors/__init__.py +12 -0
  131. opentrons/protocol_engine/errors/exceptions.py +119 -0
  132. opentrons/protocol_engine/execution/__init__.py +4 -0
  133. opentrons/protocol_engine/execution/command_executor.py +62 -1
  134. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  135. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  136. opentrons/protocol_engine/execution/movement.py +2 -0
  137. opentrons/protocol_engine/execution/pipetting.py +19 -25
  138. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  139. opentrons/protocol_engine/execution/run_control.py +8 -0
  140. opentrons/protocol_engine/execution/task_handler.py +157 -0
  141. opentrons/protocol_engine/protocol_engine.py +137 -36
  142. opentrons/protocol_engine/resources/__init__.py +4 -0
  143. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  144. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  145. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  146. opentrons/protocol_engine/resources/file_provider.py +133 -58
  147. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  148. opentrons/protocol_engine/slot_standardization.py +2 -0
  149. opentrons/protocol_engine/state/_well_math.py +60 -18
  150. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  151. opentrons/protocol_engine/state/camera.py +54 -0
  152. opentrons/protocol_engine/state/commands.py +37 -14
  153. opentrons/protocol_engine/state/geometry.py +276 -379
  154. opentrons/protocol_engine/state/labware.py +62 -108
  155. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  156. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  157. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  158. opentrons/protocol_engine/state/modules.py +30 -8
  159. opentrons/protocol_engine/state/motion.py +44 -0
  160. opentrons/protocol_engine/state/preconditions.py +59 -0
  161. opentrons/protocol_engine/state/state.py +44 -0
  162. opentrons/protocol_engine/state/state_summary.py +4 -0
  163. opentrons/protocol_engine/state/tasks.py +139 -0
  164. opentrons/protocol_engine/state/tips.py +177 -258
  165. opentrons/protocol_engine/state/update_types.py +26 -9
  166. opentrons/protocol_engine/types/__init__.py +23 -4
  167. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  168. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  169. opentrons/protocol_engine/types/instrument.py +8 -1
  170. opentrons/protocol_engine/types/labware.py +1 -13
  171. opentrons/protocol_engine/types/location.py +26 -2
  172. opentrons/protocol_engine/types/module.py +11 -1
  173. opentrons/protocol_engine/types/tasks.py +38 -0
  174. opentrons/protocol_engine/types/tip.py +9 -0
  175. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  176. opentrons/protocol_runner/protocol_runner.py +14 -1
  177. opentrons/protocol_runner/run_orchestrator.py +49 -2
  178. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  179. opentrons/protocols/api_support/definitions.py +1 -1
  180. opentrons/protocols/api_support/types.py +2 -1
  181. opentrons/simulate.py +51 -15
  182. opentrons/system/camera.py +334 -4
  183. opentrons/system/ffmpeg.py +110 -0
  184. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +188 -160
  186. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  187. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,208 @@
1
+ import asyncio
2
+ import re
3
+ from typing import Optional
4
+
5
+ from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection
6
+ from .abstract import AbstractVacuumModuleDriver
7
+ from .types import LEDColor, LEDPattern, GCODE, VacuumModuleInfo, HardwareRevision
8
+ from .errors import VacuumModuleErrorCodes
9
+
10
+
11
+ VM_BAUDRATE = 115200
12
+ DEFAULT_VM_TIMEOUT = 5
13
+ VM_ACK = "OK\n"
14
+ VM_ERROR_KEYWORD = "err"
15
+ VM_ASYNC_ERROR_ACK = "async"
16
+ DEFAULT_COMMAND_RETRIES = 2
17
+ GCODE_ROUNDING_PRECISION = 2
18
+
19
+ # LED animation range values
20
+ MIN_DURATION_MS = 25 # 25ms
21
+ MAX_DURATION_MS = 10000 # 10s
22
+ MAX_REPS = 10
23
+
24
+
25
+ class VacuumModuleDriver(AbstractVacuumModuleDriver):
26
+ """Driver for Opentrons Vacuum Module."""
27
+
28
+ @classmethod
29
+ def parse_device_info(cls, response: str) -> VacuumModuleInfo:
30
+ """Parse vacuum module info."""
31
+ _RE = re.compile(
32
+ f"^{GCODE.GET_DEVICE_INFO} FW:(?P<fw>\\S+) HW:Opentrons-vacuum-module-(?P<hw>\\S+) SerialNo:(?P<sn>\\S+)$"
33
+ )
34
+ m = _RE.match(response)
35
+ if not m:
36
+ raise ValueError(f"Incorrect Response for device info: {response}")
37
+ return VacuumModuleInfo(
38
+ m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn")
39
+ )
40
+
41
+ @classmethod
42
+ def parse_reset_reason(cls, response: str) -> int:
43
+ """Parse the reset reason"""
44
+ _RE = re.compile(rf"^{GCODE.GET_RESET_REASON} R:(?P<R>\d)$")
45
+ match = _RE.match(response)
46
+ if not match:
47
+ raise ValueError(f"Incorrect Response for reset reason: {response}")
48
+ return int(match.group("R"))
49
+
50
+ @classmethod
51
+ async def create(
52
+ cls, port: str, loop: Optional[asyncio.AbstractEventLoop]
53
+ ) -> "VacuumModuleDriver":
54
+ """Create a Vacuum Module driver."""
55
+ connection = await AsyncResponseSerialConnection.create(
56
+ port=port,
57
+ baud_rate=VM_BAUDRATE,
58
+ timeout=DEFAULT_VM_TIMEOUT,
59
+ number_of_retries=DEFAULT_COMMAND_RETRIES,
60
+ ack=VM_ACK,
61
+ loop=loop,
62
+ error_keyword=VM_ERROR_KEYWORD,
63
+ async_error_ack=VM_ASYNC_ERROR_ACK,
64
+ reset_buffer_before_write=True,
65
+ error_codes=VacuumModuleErrorCodes,
66
+ )
67
+ return cls(connection)
68
+
69
+ def __init__(self, connection: AsyncResponseSerialConnection) -> None:
70
+ """
71
+ Constructor
72
+
73
+ Args:
74
+ connection: connection to the vacuum module
75
+ """
76
+ self._connection = connection
77
+
78
+ async def connect(self) -> None:
79
+ """Connect to vacuum module."""
80
+ await self._connection.open()
81
+
82
+ async def disconnect(self) -> None:
83
+ """Disconnect from vacuum module."""
84
+ await self._connection.close()
85
+
86
+ async def is_connected(self) -> bool:
87
+ """Check connection to vacuum module."""
88
+ return await self._connection.is_open()
89
+
90
+ async def reset_serial_buffers(self) -> None:
91
+ """Reset the input and output serial buffers."""
92
+ self._connection._serial.reset_input_buffer()
93
+ self._connection._serial.reset_output_buffer()
94
+
95
+ async def get_device_info(self) -> VacuumModuleInfo:
96
+ """Get Device Info."""
97
+ response = await self._connection.send_command(
98
+ GCODE.GET_DEVICE_INFO.build_command()
99
+ )
100
+ device_info = self.parse_device_info(response)
101
+ reason_resp = await self._connection.send_command(
102
+ GCODE.GET_RESET_REASON.build_command()
103
+ )
104
+ reason = self.parse_reset_reason(reason_resp)
105
+ device_info.rr = reason
106
+ return device_info
107
+
108
+ async def enter_programming_mode(self) -> None:
109
+ """Reboot into programming mode"""
110
+ command = GCODE.ENTER_BOOTLOADER.build_command()
111
+ await self._connection.send_dfu_command(command)
112
+ await self._connection.close()
113
+
114
+ async def set_serial_number(self, sn: str) -> None:
115
+ """Set Serial Number."""
116
+ if not re.match(r"^VM[\w]{1}[\d]{2}[\d]{8}[\d]+$", sn):
117
+ raise ValueError(
118
+ f"Invalid serial number: ({sn}) expected format: VMA1020250119001"
119
+ )
120
+ resp = await self._connection.send_command(
121
+ GCODE.SET_SERIAL_NUMBER.build_command().add_element(sn)
122
+ )
123
+ if not re.match(rf"^{GCODE.SET_SERIAL_NUMBER}$", resp):
124
+ raise ValueError(f"Incorrect Response for set serial number: {resp}")
125
+
126
+ async def set_led(
127
+ self,
128
+ power: float,
129
+ color: Optional[LEDColor] = None,
130
+ external: Optional[bool] = None,
131
+ pattern: Optional[LEDPattern] = None,
132
+ duration: Optional[int] = None,
133
+ reps: Optional[int] = None,
134
+ ) -> None:
135
+ """Set LED Status bar color and pattern.
136
+
137
+ :param power: Power of the LED (0-1.0), 0 is off, 1 is full power
138
+ :param color: Color of the LED
139
+ :param external: True if external LED, False if internal LED
140
+ :param pattern: Animation pattern of the LED status bar
141
+ :param duration: Animation duration in milliseconds (25-10000), 10s max
142
+ :param reps: Number of times to repeat the animation (-1 - 10), -1 is forever.
143
+ """
144
+ power = max(0, min(power, 1.0))
145
+ command = GCODE.SET_LED.build_command().add_float(
146
+ "P", power, GCODE_ROUNDING_PRECISION
147
+ )
148
+ if color is not None:
149
+ command.add_int("C", color.value)
150
+ if external is not None:
151
+ command.add_int("K", int(external))
152
+ if pattern is not None:
153
+ command.add_int("A", pattern.value)
154
+ if duration is not None:
155
+ duration = max(MIN_DURATION_MS, min(duration, MAX_DURATION_MS))
156
+ command.add_int("D", duration)
157
+ if reps is not None:
158
+ command.add_int("R", max(-1, min(reps, MAX_REPS)))
159
+ resp = await self._connection.send_command(command)
160
+ if not re.match(rf"^{GCODE.SET_LED}$", resp):
161
+ raise ValueError(f"Incorrect Response for set led: {resp}")
162
+
163
+ async def enable_pump(self) -> None:
164
+ """Enables the vacuum pump, does not turn it on."""
165
+ ...
166
+
167
+ async def disable_pump(self) -> None:
168
+ """Disable the vacuum pump, doesn't just turn it off."""
169
+ ...
170
+
171
+ async def get_pump_motor_register(self) -> None:
172
+ """Get the register value of the pump motor driver."""
173
+ ...
174
+
175
+ async def get_pressure_sensor_register(self) -> None:
176
+ """Get the register value of the pressure sensor driver."""
177
+ ...
178
+
179
+ async def get_pressure_sensor_reading_psi(self) -> float:
180
+ """Get a reading from the pressure sensor."""
181
+ return 0.0
182
+
183
+ async def get_gage_pressure_reading_mbarg(self) -> float:
184
+ """Read each pressure sensor and return the pressure difference."""
185
+ return 0.0
186
+
187
+ async def set_vacuum_chamber_pressure(
188
+ self,
189
+ gage_pressure_mbarg: float,
190
+ duration: Optional[float],
191
+ rate: Optional[float],
192
+ ) -> None:
193
+ """Engage or release the vacuum until a desired internal pressure is reached."""
194
+ ...
195
+
196
+ # TODO: change pump power to be more specific when we find out how were gonna operate that
197
+ async def engage_vacuum(self, pump_power: Optional[float] = None) -> None:
198
+ """Engage the vacuum without regard to chamber pressure."""
199
+ ...
200
+
201
+ async def disengage_vacuum_pump(self) -> None:
202
+ """Stops the vacuum pump, doesn't vent air or disable the motor."""
203
+ ...
204
+
205
+ # turns off motor, then releases, takes a timeout for buffer between turn off and vent
206
+ async def vent(self) -> None:
207
+ """Release the vacuum in the module chamber."""
208
+ ...
@@ -0,0 +1,39 @@
1
+ """Vacuum Module-specific errors and exceptions."""
2
+
3
+ from opentrons.drivers.asyncio.communication.errors import (
4
+ BaseErrorCode,
5
+ ErrorResponse,
6
+ GCodeCacheFull,
7
+ TaskNotReady,
8
+ UnhandledGcode,
9
+ )
10
+
11
+
12
+ class EStopTriggered(ErrorResponse):
13
+ """Raised when the estop is triggered during a module action."""
14
+
15
+ def __init__(self, port: str, response: str, command: str) -> None:
16
+ super().__init__(port, response, command)
17
+
18
+
19
+ class PumpMotorError(ErrorResponse):
20
+ """Raised when pump motor error is received."""
21
+
22
+ def __init__(self, port: str, response: str, command: str) -> None:
23
+ super().__init__(port, response, command)
24
+
25
+
26
+ class StopRequested(ErrorResponse):
27
+ """Raised when a stop is requested during a module action."""
28
+
29
+ def __init__(self, port: str, response: str, command: str) -> None:
30
+ super().__init__(port, response, command)
31
+
32
+
33
+ class VacuumModuleErrorCodes(BaseErrorCode):
34
+ """Vacuum Module Error Codes."""
35
+
36
+ UNHANDLED_GCODE = ("ERR003", UnhandledGcode)
37
+ GCODE_CACHE_FULL = ("ERR004", GCodeCacheFull)
38
+ TASK_NOT_READY = ("ERR007", TaskNotReady)
39
+ STOP_REQUESTED = ("ERR504", StopRequested)
@@ -0,0 +1,85 @@
1
+ from typing import Optional
2
+
3
+ from .abstract import AbstractVacuumModuleDriver
4
+ from .types import VacuumModuleInfo, HardwareRevision, LEDPattern, LEDColor
5
+ from opentrons.util.async_helpers import ensure_yield
6
+
7
+
8
+ class SimulatingDriver(AbstractVacuumModuleDriver):
9
+ def __init__(self, serial_number: str) -> None:
10
+ self._serial_number = serial_number
11
+ self.vacuum_on = False
12
+ self.pump_enabled = False
13
+ self.pressure_sensor_enabled = False
14
+
15
+ @ensure_yield
16
+ async def connect(self) -> None:
17
+ pass
18
+
19
+ @ensure_yield
20
+ async def disconnect(self) -> None:
21
+ pass
22
+
23
+ @ensure_yield
24
+ async def is_connected(self) -> bool:
25
+ return True
26
+
27
+ async def get_device_info(self) -> VacuumModuleInfo:
28
+ return VacuumModuleInfo(
29
+ fw="vacuum-fw", hw=HardwareRevision.NFF, sn=self._serial_number
30
+ )
31
+
32
+ async def set_serial_number(self, sn: str) -> None:
33
+ self._serial_number = sn
34
+
35
+ async def enable_pump(self) -> None:
36
+ self.pump_enabled = True
37
+
38
+ async def disable_pump(self) -> None:
39
+ self.pump_enabled = False
40
+
41
+ async def get_pump_motor_register(self) -> None:
42
+ """Get the register value of the pump motor driver."""
43
+ pass
44
+
45
+ async def get_pressure_sensor_register(self) -> None:
46
+ """Get the register value of the pressure sensor driver."""
47
+ pass
48
+
49
+ async def get_pressure_sensor_reading(self) -> float:
50
+ """Get a reading from the pressure sensor."""
51
+ return 0.0
52
+
53
+ # TODO: update the pressure arg with the units when we find out which unit
54
+ async def set_vacuum_chamber_pressure(
55
+ self,
56
+ guage_pressure_mbar: float,
57
+ duration: Optional[float],
58
+ rate: Optional[float],
59
+ vent_after: bool = False,
60
+ ) -> None:
61
+ """Engage or release the vacuum until a desired internal pressure is reached."""
62
+ pass
63
+
64
+ async def engage_vacuum(self, pump_power: Optional[float] = None) -> None:
65
+ self.vacuum_on = True
66
+
67
+ async def vent(self, delay_s: float = 0.0) -> None:
68
+ self.vacuum_on = False
69
+
70
+ async def set_led(
71
+ self,
72
+ power: float,
73
+ color: Optional[LEDColor] = None,
74
+ external: Optional[bool] = None,
75
+ pattern: Optional[LEDPattern] = None,
76
+ duration: Optional[int] = None, # Default firmware duration is 500ms
77
+ reps: Optional[int] = None, # Default firmware reps is 0
78
+ ) -> None:
79
+ pass
80
+
81
+ async def enter_programming_mode(self) -> None:
82
+ pass
83
+
84
+ async def reset_serial_buffers(self) -> None:
85
+ pass
@@ -0,0 +1,79 @@
1
+ from enum import Enum
2
+ from dataclasses import dataclass
3
+ from typing import Dict
4
+
5
+ from opentrons.drivers.command_builder import CommandBuilder
6
+
7
+
8
+ class GCODE(str, Enum):
9
+
10
+ GET_RESET_REASON = "M114"
11
+ GET_DEVICE_INFO = "M115"
12
+ SET_SERIAL_NUMBER = "M996"
13
+ ENTER_BOOTLOADER = "dfu"
14
+ SET_LED = "M200"
15
+
16
+ def build_command(self) -> CommandBuilder:
17
+ """Build command."""
18
+ return CommandBuilder().add_gcode(self)
19
+
20
+
21
+ class HardwareRevision(Enum):
22
+ """Hardware Revision."""
23
+
24
+ NFF = "nff"
25
+
26
+
27
+ @dataclass
28
+ class VacuumModuleInfo:
29
+ """Vacuum module info."""
30
+
31
+ fw: str
32
+ hw: HardwareRevision
33
+ sn: str
34
+ rr: int = 0
35
+
36
+ def to_dict(self) -> Dict[str, str]:
37
+ """Build vacuum module info."""
38
+ return {
39
+ "serial": self.sn,
40
+ "version": self.fw,
41
+ "model": self.hw.value,
42
+ "reset_reason": str(self.rr),
43
+ }
44
+
45
+
46
+ class LEDColor(Enum):
47
+ """Vacuum Module LED Color."""
48
+
49
+ WHITE = 0
50
+ RED = 1
51
+ GREEN = 2
52
+ BLUE = 3
53
+ YELLOW = 4
54
+
55
+ @classmethod
56
+ def from_name(cls, name: str) -> "LEDColor":
57
+ match (name.lower()):
58
+ case "red":
59
+ return cls.RED
60
+ case "green":
61
+ return cls.GREEN
62
+ case "blue":
63
+ return cls.BLUE
64
+ case "yellow":
65
+ return cls.YELLOW
66
+ case _:
67
+ return cls.WHITE
68
+
69
+ def to_name(self) -> "str":
70
+ return self.name.lower()
71
+
72
+
73
+ class LEDPattern(Enum):
74
+ """Vacuum Module LED Pattern."""
75
+
76
+ STATIC = 0
77
+ FLASH = 1
78
+ PULSE = 2
79
+ CONFIRM = 3
opentrons/execute.py CHANGED
@@ -555,6 +555,7 @@ def _create_live_context_pe(
555
555
  load_fixed_trash=should_load_fixed_trash_labware_for_python_protocol(
556
556
  api_version
557
557
  ),
558
+ camera_provider=None,
558
559
  )
559
560
  )
560
561
 
@@ -644,6 +645,7 @@ def _run_file_pe(
644
645
  hardware_api=hardware_api_wrapped,
645
646
  )
646
647
 
648
+ # todo (chb, 2025-09-30): The Camera Provider is provided in to a run by the robot server, no analog exists for execute
647
649
  orchestrator = RunOrchestrator(
648
650
  hardware_api=hardware_api_wrapped,
649
651
  protocol_engine=protocol_engine,
@@ -657,6 +659,7 @@ def _run_file_pe(
657
659
  protocol_live_runner=LiveRunner(
658
660
  protocol_engine=protocol_engine, hardware_api=hardware_api_wrapped
659
661
  ),
662
+ camera_provider=None,
660
663
  )
661
664
 
662
665
  unsubscribe = protocol_runner.broker.subscribe(
@@ -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:
@@ -176,6 +176,7 @@ class FlexBackend(Protocol):
176
176
  speed: float,
177
177
  stop_condition: HWStopCondition = HWStopCondition.none,
178
178
  nodes_in_moves_only: bool = True,
179
+ delay: Optional[Tuple[List[Axis], float]] = None,
179
180
  ) -> None:
180
181
  """Move to a position.
181
182
 
@@ -49,6 +49,7 @@ from .ot3utils import (
49
49
  gripper_jaw_state_from_fw,
50
50
  get_system_constraints,
51
51
  get_system_constraints_for_plunger_acceleration,
52
+ add_delay_to_move_group,
52
53
  )
53
54
  from .tip_presence_manager import TipPresenceManager
54
55
 
@@ -657,11 +658,17 @@ class OT3Controller(FlexBackend):
657
658
  speed: float,
658
659
  stop_condition: HWStopCondition,
659
660
  nodes_in_moves_only: bool,
661
+ delay: Optional[Tuple[List[Axis], float]] = None,
660
662
  ) -> Tuple[Optional[MoveGroupRunner], bool]:
661
663
  if not target:
662
664
  return None, False
663
- move_target = MoveTarget.build(position=target, max_speed=speed)
665
+ # Create a target that doesn't incorporate the plunger into a joint axis with the gantry
666
+ plunger_axes = [Axis.P_L, Axis.P_R]
667
+
664
668
  try:
669
+ move_target = self._move_manager.devectorize_axes(
670
+ origin, target, speed, plunger_axes
671
+ )
665
672
  _, movelist = self._move_manager.plan_motion(
666
673
  origin=origin, target_list=[move_target]
667
674
  )
@@ -683,6 +690,28 @@ class OT3Controller(FlexBackend):
683
690
  move_group, _ = create_move_group(
684
691
  origin, moves, ordered_nodes, MoveStopCondition[stop_condition.name]
685
692
  )
693
+
694
+ if delay is not None:
695
+ delay_axes, delay_time = delay
696
+ delay_nodes = [axis_to_node(ax) for ax in delay_axes]
697
+ move_group = add_delay_to_move_group(
698
+ move_group, ordered_nodes, (delay_nodes, delay_time)
699
+ )
700
+
701
+ (
702
+ plunger_slowed,
703
+ error_str,
704
+ ) = self._move_manager.ensure_pipette_flow_rate_unchanged(
705
+ [node_to_axis(node) for node in ordered_nodes],
706
+ origin,
707
+ target,
708
+ speed,
709
+ move_group,
710
+ [(ax, axis_to_node(ax)) for ax in plunger_axes],
711
+ )
712
+ if plunger_slowed:
713
+ log.error(error_str)
714
+
686
715
  return (
687
716
  MoveGroupRunner(
688
717
  move_groups=[move_group],
@@ -728,6 +757,7 @@ class OT3Controller(FlexBackend):
728
757
  speed: float,
729
758
  stop_condition: HWStopCondition = HWStopCondition.none,
730
759
  nodes_in_moves_only: bool = True,
760
+ delay: Optional[Tuple[List[Axis], float]] = None,
731
761
  ) -> None:
732
762
  """Move to a position.
733
763
 
@@ -750,7 +780,7 @@ class OT3Controller(FlexBackend):
750
780
 
751
781
  maybe_runners = (
752
782
  self._build_move_node_axis_runner(
753
- origin, target, speed, stop_condition, nodes_in_moves_only
783
+ origin, target, speed, stop_condition, nodes_in_moves_only, delay
754
784
  ),
755
785
  self._build_move_gear_axis_runner(
756
786
  possible_q_axis_origin,
@@ -1398,6 +1428,9 @@ class OT3Controller(FlexBackend):
1398
1428
  except RuntimeError:
1399
1429
  return
1400
1430
 
1431
+ if hasattr(self, "_module_controls") and self._module_controls is not None:
1432
+ await self._module_controls.clean_up()
1433
+
1401
1434
  if hasattr(self, "_event_watcher"):
1402
1435
  if (
1403
1436
  loop.is_running()
@@ -367,6 +367,7 @@ class OT3Simulator(FlexBackend):
367
367
  speed: Optional[float] = None,
368
368
  stop_condition: HWStopCondition = HWStopCondition.none,
369
369
  nodes_in_moves_only: bool = True,
370
+ delay: Optional[Tuple[List[Axis], float]] = None,
370
371
  ) -> None:
371
372
  """Move to a position.
372
373
 
@@ -728,7 +729,8 @@ class OT3Simulator(FlexBackend):
728
729
  @ensure_yield
729
730
  async def clean_up(self) -> None:
730
731
  """Clean up."""
731
- pass
732
+ if hasattr(self, "_module_controls") and self._module_controls is not None:
733
+ await self._module_controls.clean_up()
732
734
 
733
735
  @staticmethod
734
736
  def _get_home_position() -> Dict[Axis, float]:
@@ -1,4 +1,5 @@
1
1
  """Shared utilities for ot3 hardware control."""
2
+ import copy
2
3
  from typing import Dict, Iterable, List, Set, Tuple, TypeVar, cast, Sequence, Optional
3
4
  from typing_extensions import Literal
4
5
  from logging import getLogger
@@ -57,6 +58,8 @@ from opentrons_hardware.hardware_control.motion import (
57
58
  MoveStopCondition,
58
59
  create_gripper_jaw_step,
59
60
  create_tip_action_step,
61
+ SingleMoveStep,
62
+ MoveGroupSingleAxisStep,
60
63
  )
61
64
  from opentrons_hardware.hardware_control.constants import interrupts_per_sec
62
65
 
@@ -376,6 +379,40 @@ def motor_nodes(devices: Set[FirmwareTarget]) -> Set[NodeId]:
376
379
  return {NodeId(target) for target in motor_nodes if target in NodeId}
377
380
 
378
381
 
382
+ def add_delay_to_move_group(
383
+ group: MoveGroup,
384
+ present_nodes: Iterable[NodeId],
385
+ delay: Tuple[List[NodeId], float],
386
+ ) -> MoveGroup:
387
+ delay_nodes, delay_time = delay
388
+ if delay_time == 0.0:
389
+ return group
390
+
391
+ as_single_moves: Dict[NodeId, List[SingleMoveStep]] = {}
392
+ for node in present_nodes:
393
+ as_single_moves[node] = [step[node] for step in group]
394
+
395
+ delay_step = MoveGroupSingleAxisStep(
396
+ distance_mm=np.float64(0),
397
+ velocity_mm_sec=np.float64(0),
398
+ duration_sec=np.float64(delay_time),
399
+ )
400
+ for node in present_nodes:
401
+ if node in delay_nodes:
402
+ # Add the delay at the beginning
403
+ as_single_moves[node] = [copy.deepcopy(delay_step)] + as_single_moves[node]
404
+ else:
405
+ # Add the delay at the end.
406
+ as_single_moves[node] = as_single_moves[node] + [copy.deepcopy(delay_step)]
407
+
408
+ new_move_group: MoveGroup = []
409
+ for i in range(len(group) + 1):
410
+ new_move_group.append(
411
+ {node: as_single_moves[node][i] for node in present_nodes}
412
+ )
413
+ return new_move_group
414
+
415
+
379
416
  def create_move_group(
380
417
  origin: Coordinates[Axis, CoordinateValue],
381
418
  moves: List[Move[Axis]],