opentrons 8.7.0a7__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 (109) 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 +8 -5
  5. opentrons/drivers/flex_stacker/driver.py +6 -1
  6. opentrons/drivers/vacuum_module/__init__.py +5 -0
  7. opentrons/drivers/vacuum_module/abstract.py +93 -0
  8. opentrons/drivers/vacuum_module/driver.py +208 -0
  9. opentrons/drivers/vacuum_module/errors.py +39 -0
  10. opentrons/drivers/vacuum_module/simulator.py +85 -0
  11. opentrons/drivers/vacuum_module/types.py +79 -0
  12. opentrons/execute.py +3 -0
  13. opentrons/hardware_control/backends/flex_protocol.py +2 -0
  14. opentrons/hardware_control/backends/ot3controller.py +35 -2
  15. opentrons/hardware_control/backends/ot3simulator.py +2 -0
  16. opentrons/hardware_control/backends/ot3utils.py +37 -0
  17. opentrons/hardware_control/module_control.py +23 -2
  18. opentrons/hardware_control/modules/mod_abc.py +1 -1
  19. opentrons/hardware_control/modules/types.py +1 -1
  20. opentrons/hardware_control/motion_utilities.py +6 -6
  21. opentrons/hardware_control/ot3api.py +62 -13
  22. opentrons/hardware_control/protocols/gripper_controller.py +1 -0
  23. opentrons/hardware_control/protocols/liquid_handler.py +6 -2
  24. opentrons/hardware_control/types.py +12 -0
  25. opentrons/legacy_commands/commands.py +58 -5
  26. opentrons/legacy_commands/module_commands.py +29 -0
  27. opentrons/legacy_commands/protocol_commands.py +33 -1
  28. opentrons/legacy_commands/types.py +75 -1
  29. opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
  30. opentrons/protocol_api/_types.py +2 -0
  31. opentrons/protocol_api/core/engine/_default_labware_versions.py +1 -0
  32. opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
  33. opentrons/protocol_api/core/engine/instrument.py +109 -26
  34. opentrons/protocol_api/core/engine/module_core.py +27 -3
  35. opentrons/protocol_api/core/engine/protocol.py +33 -1
  36. opentrons/protocol_api/core/engine/stringify.py +2 -0
  37. opentrons/protocol_api/core/instrument.py +19 -2
  38. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  39. opentrons/protocol_api/core/legacy/legacy_module_core.py +15 -4
  40. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +12 -0
  41. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  42. opentrons/protocol_api/core/module.py +25 -2
  43. opentrons/protocol_api/core/protocol.py +12 -0
  44. opentrons/protocol_api/instrument_context.py +388 -2
  45. opentrons/protocol_api/labware.py +5 -2
  46. opentrons/protocol_api/module_contexts.py +133 -30
  47. opentrons/protocol_api/protocol_context.py +61 -17
  48. opentrons/protocol_api/robot_context.py +3 -4
  49. opentrons/protocol_api/validation.py +43 -2
  50. opentrons/protocol_engine/__init__.py +4 -0
  51. opentrons/protocol_engine/actions/__init__.py +2 -0
  52. opentrons/protocol_engine/actions/actions.py +9 -0
  53. opentrons/protocol_engine/commands/__init__.py +14 -0
  54. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  55. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  56. opentrons/protocol_engine/commands/capture_image.py +302 -0
  57. opentrons/protocol_engine/commands/command.py +1 -0
  58. opentrons/protocol_engine/commands/command_unions.py +13 -0
  59. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  60. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  61. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  62. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +1 -1
  63. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
  64. opentrons/protocol_engine/commands/move_labware.py +3 -4
  65. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  66. opentrons/protocol_engine/commands/movement_common.py +29 -2
  67. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  68. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +12 -9
  69. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +17 -12
  70. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +1 -1
  71. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  72. opentrons/protocol_engine/engine_support.py +3 -0
  73. opentrons/protocol_engine/errors/__init__.py +8 -0
  74. opentrons/protocol_engine/errors/exceptions.py +64 -0
  75. opentrons/protocol_engine/execution/__init__.py +2 -0
  76. opentrons/protocol_engine/execution/command_executor.py +54 -1
  77. opentrons/protocol_engine/execution/create_queue_worker.py +4 -1
  78. opentrons/protocol_engine/execution/labware_movement.py +13 -4
  79. opentrons/protocol_engine/execution/pipetting.py +19 -25
  80. opentrons/protocol_engine/protocol_engine.py +62 -2
  81. opentrons/protocol_engine/resources/__init__.py +2 -0
  82. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  83. opentrons/protocol_engine/resources/file_provider.py +133 -58
  84. opentrons/protocol_engine/slot_standardization.py +2 -0
  85. opentrons/protocol_engine/state/camera.py +54 -0
  86. opentrons/protocol_engine/state/commands.py +24 -4
  87. opentrons/protocol_engine/state/geometry.py +68 -10
  88. opentrons/protocol_engine/state/labware.py +10 -6
  89. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +6 -1
  90. opentrons/protocol_engine/state/modules.py +9 -0
  91. opentrons/protocol_engine/state/preconditions.py +59 -0
  92. opentrons/protocol_engine/state/state.py +30 -0
  93. opentrons/protocol_engine/state/state_summary.py +2 -0
  94. opentrons/protocol_engine/state/update_types.py +10 -0
  95. opentrons/protocol_engine/types/__init__.py +14 -1
  96. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  97. opentrons/protocol_engine/types/location.py +26 -2
  98. opentrons/protocol_engine/types/module.py +1 -1
  99. opentrons/protocol_runner/protocol_runner.py +14 -1
  100. opentrons/protocol_runner/run_orchestrator.py +31 -0
  101. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  102. opentrons/simulate.py +3 -0
  103. opentrons/system/camera.py +333 -3
  104. opentrons/system/ffmpeg.py +110 -0
  105. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
  106. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +109 -97
  107. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
  108. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
  109. {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
opentrons/_version.py CHANGED
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '8.7.0a7'
32
- __version_tuple__ = version_tuple = (8, 7, 0, 'a7')
31
+ __version__ = version = '8.8.0a7'
32
+ __version_tuple__ = version_tuple = (8, 8, 0, 'a7')
33
33
 
34
34
  __commit_id__ = commit_id = None
opentrons/cli/analyze.py CHANGED
@@ -60,7 +60,7 @@ from opentrons.protocol_engine import (
60
60
  StateSummary,
61
61
  )
62
62
  from opentrons.protocol_engine.protocol_engine import code_in_error_tree
63
- from opentrons.protocol_engine.types import CommandAnnotation
63
+ from opentrons.protocol_engine.types import CommandAnnotation, CommandPreconditions
64
64
 
65
65
  from opentrons_shared_data.robot.types import RobotType
66
66
 
@@ -367,6 +367,7 @@ async def _do_analyze(
367
367
  ),
368
368
  parameters=[],
369
369
  command_annotations=[],
370
+ command_preconditions=None,
370
371
  )
