opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__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 (190) 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 +92 -17
  45. opentrons/hardware_control/poller.py +22 -8
  46. opentrons/hardware_control/protocols/liquid_handler.py +12 -4
  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/pipette_movement_conflict.py +4 -18
  65. opentrons/protocol_api/core/engine/protocol.py +51 -2
  66. opentrons/protocol_api/core/engine/stringify.py +2 -0
  67. opentrons/protocol_api/core/engine/tasks.py +48 -0
  68. opentrons/protocol_api/core/engine/well.py +8 -0
  69. opentrons/protocol_api/core/instrument.py +19 -2
  70. opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
  71. opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
  72. opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
  73. opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
  74. opentrons/protocol_api/core/legacy/tasks.py +19 -0
  75. opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
  76. opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
  77. opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
  78. opentrons/protocol_api/core/module.py +58 -2
  79. opentrons/protocol_api/core/protocol.py +23 -2
  80. opentrons/protocol_api/core/tasks.py +31 -0
  81. opentrons/protocol_api/core/well.py +4 -0
  82. opentrons/protocol_api/instrument_context.py +388 -2
  83. opentrons/protocol_api/labware.py +10 -2
  84. opentrons/protocol_api/module_contexts.py +170 -6
  85. opentrons/protocol_api/protocol_context.py +87 -21
  86. opentrons/protocol_api/robot_context.py +41 -25
  87. opentrons/protocol_api/tasks.py +48 -0
  88. opentrons/protocol_api/validation.py +49 -3
  89. opentrons/protocol_engine/__init__.py +4 -0
  90. opentrons/protocol_engine/actions/__init__.py +6 -2
  91. opentrons/protocol_engine/actions/actions.py +31 -9
  92. opentrons/protocol_engine/clients/sync_client.py +42 -7
  93. opentrons/protocol_engine/commands/__init__.py +56 -0
  94. opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
  95. opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
  96. opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
  97. opentrons/protocol_engine/commands/aspirate.py +1 -0
  98. opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
  99. opentrons/protocol_engine/commands/capture_image.py +302 -0
  100. opentrons/protocol_engine/commands/command.py +2 -0
  101. opentrons/protocol_engine/commands/command_unions.py +62 -0
  102. opentrons/protocol_engine/commands/create_timer.py +83 -0
  103. opentrons/protocol_engine/commands/dispense.py +1 -0
  104. opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
  105. opentrons/protocol_engine/commands/drop_tip.py +32 -8
  106. opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
  107. opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
  108. opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
  109. opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
  110. opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
  111. opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
  112. opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
  113. opentrons/protocol_engine/commands/move_labware.py +3 -4
  114. opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
  115. opentrons/protocol_engine/commands/movement_common.py +31 -2
  116. opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
  117. opentrons/protocol_engine/commands/pipetting_common.py +48 -3
  118. opentrons/protocol_engine/commands/set_tip_state.py +97 -0
  119. opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
  120. opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
  121. opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
  122. opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
  123. opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
  124. opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
  125. opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
  126. opentrons/protocol_engine/commands/touch_tip.py +1 -1
  127. opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
  128. opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
  129. opentrons/protocol_engine/create_protocol_engine.py +12 -0
  130. opentrons/protocol_engine/engine_support.py +3 -0
  131. opentrons/protocol_engine/errors/__init__.py +12 -0
  132. opentrons/protocol_engine/errors/exceptions.py +119 -0
  133. opentrons/protocol_engine/execution/__init__.py +4 -0
  134. opentrons/protocol_engine/execution/command_executor.py +62 -1
  135. opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
  136. opentrons/protocol_engine/execution/labware_movement.py +13 -15
  137. opentrons/protocol_engine/execution/movement.py +2 -0
  138. opentrons/protocol_engine/execution/pipetting.py +26 -25
  139. opentrons/protocol_engine/execution/queue_worker.py +4 -0
  140. opentrons/protocol_engine/execution/run_control.py +8 -0
  141. opentrons/protocol_engine/execution/task_handler.py +157 -0
  142. opentrons/protocol_engine/protocol_engine.py +137 -36
  143. opentrons/protocol_engine/resources/__init__.py +4 -0
  144. opentrons/protocol_engine/resources/camera_provider.py +110 -0
  145. opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
  146. opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
  147. opentrons/protocol_engine/resources/file_provider.py +133 -58
  148. opentrons/protocol_engine/resources/labware_validation.py +10 -6
  149. opentrons/protocol_engine/slot_standardization.py +2 -0
  150. opentrons/protocol_engine/state/_well_math.py +60 -18
  151. opentrons/protocol_engine/state/addressable_areas.py +2 -0
  152. opentrons/protocol_engine/state/camera.py +54 -0
  153. opentrons/protocol_engine/state/commands.py +37 -14
  154. opentrons/protocol_engine/state/geometry.py +276 -379
  155. opentrons/protocol_engine/state/labware.py +62 -108
  156. opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
  157. opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
  158. opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
  159. opentrons/protocol_engine/state/modules.py +30 -8
  160. opentrons/protocol_engine/state/motion.py +60 -18
  161. opentrons/protocol_engine/state/preconditions.py +59 -0
  162. opentrons/protocol_engine/state/state.py +44 -0
  163. opentrons/protocol_engine/state/state_summary.py +4 -0
  164. opentrons/protocol_engine/state/tasks.py +139 -0
  165. opentrons/protocol_engine/state/tips.py +177 -258
  166. opentrons/protocol_engine/state/update_types.py +26 -9
  167. opentrons/protocol_engine/types/__init__.py +23 -4
  168. opentrons/protocol_engine/types/command_preconditions.py +18 -0
  169. opentrons/protocol_engine/types/deck_configuration.py +5 -1
  170. opentrons/protocol_engine/types/instrument.py +8 -1
  171. opentrons/protocol_engine/types/labware.py +1 -13
  172. opentrons/protocol_engine/types/location.py +26 -2
  173. opentrons/protocol_engine/types/module.py +11 -1
  174. opentrons/protocol_engine/types/tasks.py +38 -0
  175. opentrons/protocol_engine/types/tip.py +9 -0
  176. opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
  177. opentrons/protocol_runner/protocol_runner.py +14 -1
  178. opentrons/protocol_runner/run_orchestrator.py +49 -2
  179. opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
  180. opentrons/protocols/api_support/definitions.py +1 -1
  181. opentrons/protocols/api_support/types.py +2 -1
  182. opentrons/simulate.py +51 -15
  183. opentrons/system/camera.py +334 -4
  184. opentrons/system/ffmpeg.py +110 -0
  185. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
  186. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
  187. opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
  188. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
  189. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
  190. {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.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.0a9'
32
- __version_tuple__ = version_tuple = (8, 7, 0, 'a9')
31
+ __version__ = version = '8.8.0a8'
32
+ __version_tuple__ = version_tuple = (8, 8, 0, 'a8')
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,
@@ -2,7 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import asyncio
4
4
  import logging
5
- from typing import Optional, List, Type
5
+ from typing import Type, Literal, AsyncIterator
6
6
 
7
7
  from opentrons.drivers.command_builder import CommandBuilder
8
8
 
@@ -25,7 +25,7 @@ class SerialConnection:
25
25
  port: str,
26
26
  baud_rate: int,
27
27
  timeout: float,
28
- loop: Optional[asyncio.AbstractEventLoop],
28
+ loop: asyncio.AbstractEventLoop | None,
29
29
  reset_buffer_before_write: bool,
30
30
  ) -> AsyncSerial:
31
31
  return await AsyncSerial.create(
@@ -43,11 +43,11 @@ class SerialConnection:
43
43
  baud_rate: int,
44
44
  timeout: float,
45
45
  ack: str,
46
- name: Optional[str] = None,
46
+ name: str | None = None,
47
47
  retry_wait_time_seconds: float = 0.1,
48
- loop: Optional[asyncio.AbstractEventLoop] = None,
49
- error_keyword: Optional[str] = None,
50
- alarm_keyword: Optional[str] = None,
48
+ loop: asyncio.AbstractEventLoop | None = None,
49
+ error_keyword: str | None = None,
50
+ alarm_keyword: str | None = None,
51
51
  reset_buffer_before_write: bool = False,
52
52
  error_codes: Type[BaseErrorCode] = DefaultErrorCodes,
53
53
  ) -> "SerialConnection":
@@ -133,7 +133,7 @@ class SerialConnection:
133
133
  self._error_codes = error_codes
134
134
 
135
135
  async def send_command(
136
- self, command: CommandBuilder, retries: int = 0, timeout: Optional[float] = None
136
+ self, command: CommandBuilder, retries: int = 0, timeout: float | None = None
137
137
  ) -> str:
138
138
  """
139
139
  Send a command and return the response.
@@ -165,7 +165,7 @@ class SerialConnection:
165
165
  await self._serial.write(data=encoded_command)
166
166
 
167
167
  async def send_data(
168
- self, data: str, retries: int = 0, timeout: Optional[float] = None
168
+ self, data: str, retries: int = 0, timeout: float | None = None
169
169
  ) -> str:
170
170
  """