371
372
  return analysis
372
373
  return await orchestrator.run(deck_configuration=[])
@@ -465,6 +466,7 @@ async def _analyze( # noqa: C901
465
466
  liquids=analysis.state_summary.liquids,
466
467
  commandAnnotations=analysis.command_annotations,
467
468
  liquidClasses=analysis.state_summary.liquidClasses,
469
+ commandPreconditions=analysis.command_preconditions,
468
470
  )
469
471
 
470
472
  _call_for_output_of_kind(
@@ -555,3 +557,4 @@ class AnalyzeResults(BaseModel):
555
557
  liquidClasses: List[LiquidClassRecordWithId]
556
558
  errors: List[ErrorOccurrence]
557
559
  commandAnnotations: List[CommandAnnotation]
560
+ commandPreconditions: Optional[CommandPreconditions]
@@ -307,6 +307,13 @@ CONFIG_ELEMENTS = (
307
307
  ConfigElementType.DIR,
308
308
  "The dir where performance metrics are stored",
309
309
  ),
310
+ ConfigElement(
311
+ "live_stream_environment_file",
312
+ "Live Stream Configuration",
313
+ Path("opentrons-live-stream.env"),
314
+ ConfigElementType.FILE,
315
+ "The file storing the Opentrons Live Stream Configuration values.",
316
+ ),
310
317
  )
311
318
  #: The available configuration file elements to modify. All of these can be
312
319
  #: changed by editing opentrons.json, where the keys are the name elements,
@@ -483,7 +483,10 @@ class AsyncResponseSerialConnection(SerialConnection):
483
483
  )
484
484
 
485
485
  async def send_command(
486
- self, command: CommandBuilder, retries: int = 0, timeout: float | None = None
486
+ self,
487
+ command: CommandBuilder,
488
+ retries: int | None = None,
489
+ timeout: float | None = None,
487
490
  ) -> str:
488
491
  """
489
492
  Send a command and return the response.
@@ -499,7 +502,7 @@ class AsyncResponseSerialConnection(SerialConnection):
499
502
  """
500
503
  return await self.send_data(
501
504
  data=command.build(),
502
- retries=retries or self._number_of_retries,
505
+ retries=retries if retries is not None else self._number_of_retries,
503
506
  timeout=timeout,
504
507
  )
505
508
 
@@ -515,7 +518,7 @@ class AsyncResponseSerialConnection(SerialConnection):
515
518
  )
516
519
 
517
520
  async def send_data(
518
- self, data: str, retries: int = 0, timeout: float | None = None
521
+ self, data: str, retries: int | None = None, timeout: float | None = None
519
522
  ) -> str:
520
523
  """
521
524
  Send data and return the response.
@@ -533,7 +536,8 @@ class AsyncResponseSerialConnection(SerialConnection):
533
536
  "timeout", timeout
534
537
  ):
535
538
  return await self._send_data(
536
- data=data, retries=retries or self._number_of_retries
539
+ data=data,
540
+ retries=retries if retries is not None else self._number_of_retries,
537
541
  )
538
542
 
539
543
  async def _consume_responses(
@@ -618,7 +622,6 @@ class AsyncResponseSerialConnection(SerialConnection):
618
622
 
619
623
  Raises: SerialException from an error ack to this command or an async error.
620
624
  """