171
171
  Send data and return the response.
@@ -184,7 +184,7 @@ class SerialConnection:
184
184
  ):
185
185
  return await self._send_data(data=data, retries=retries)
186
186
 
187
- async def _send_data(self, data: str, retries: int = 0) -> str:
187
+ async def _send_data(self, data: str, retries: int) -> str:
188
188
  """
189
189
  Send data and return the response.
190
190
 
@@ -351,14 +351,14 @@ class AsyncResponseSerialConnection(SerialConnection):
351
351
  baud_rate: int,
352
352
  timeout: float,
353
353
  ack: str,
354
- name: Optional[str] = None,
354
+ name: str | None = None,
355
355
  retry_wait_time_seconds: float = 0.1,
356
- loop: Optional[asyncio.AbstractEventLoop] = None,
357
- error_keyword: Optional[str] = None,
358
- alarm_keyword: Optional[str] = None,
356
+ loop: asyncio.AbstractEventLoop | None = None,
357
+ error_keyword: str | None = None,
358
+ alarm_keyword: str | None = None,
359
359
  reset_buffer_before_write: bool = False,
360
360
  error_codes: Type[BaseErrorCode] = DefaultErrorCodes,
361
- async_error_ack: Optional[str] = None,
361
+ async_error_ack: str | None = None,
362
362
  number_of_retries: int = 0,
363
363
  ) -> AsyncResponseSerialConnection:
364
364
  """
@@ -461,6 +461,27 @@ class AsyncResponseSerialConnection(SerialConnection):
461
461
  self._alarm_keyword = alarm_keyword.lower()
462
462
  self._async_error_ack = async_error_ack.lower()
463
463
 
464
+ async def send_multiack_command(
465
+ self,
466
+ command: CommandBuilder,
467
+ retries: int = 0,
468
+ timeout: float | None = None,
469
+ acks: int = 1,
470
+ ) -> list[str]:
471
+ """Send a command and return the responses.
472
+
473
+ Some commands result in multiple responses; collate them and return them all.
474
+
475
+ Args:
476
+ command: A command builder.
477
+ retries: number of times to retry in case of timeout
478
+ timeout: optional override of default timeout in seconds
479
+ acks: the number of acks to expect
480
+ """
481
+ return await self.send_data_multiack(
482
+ data=command.build(), retries=retries, timeout=timeout, acks=acks
483
+ )
484
+
464
485
  async def send_command(
465
486
  self,
466
487
  command: CommandBuilder,
@@ -485,6 +506,17 @@ class AsyncResponseSerialConnection(SerialConnection):
485
506
  timeout=timeout,
486
507
  )
487
508
 
509
+ async def send_data_multiack(
510
+ self, data: str, retries: int = 0, timeout: float | None = None, acks: int = 1
511
+ ) -> list[str]:
512
+ """Send data and return all responses."""
513
+ async with super().send_data_lock, self._serial.timeout_override(
514
+ "timeout", timeout
515
+ ):
516
+ return await self._send_data_multiack(
517
+ data=data, retries=retries or self._number_of_retries, acks=acks
518
+ )
519
+
488
520
  async def send_data(
489
521
  self, data: str, retries: int | None = None, timeout: float | None = None
490
522
  ) -> str:
@@ -508,53 +540,98 @@ class AsyncResponseSerialConnection(SerialConnection):
508
540
  retries=retries if retries is not None else self._number_of_retries,
509
541
  )
510
542
 
511
- async def _send_data(self, data: str, retries: int = 0) -> str:
543
+ async def _consume_responses(
544
+ self, acks: int
545
+ ) -> AsyncIterator[tuple[Literal["response", "error", "empty-unknown"], bytes]]:
546
+ while acks > 0:
547
+ data = await self._serial.read_until(match=self._ack)
548
+ log.debug(f"{self._name}: Read <- {data!r}")
549
+ if self._async_error_ack.encode() in data:
550
+ yield "error", data
551
+ elif self._ack in data:
552
+ yield "response", data
553
+ acks -= 1
554
+ else:
555
+ # A read timeout, end
556
+ yield "empty-unknown", data
557
+
558
+ async def _send_one_retry(self, data: str, acks: int) -> list[str]:
559
+ data_encode = data.encode("utf-8")
560
+ log.debug(f"{self._name}: Write -> {data_encode!r}")
561
+ await self._serial.write(data=data_encode)
562
+
563
+ command_acks: list[bytes] = []
564
+ async_errors: list[bytes] = []
565
+ # consume responses before raising so we don't raise and orphan
566
+ # a response in the buffer
567
+ async for response_type, response in self._consume_responses(acks):
568
+ if response_type == "error":
569
+ async_errors.append(response)
570
+ elif response_type == "response":
571
+ command_acks.append(response)
572
+ else:
573
+ break
574
+
575
+ for async_error in async_errors:
576
+ # Remove ack from response
577
+ ackless_response = async_error.replace(self._ack, b"")
578
+ str_response = self.process_raw_response(
579
+ command=data, response=ackless_response.decode()
580
+ )
581
+ self.raise_on_error(response=str_response, request=data)
582
+
583
+ ackless_responses: list[str] = []
584
+ for command_ack in command_acks:
585
+ # Remove ack from response
586
+ ackless_response = command_ack.replace(self._ack, b"")
587
+ str_response = self.process_raw_response(
588
+ command=data, response=ackless_response.decode()
589
+ )
590
+ self.raise_on_error(response=str_response, request=data)
591
+ ackless_responses.append(str_response)
592
+ return ackless_responses
593
+
594
+ async def _send_data_multiack(
595
+ self, data: str, retries: int, acks: int
596
+ ) -> list[str]:
512
597
  """
513
- Send data and return the response.
598
+ Send data and return the response(s).
514
599
 
515
600
  Args:
516
601
  data: The data to send.
517
602
  retries: number of times to retry in case of timeout
603
+ acks: The number of expected command responses
518
604
 
519
- Returns: The command response
605
+ This function retries (resends the command) up to (retries) times, and waits
606
+ for (acks) responses. It also listens for async errors. These are an older
607
+ mechanism where at the moment an error occurs, some modules will send a message
608
+ like async error ERR:202:whatever
520
609
 
521
- Raises: SerialException
522
- """
523
- data_encode = data.encode()
610
+ This function will detect async error messages if they were sent before it
611
+ sent the command or if they are sent before the final ack for the command is
612
+ sent. It will not catch async errors otherwise.
524
613
 