621
- retries = retries or self._number_of_retries
622
625
  responses: list[str] = []
623
626
 
624
627
  for retry in range(retries + 1):
@@ -461,7 +461,12 @@ class FlexStackerDriver(AbstractFlexStackerDriver):
461
461
  command = GCODE.GET_TOF_MEASUREMENT.build_command().add_element(sensor.name)
462
462
  if resend:
463
463
  command.add_element("R")
464
- resp = await self._connection.send_command(command)
464
+
465
+ # Note: We DONT want to auto resend the request if it fails, because the
466
+ # firmware will send the next frame id instead of the current one missed.
467
+ # So lets set `retries=0` so we only send the frame once and we can
468
+ # use the retry mechanism of the `get_tof_histogram` method instead.
469
+ resp = await self._connection.send_command(command, retries=0)
465
470
  return self.parse_get_tof_measurement(resp)
466
471
 
467
472
  async def get_tof_histogram(self, sensor: TOFSensor) -> TOFMeasurementResult:
@@ -0,0 +1,5 @@
1
+ from .driver import VacuumModuleDriver
2
+ from .simulator import SimulatingDriver
3
+ from .abstract import AbstractVacuumModuleDriver
4
+
5
+ __all__ = ["VacuumModuleDriver", "SimulatingDriver", "AbstractVacuumModuleDriver"]
@@ -0,0 +1,93 @@
1
+ from typing import Protocol, Optional
2
+
3
+ from .types import VacuumModuleInfo, LEDColor, LEDPattern
4
+
5
+
6
+ class AbstractVacuumModuleDriver(Protocol):
7
+ """Protocol for the Vacuum Module driver."""
8
+
9
+ async def connect(self) -> None:
10
+ """Connect to vacuum module."""
11
+ ...
12
+
13
+ async def disconnect(self) -> None:
14
+ """Disconnect from vacuum module."""
15
+ ...
16
+
17
+ async def is_connected(self) -> bool:
18
+ """Check connection to vacuum module."""
19
+ ...
20
+
21
+ async def get_device_info(self) -> VacuumModuleInfo:
22
+ """Get Device Info."""
23
+ ...
24
+
25
+ async def set_serial_number(self, sn: str) -> None:
26
+ """Set Serial Number."""
27
+ ...
28
+
29
+ async def enable_pump(self) -> None:
30
+ """Enable the vacuum pump."""
31
+ ...
32
+
33
+ async def disable_pump(self) -> None:
34
+ """Disable the vacuum pump."""
35
+ ...
36
+
37
+ async def get_pump_motor_register(self) -> None:
38
+ """Get the register value of the pump motor driver."""
39
+ ...
40
+
41
+ async def get_pressure_sensor_register(self) -> None:
42
+ """Get the register value of the pressure sensor driver."""
43
+ ...
44
+
45
+ async def get_pressure_sensor_reading_psi(self) -> float:
46
+ """Get a reading from the pressure sensor."""
47
+ ...
48
+
49
+ async def set_vacuum_chamber_pressure(
50
+ self,
51
+ gage_pressure_mbarg: float,
52
+ duration: Optional[float],
53
+ rate: Optional[float],
54
+ ) -> None:
55
+ """Engage or release the vacuum until a desired internal pressure is reached."""
56
+ ...
57
+
58
+ async def get_gage_pressure_reading_mbarg(self) -> float:
59
+ """Read each pressure sensor and return the pressure difference."""
60
+ return 0.0
61
+
62
+ # TODO: change pump power to be more specific when we find out how were gonna operate that
63
+ async def engage_vacuum(self, pump_power: Optional[float] = None) -> None:
64
+ """Engage the vacuum without regard to chamber pressure."""
65
+ ...
66
+
67
+ async def disengage_vacuum_pump(self) -> None:
68
+ """Stops the vacuum pump, doesn't vent air or disable the motor."""
69
+ ...
70
+
71
+ async def vent(self) -> None:
72
+ """Release the vacuum in the module chamber."""
73
+ ...
74
+
75
+ async def set_led(
76
+ self,
77
+ power: float,
78
+ color: Optional[LEDColor] = None,
79
+ external: Optional[bool] = None,
80
+ pattern: Optional[LEDPattern] = None,
81
+ duration: Optional[int] = None, # Default firmware duration is 500ms
82
+ reps: Optional[int] = None, # Default firmware reps is 0
83
+ ) -> None:
84
+ """Set LED Status bar color and pattern."""
85
+ ...
86
+
87
+ async def enter_programming_mode(self) -> None:
88
+ """Reboot into programming mode"""
89
+ ...
90
+
91
+ async def reset_serial_buffers(self) -> None:
92
+ """Reset the input and output serial buffers."""
93
+ ...
@@ -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(
@@ -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
 
@@ -451,6 +452,7 @@ class FlexBackend(Protocol):
451
452
  max_allowed_grip_error: float,
452
453
  hard_limit_lower: float,
453
454
  hard_limit_upper: float,
455
+ disable_geometry_grip_check: bool = False,
454
456
  ) -> None:
455
457
  ...
456
458