525
- for retry in range(retries + 1):
526
- log.debug(f"{self._name}: Write -> {data_encode!r}")
527
- await self._serial.write(data=data_encode)
614
+ This function will always try and consume all the acknowledgements specified for
615
+ its command if it sends the command, even if an async error happens in between.
528
616
 
529
- response: List[bytes] = []
530
- response.append(await self._serial.read_until(match=self._ack))
531
- log.debug(f"{self._name}: Read <- {response[-1]!r}")
532
-
533
- while self._async_error_ack.encode() in response[-1].lower():
534
- # check for multiple a priori async errors
535
- response.append(await self._serial.read_until(match=self._ack))
536
- log.debug(f"{self._name}: Read <- {response[-1]!r}")
537
-
538
- for r in response:
539
- if self._async_error_ack.encode() in r:
540
- # Remove ack from response
541
- ackless_response = r.replace(self._ack, b"")
542
- str_response = self.process_raw_response(
543
- command=data, response=ackless_response.decode()
544
- )
545
- self.raise_on_error(response=str_response, request=data)
617
+ This should all work together to make sure that there aren't any leftover acks
618
+ after the function ends, which could lead to the read/write mechanics getting out
619
+ of sync.
546
620
 
547
- if self._ack in response[-1]:
548
- # Remove ack from response
549
- ackless_response = response[-1].replace(self._ack, b"")
550
- str_response = self.process_raw_response(
551
- command=data, response=ackless_response.decode()
552
- )
553
- self.raise_on_error(response=str_response, request=data)
554
- return str_response
621
+ Returns: The command responses
555
622
 
556
- log.info(f"{self._name}: retry number {retry}/{retries}")
623
+ Raises: SerialException from an error ack to this command or an async error.
624
+ """
625
+ responses: list[str] = []
557
626
 
627
+ for retry in range(retries + 1):
628
+ responses = await self._send_one_retry(data, acks)
629
+ if responses:
630
+ return responses
631
+ log.info(f"{self._name}: retry number {retry}/{retries}")
558
632
  await self.on_retry()
559
633
 
560
634
  raise NoResponse(port=self._port, command=data)
635
+
636
+ async def _send_data(self, data: str, retries: int) -> str:
637
+ return (await self._send_data_multiack(data, retries, 1))[0]
@@ -74,3 +74,8 @@ class AbstractHeaterShakerDriver(ABC):
74
74
  async def enter_programming_mode(self) -> None:
75
75
  """Reboot into programming mode"""
76
76
  ...
77
+
78
+ @abstractmethod
79
+ async def get_error_state(self) -> None:
80
+ """Raise if the module is in an error state."""
81
+ ...
@@ -27,6 +27,7 @@ class GCODE(str, Enum):
27
27
  GET_LABWARE_LATCH_STATE = "M241"
28
28
  DEACTIVATE_HEATER = "M106"
29
29
  GET_RESET_REASON = "M114"
30
+ GET_ERROR_STATE = "M411"
30
31
 
31
32
 
32
33
  HS_BAUDRATE = 115200
@@ -202,3 +203,12 @@ class HeaterShakerDriver(AbstractHeaterShakerDriver):
202
203
  gcode=GCODE.DEACTIVATE_HEATER
203
204
  )
204
205
  await self._connection.send_command(command=c, retries=DEFAULT_COMMAND_RETRIES)
206
+
207
+ async def get_error_state(self) -> None:
208
+ """Raise if the module is in an error state."""
209
+ await self._connection.send_multiack_command(
210
+ command=CommandBuilder(terminator=HS_COMMAND_TERMINATOR).add_gcode(
211
+ gcode=GCODE.GET_ERROR_STATE
212
+ ),
213
+ acks=2,
214
+ )
@@ -92,3 +92,7 @@ class SimulatingDriver(AbstractHeaterShakerDriver):
92
92
  @ensure_yield
93
93
  async def enter_programming_mode(self) -> None:
94
94
  pass
95
+
96
+ @ensure_yield
97
+ async def get_error_state(self) -> None:
98
+ return
@@ -55,6 +55,7 @@ class AbstractThermocyclerDriver(ABC):
55
55
  temp: float,
56
56
  hold_time: Optional[float] = None,
57
57
  volume: Optional[float] = None,
58
+ ramp_rate: Optional[float] = None,
58
59
  ) -> None:
59
60
  """Send set plate temperature command"""
60
61
  ...
@@ -97,3 +98,8 @@ class AbstractThermocyclerDriver(ABC):
97
98
  async def jog_lid(self, angle: float) -> None:
98
99
  """Send the Jog Lid command."""
99
100
  ...
101
+
102
+ @abstractmethod
103
+ async def get_error_state(self) -> None:
104
+ """Raise if the thermocycler is in an error state."""
105
+ ...
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import logging
5
5
  from enum import Enum
6
- from typing import Optional, Dict, Union
6
+ from typing import Optional, Dict, Union, TypeVar, Generic
7
7
 
8
8
  from opentrons.drivers import utils
9
9
  from opentrons.drivers.command_builder import CommandBuilder
@@ -36,6 +36,7 @@ class GCODE(str, Enum):
36
36
  DEVICE_INFO = "M115"
37
37
  GET_RESET_REASON = "M114"
38
38
  ENTER_PROGRAMMING = "dfu"
39
+ GET_ERROR_STATE = "M411"
39
40
 
40
41
 
41
42
  LID_TARGET_DEFAULT = 105 # Degree celsius (floats)
@@ -73,7 +74,7 @@ class ThermocyclerDriverFactory:
73
74
  @staticmethod
74
75
  async def create(
75
76
  port: str, loop: Optional[asyncio.AbstractEventLoop]
76
- ) -> ThermocyclerDriver:
77
+ ) -> ThermocyclerDriver | ThermocyclerDriverV2:
77
78
  """
78
79
  Create a thermocycler driver.
79
80
 
@@ -148,10 +149,15 @@ class ThermocyclerDriverFactory:
148
149
  return response.startswith(GCODE.DEVICE_INFO)
149
150
 
150
151
 
151
- class ThermocyclerDriver(AbstractThermocyclerDriver):
152
+ _ConnectionKind = TypeVar(
153
+ "_ConnectionKind", SerialConnection, AsyncResponseSerialConnection
154
+ )
155
+
156
+
157
+ class _BaseThermocyclerDriver(AbstractThermocyclerDriver, Generic[_ConnectionKind]):
152
158
  def __init__(
153
159
  self,
154
- connection: SerialKind,
160
+ connection: _ConnectionKind,
155
161
  ) -> None:
156
162
  """
157
163
  Constructor
@@ -159,7 +165,7 @@ class ThermocyclerDriver(AbstractThermocyclerDriver):
159
165
  Args:
160
166
  connection: SerialConnection to the thermocycler
161
167
  """
162
- self._connection = connection
168
+ self._connection: _ConnectionKind = connection
163
169
 
164
170
  async def connect(self) -> None:
165
171
  """Connect to thermocycler"""
@@ -226,6 +232,7 @@ class ThermocyclerDriver(AbstractThermocyclerDriver):
226
232
  temp: float,
227
233
  hold_time: Optional[float] = None,
228
234
  volume: Optional[float] = None,
235
+ ramp_rate: Optional[float] = None,
229
236
  ) -> None:
230
237
  """Send set plate temperature command"""
231
238
  temp = min(BLOCK_TARGET_MAX, max(BLOCK_TARGET_MIN, temp))
@@ -328,8 +335,15 @@ class ThermocyclerDriver(AbstractThermocyclerDriver):
328
335
  "Gen1 Thermocyclers do not support the Jog Lid command."
329
336
  )
330
337
 
338
+ async def get_error_state(self) -> None:
339
+ """For the gen1, do nothing."""
340
+ pass
341
+
342
+
343
+ ThermocyclerDriver = _BaseThermocyclerDriver[SerialConnection]
331
344
 
332
- class ThermocyclerDriverV2(ThermocyclerDriver):
345
+
346
+ class ThermocyclerDriverV2(_BaseThermocyclerDriver[AsyncResponseSerialConnection]):
333
347
  """
334
348
  This driver is for Thermocycler model Gen2.
335
349
  """
@@ -343,10 +357,38 @@ class ThermocyclerDriverV2(ThermocyclerDriver):
343
357
  """
344
358
  super().__init__(connection)
345
359
 
346
- async def set_ramp_rate(self, ramp_rate: float) -> None:
347
- """Send a set ramp rate command"""
348
- # This command is fully unsupported on TC Gen2
349
- return None
360
+ async def set_plate_temperature(
361
+ self,
362
+ temp: float,
363
+ hold_time: Optional[float] = None,
364
+ volume: Optional[float] = None,
365
+ ramp_rate: Optional[float] = None,
366
+ ) -> None:
367
+ """Send set plate temperature command"""
368
+ temp = min(BLOCK_TARGET_MAX, max(BLOCK_TARGET_MIN, temp))
369
+
370
+ c = (
371
+ CommandBuilder(terminator=TC_COMMAND_TERMINATOR)
372
+ .add_gcode(gcode=GCODE.SET_PLATE_TEMP)
373
+ .add_float(
374
+ prefix="S", value=temp, precision=utils.TC_GCODE_ROUNDING_PRECISION
375
+ )
376
+ )
377
+ if hold_time is not None:
378
+ c = c.add_float(
379
+ prefix="H", value=hold_time, precision=utils.TC_GCODE_ROUNDING_PRECISION
380
+ )
381
+ if volume is not None:
382
+ c = c.add_float(
383
+ prefix="V", value=volume, precision=utils.TC_GCODE_ROUNDING_PRECISION
384
+ )
385
+
386
+ if ramp_rate is not None:
387
+ c = c.add_float(
388
+ prefix="R", value=ramp_rate, precision=utils.TC_GCODE_ROUNDING_PRECISION
389
+ )
390
+
391
+ await self._connection.send_command(command=c, retries=DEFAULT_COMMAND_RETRIES)
350
392
 
351
393
  async def get_device_info(self) -> Dict[str, str]:
352
394
  """Send get device info command"""
@@ -393,3 +435,12 @@ class ThermocyclerDriverV2(ThermocyclerDriver):
393
435
  .add_element("O")
394
436
  )
395
437
  await self._connection.send_command(command=c, retries=1)
438
+
439
+ async def get_error_state(self) -> None:
440
+ """Raise an error if the thermocycler is stuck in an error state."""
441
+ await self._connection.send_multiack_command(
442
+ command=CommandBuilder(terminator=TC_COMMAND_TERMINATOR).add_gcode(
443
+ gcode=GCODE.GET_ERROR_STATE
444
+ ),
445
+ acks=2,
446
+ )
@@ -67,10 +67,12 @@ class SimulatingDriver(AbstractThermocyclerDriver):
67
67
  temp: float,
68
68
  hold_time: Optional[float] = None,
69
69
  volume: Optional[float] = None,
70
+ ramp_rate: Optional[float] = None,
70
71
  ) -> None:
71
72
  self._plate_temperature.target = temp
72
73
  self._plate_temperature.current = temp
73
74
  self._plate_temperature.hold = 0
75
+ self._ramp_rate = ramp_rate
74
76
 
75
77
  @ensure_yield
76
78
  async def get_plate_temperature(self) -> PlateTemperature:
@@ -124,3 +126,7 @@ class SimulatingDriver(AbstractThermocyclerDriver):
124
126
  if angle < 0
125
127
  else ThermocyclerLidStatus.OPEN
126
128
  )
129
+
130
+ @ensure_yield
131
+ async def get_error_state(self) -> None:
132
+ return
@@ -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
+ ...