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.

Potentially problematic release.


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

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
@@ -44,7 +44,7 @@ from .module_validation_and_errors import (
44
44
  from .labware import Labware
45
45
  from . import validation
46
46
  from . import Task
47
-
47
+ from opentrons.drivers.thermocycler.driver import BLOCK_VOL_MIN, BLOCK_VOL_MAX
48
48
 
49
49
  _MAGNETIC_MODULE_HEIGHT_PARAM_REMOVED_IN = APIVersion(2, 14)
50
50
 
@@ -667,16 +667,9 @@ class ThermocyclerContext(ModuleContext):
667
667
  hold_time_minutes: Optional[float] = None,
668
668
  ramp_rate: Optional[float] = None,
669
669
  block_max_volume: Optional[float] = None,
670
- ) -> Task:
670
+ ) -> None:
671
671
  """Set the target temperature for the well block, in °C.
672
672
 
673
- .. versionchanged::2.27
674
- Returns a task object that represents concurrent preheating.
675
- Pass the task object to :py:meth:`ProtocolContext.wait_for_tasks` to wait for
676
- the preheat to complete.
677
-
678
- On version 2.26 or below, this function returns ``None``.
679
-
680
673
  :param temperature: A value between 4 and 99, representing the target
681
674
  temperature in °C.
682
675
  :param hold_time_minutes: The number of minutes to hold, after reaching
@@ -706,28 +699,77 @@ class ThermocyclerContext(ModuleContext):
706
699
  )
707
700
  if self._api_version >= APIVersion(2, 27) and block_max_volume is None:
708
701
  block_max_volume = self._get_current_labware_max_vol()
709
- task = self._core.set_target_block_temperature(
702
+ self._core.set_target_block_temperature(
710
703
  celsius=temperature,
711
704
  hold_time_seconds=seconds,
712
705
  block_max_volume=block_max_volume,
713
706
  ramp_rate=ramp_rate,
714
707
  )
715
- if self._api_version >= APIVersion(2, 27):
716
- return Task(api_version=self._api_version, core=task)
717
- else:
718
- return cast(Task, None)
708
+ self._core.wait_for_block_temperature()
709
+
710
+ @publish(command=cmds.thermocycler_start_set_block_temp)
711
+ @requires_version(2, 27)
712
+ def start_set_block_temperature(
713
+ self,
714
+ temperature: float,
715
+ ramp_rate: Optional[float] = None,
716
+ block_max_volume: Optional[float] = None,
717
+ ) -> Task:
718
+ """Starts to set the target temperature for the well block, in °C.
719
+
720
+ Returns a task object that represents concurrent preheating.
721
+ Pass the task object to :py:meth:`ProtocolContext.wait_for_tasks` to wait for
722
+ the preheat to complete.
723
+
724
+ :param temperature: A value between 4 and 99, representing the target
725
+ temperature in °C.
726
+ :param block_max_volume: The greatest volume of liquid contained in any
727
+ individual well of the loaded labware, in µL.
728
+ If not specified, the default is 25 µL.
729
+ After API version 2.27 it will attempt to use
730
+ the liquid tracking of the labware first and
731
+ then fall back to the 25 if there is no probed
732
+ or loaded liquid.
733
+ """
734
+
735
+ if block_max_volume is None:
736
+ block_max_volume = self._get_current_labware_max_vol()
737
+ task = self._core.start_set_target_block_temperature(
738
+ celsius=temperature,
739
+ block_max_volume=block_max_volume,
740
+ ramp_rate=ramp_rate,
741
+ )
742
+ return Task(api_version=self._api_version, core=task)
719
743
 
720
744
  @publish(command=cmds.thermocycler_set_lid_temperature)
721
745
  @requires_version(2, 0)
722
- def set_lid_temperature(self, temperature: float) -> Task:
746
+ def set_lid_temperature(self, temperature: float) -> None:
723
747
  """Set the target temperature for the heated lid, in °C.
724
748
 
725
- .. versionchanged::2.27
726
- Returns a task object that represents concurrent preheating.
727
- Pass the task object to :py:meth:`ProtocolContext.wait_for_tasks` to wait for
728
- the preheat to complete.
749
+ Returns a task object that represents concurrent preheating.
750
+ Pass the task object to :py:meth:`ProtocolContext.wait_for_tasks` to wait for
751
+ the preheat to complete.
729
752
 
730
- On version 2.26 or below, this function returns ``None``.
753
+ :param temperature: A value between 37 and 110, representing the target
754
+ temperature in °C.
755
+
756
+ .. note::
757
+
758
+ The Thermocycler will proceed to the next command immediately after
759
+ ``temperature`` is reached.
760
+
761
+ """
762
+ self._core.set_target_lid_temperature(celsius=temperature)
763
+ self._core.wait_for_lid_temperature()
764
+
765
+ @publish(command=cmds.thermocycler_start_set_lid_temperature)
766
+ @requires_version(2, 27)
767
+ def start_set_lid_temperature(self, temperature: float) -> Task:
768
+ """Set the target temperature for the heated lid, in °C.
769
+
770
+ Returns a task object that represents concurrent preheating.
771
+ Pass the task object to :py:meth:`ProtocolContext.wait_for_tasks` to wait for
772
+ the preheat to complete.
731
773
 
732
774
  :param temperature: A value between 37 and 110, representing the target
733
775
  temperature in °C.
@@ -738,11 +780,8 @@ class ThermocyclerContext(ModuleContext):
738
780
  ``temperature`` is reached.
739
781
 
740
782
  """
741
- task = self._core.set_target_lid_temperature(celsius=temperature)
742
- if self._api_version >= APIVersion(2, 27):
743
- return Task(api_version=self._api_version, core=task)
744
- else:
745
- return cast(Task, None)
783
+ task = self._core.start_set_target_lid_temperature(celsius=temperature)
784
+ return Task(api_version=self._api_version, core=task)
746
785
 
747
786
  @publish(command=cmds.thermocycler_execute_profile)
748
787
  @requires_version(2, 0)
@@ -942,6 +981,10 @@ class ThermocyclerContext(ModuleContext):
942
981
  # ignore simulated probe results
943
982
  if isinstance(well_vol, float):
944
983
  max_vol = max(max_vol, well_vol)
984
+ if max_vol > BLOCK_VOL_MAX:
985
+ max_vol = BLOCK_VOL_MAX
986
+ elif max_vol < BLOCK_VOL_MIN:
987
+ max_vol = BLOCK_VOL_MIN
945
988
  return max_vol
946
989
 
947
990
 
@@ -1444,7 +1487,7 @@ class FlexStackerContext(ModuleContext):
1444
1487
  def set_stored_labware_items(
1445
1488
  self,
1446
1489
  labware: list[Labware],
1447
- stacking_offset_z: float | None,
1490
+ stacking_offset_z: float | None = None,
1448
1491
  ) -> None:
1449
1492
  """Configure the labware the Flex Stacker will store during a protocol by providing an initial list of stored labware objects. The start of the list represents the bottom of the Stacker,
1450
1493
  and the end of the list represents the top of the Stacker.
@@ -1537,9 +1580,15 @@ class FlexStackerContext(ModuleContext):
1537
1580
  :param adapter_namespace: Applies to ``adapter`` the same way that ``namespace``
1538
1581
  applies to ``load_name``.
1539
1582
 
1583
+ .. versionchanged:: 2.26
1584
+ ``adapter_namespace`` may now be specified explicitly. When you've specified ``namespace`` for ``load_name`` but not ``adapter_namespace``, ``adapter_namespace`` now independently follows the same search rules described in ``namespace``. Formerly, it took the exact ``namespace`` value.
1585
+
1540
1586
  :param adapter_version: Applies to ``adapter`` the same way that ``version``
1541
1587
  applies to ``load_name``.
1542
1588
 
1589
+ .. versionchanged:: 2.26
1590
+ ``adapter_version`` may now be specified explictly. When unspecified, improved search rules prevent selecting a version that does not exist.
1591
+
1543
1592
  :param lid: A lid to load the on top of the main labware. Accepts the same
1544
1593
  values as the ``load_name`` parameter of :py:meth:`~.ProtocolContext.load_lid_stack`. The
1545
1594
  lid will use the same namespace as the labware, and the API will
@@ -1548,9 +1597,18 @@ class FlexStackerContext(ModuleContext):
1548
1597
  :param lid_namespace: Applies to ``lid`` the same way that ``namespace``
1549
1598
  applies to ``load_name``.
1550
1599
 
1600
+ .. versionchanged:: 2.26
1601
+ ``lid_namespace`` may now be specified explicitly.
1602
+ When you've specified ``namespace`` for ``load_name`` but not ``lid_namespace``,
1603
+ ``lid_namespace`` now independently follows the same search rules
1604
+ described in ``namespace``. Formerly, it took the exact ``namespace`` value.
1605
+
1551
1606
  :param lid_version: Applies to ``lid`` the same way that ``version``
1552
1607
  applies to ``load_name``.
1553
1608
 
1609
+ .. versionchanged:: 2.26
1610
+ ``lid_version`` may now be specified explicitly. When unspecified, improved search rules prevent selecting a version that does not exist.
1611
+
1554
1612
  :param count: The number of labware that the Flex Stacker should store. If not specified, this will be the maximum amount of this kind of
1555
1613
  labware that the Flex Stacker is capable of storing.
1556
1614
 
@@ -1573,16 +1631,61 @@ class FlexStackerContext(ModuleContext):
1573
1631
  - Labware with lid and adapter: the adapter (bottom side) of the upper labware unit overlaps with the lid (top side) of the unit below.
1574
1632
  """
1575
1633
 
1634
+ if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE:
1635
+ if adapter_namespace is not None:
1636
+ raise APIVersionError(
1637
+ api_element="The `adapter_namespace` parameter",
1638
+ until_version=str(
1639
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
1640
+ ),
1641
+ current_version=str(self._api_version),
1642
+ )
1643
+ if adapter_version is not None:
1644
+ raise APIVersionError(
1645
+ api_element="The `adapter_version` parameter",
1646
+ until_version=str(
1647
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
1648
+ ),
1649
+ current_version=str(self._api_version),
1650
+ )
1651
+ if lid_namespace is not None:
1652
+ raise APIVersionError(
1653
+ api_element="The `lid_namespace` parameter",
1654
+ until_version=str(
1655
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
1656
+ ),
1657
+ current_version=str(self._api_version),
1658
+ )
1659
+ if lid_version is not None:
1660
+ raise APIVersionError(
1661
+ api_element="The `lid_version` parameter",
1662
+ until_version=str(
1663
+ validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE
1664
+ ),
1665
+ current_version=str(self._api_version),
1666
+ )
1667
+
1668
+ if self._api_version < validation.NAMESPACE_VERSION_ADAPTER_LID_VERSION_GATE:
1669
+ checked_adapter_namespace = namespace
1670
+ checked_adapter_version = version
1671
+ checked_lid_namespace = namespace
1672
+ checked_lid_version = version
1673
+ else:
1674
+ checked_adapter_namespace = adapter_namespace
1675
+ checked_adapter_version = adapter_version
1676
+ checked_lid_namespace = lid_namespace
1677
+ checked_lid_version = lid_version
1678
+
1576
1679
  self._core.set_stored_labware(
1577
1680
  main_load_name=load_name,
1578
1681
  main_namespace=namespace,
1579
1682
  main_version=version,
1580
1683
  lid_load_name=lid,
1581
- lid_namespace=lid_namespace,
1582
- lid_version=lid_version,
1684
+ lid_namespace=checked_lid_namespace,
1685
+ lid_version=checked_lid_version,
1583
1686
  adapter_load_name=adapter,
1584
- adapter_namespace=adapter_namespace,
1585
- adapter_version=adapter_version,
1687
+ adapter_namespace=checked_adapter_namespace,
1688
+ adapter_version=checked_adapter_version,
1586
1689
  count=count,
1587
1690
  stacking_offset_z=stacking_offset_z,
1588
1691
  )
@@ -11,6 +11,7 @@ from typing import (
11
11
  Union,
12
12
  Mapping,
13
13
  cast,
14
+ Tuple,
14
15
  )
15
16
 
16
17
  from opentrons_shared_data.labware.types import LabwareDefinition
@@ -468,17 +469,15 @@ class ProtocolContext(CommandPublisher):
468
469
 
469
470
  .. versionchanged:: 2.26
470
471
  ``adapter_namespace`` may now be specified explicitly.
471
- Also, when you've specified ``namespace`` but not ``adapter_namespace``,
472
- ``adapter_namespace`` will now independently follow the same search rules
473
- described in ``namespace``. Formerly, it took ``namespace``'s exact value.
472
+ When you've specified ``namespace`` for ``load_name`` but not ``adapter_namespace``,
473
+ ``adapter_namespace`` now independently follows the same search rules
474
+ described in ``namespace``. Formerly, it took the exact ``namespace`` value.
474
475
 
475
476
  :param adapter_version: The version of the adapter being loaded.
476
477
  Applies to ``adapter`` the same way that ``version`` applies to ``load_name``.
477
478
 
478
479
  .. versionchanged:: 2.26
479
- ``adapter_version`` may now be specified explicitly. Also, when it's unspecified,
480
- the algorithm to select a version automatically has improved to avoid
481
- selecting versions that do not exist.
480
+ ``adapter_version`` may now be specified explicitly. When unspecified, the API uses the newest version available for your protocol's API level.
482
481
 
483
482
  :param lid: A lid to load on the top of the main labware. Accepts the same
484
483
  values as the ``load_name`` parameter of :py:meth:`.load_lid_stack`. The
@@ -492,17 +491,15 @@ class ProtocolContext(CommandPublisher):
492
491
 
493
492
  .. versionchanged:: 2.26
494
493
  ``lid_namespace`` may now be specified explicitly.
495
- Also, when you've specified ``namespace`` but not ``lid_namespace``,
496
- ``lid_namespace`` will now independently follow the same search rules
497
- described in ``namespace``. Formerly, it took ``namespace``'s exact value.
494
+ When you've specified ``namespace`` for ``load_name`` but not ``lid_namespace``,
495
+ ``lid_namespace`` now independently follows the same search rules
496
+ described in ``namespace``. Formerly, it took the exact ``namespace`` value.
498
497
 
499
498
  :param lid_version: The version of the adapter being loaded.
500
499
  Applies to ``lid`` the same way that ``version`` applies to ``load_name``.
501
500
 
502
501
  .. versionchanged:: 2.26
503
- ``lid_version`` may now be specified explicitly. Also, when it's unspecified,
504
- the algorithm to select a version automatically has improved to avoid
505
- selecting versions that do not exist.
502
+ ``lid_version`` may now be specified explicitly. When unspecified, the API uses the newest version available for your protocol's API level.
506
503
  """
507
504
 
508
505
  if isinstance(location, OffDeckType) and self._api_version < APIVersion(2, 15):
@@ -1505,8 +1502,8 @@ class ProtocolContext(CommandPublisher):
1505
1502
  - ``"water"``: an Opentrons-verified liquid class based on deionized water.
1506
1503
  - ``"glycerol_50"``: an Opentrons-verified liquid class for viscous liquid. Based on 50% glycerol.
1507
1504
  - ``"ethanol_80"``: an Opentrons-verified liquid class for volatile liquid. Based on 80% ethanol.
1508
- :param version: The version of the liquid class to retrieve. If left unspecified, the latest definition for the
1509
- protocol's API version will be loaded.
1505
+ :param version: Version of the liquid class to retrieve. If left unspecified, defaults to the latest version for the
1506
+ protocol's API level.
1510
1507
 
1511
1508
  :raises: ``LiquidClassDefinitionDoesNotExist``: if the specified liquid class does not exist.
1512
1509
 
@@ -1623,9 +1620,9 @@ class ProtocolContext(CommandPublisher):
1623
1620
 
1624
1621
  .. versionchanged:: 2.26
1625
1622
  ``adapter_namespace`` may now be specified explicitly.
1626
- Also, when you've specified ``namespace`` but not ``adapter_namespace``,
1627
- ``adapter_namespace`` will now independently follow the same search rules
1628
- described in ``namespace``. Formerly, it took ``namespace``'s exact value.
1623
+ When you've specified ``namespace`` for ``load_name`` but not ``adapter_namespace``,
1624
+ ``adapter_namespace`` now independently follows the same search rules
1625
+ described in ``namespace``. Formerly, it took the exact ``namespace`` value.
1629
1626
 
1630
1627
  :param adapter_version: The version of the adapter being loaded.
1631
1628
  Applies to ``adapter`` the same way that ``version`` applies to ``load_name``.
@@ -1825,6 +1822,53 @@ class ProtocolContext(CommandPublisher):
1825
1822
  )
1826
1823
  return None
1827
1824
 
1825
+ @requires_version(2, 27)
1826
+ def capture_image(
1827
+ self,
1828
+ home_before: Optional[bool] = False,
1829
+ filename: Optional[str] = None,
1830
+ resolution: Optional[Tuple[int, int]] = None,
1831
+ zoom: Optional[float] = None,
1832
+ contrast: Optional[float] = None,
1833
+ brightness: Optional[float] = None,
1834
+ saturation: Optional[float] = None,
1835
+ ) -> None:
1836
+ """Capture an image using the camera. Captured images get saved as a result of the protocol run.
1837
+
1838
+ :param home_before: Boolean to home the pipette before capturing an image.
1839
+ :param filename: Filename to use when saving the captured image as a file.
1840
+ :param resolution: Width/height tuple to determine the resolution to use when capturing an image.
1841
+ :param zoom: Optional zoom level, with minimum/default of 1x zoom and maximum of 2x zoom.
1842
+ :param contrast: Contrast level to be applied to an image, range is 0% to 100%.
1843
+ :param brightness: Brightness level to be applied to an image, range is 0% to 100%.
1844
+ :param saturation: Saturation level to be applied to an image, range is 0% to 100%.
1845
+
1846
+ .. versionadded:: 2.27
1847
+
1848
+ """
1849
+ if home_before is True:
1850
+ self._core.home()
1851
+
1852
+ with publish_context(
1853
+ broker=self.broker,
1854
+ command=cmds.capture_image(
1855
+ resolution=resolution,
1856
+ zoom=zoom,
1857
+ contrast=contrast,
1858
+ brightness=brightness,
1859
+ saturation=saturation,
1860
+ ),
1861
+ ):
1862
+ self._core.capture_image(
1863
+ filename=filename,
1864
+ resolution=resolution,
1865
+ zoom=zoom,
1866
+ contrast=contrast,
1867
+ brightness=brightness,
1868
+ saturation=saturation,
1869
+ )
1870
+ return None
1871
+
1828
1872
 
1829
1873
  def _create_module_context(
1830
1874
  module_core: Union[ModuleCore, NonConnectedModuleCore],
@@ -15,7 +15,6 @@ from opentrons.legacy_commands import publisher
15
15
  from opentrons.hardware_control import SyncHardwareAPI
16
16
  from opentrons.protocols.api_support.util import requires_version
17
17
  from opentrons.protocols.api_support.types import APIVersion
18
- from opentrons_shared_data.pipette.types import PipetteNameType
19
18
 
20
19
  from . import validation
21
20
  from .core.common import ProtocolCore, RobotCore
@@ -125,7 +124,7 @@ class RobotContext(publisher.CommandPublisher):
125
124
 
126
125
  """
127
126
  instrument_on_left = self._core.get_pipette_type_from_engine(Mount.LEFT)
128
- is_96_channel = instrument_on_left == PipetteNameType.P1000_96
127
+ is_96_channel = validation.is_pipette_96_channel(instrument_on_left)
129
128
  axis_map = validation.ensure_axis_map_type(
130
129
  axis_map, self._protocol_core.robot_type, is_96_channel
131
130
  )
@@ -162,7 +161,7 @@ class RobotContext(publisher.CommandPublisher):
162
161
  :param float speed: The maximum speed with which to move all axes in mm/s.
163
162
  """
164
163
  instrument_on_left = self._core.get_pipette_type_from_engine(Mount.LEFT)
165
- is_96_channel = instrument_on_left == PipetteNameType.P1000_96
164
+ is_96_channel = validation.is_pipette_96_channel(instrument_on_left)
166
165
 
167
166
  axis_map = validation.ensure_axis_map_type(
168
167
  axis_map, self._protocol_core.robot_type, is_96_channel
@@ -313,7 +312,7 @@ class RobotContext(publisher.CommandPublisher):
313
312
 
314
313
  """
315
314
  instrument_on_left = self._core.get_pipette_type_from_engine(Mount.LEFT)
316
- is_96_channel = instrument_on_left == PipetteNameType.P1000_96
315
+ is_96_channel = validation.is_pipette_96_channel(instrument_on_left)
317
316
 
318
317
  return validation.ensure_axis_map_type(
319
318
  axis_map, self._protocol_core.robot_type, is_96_channel
@@ -99,11 +99,18 @@ class InvalidFixtureLocationError(ValueError):
99
99
  """An error raised when attempting to load a fixture in an invalid cutout."""
100
100
 
101
101
 
102
+ def is_pipette_96_channel(pipette: Optional[PipetteNameType]) -> bool:
103
+ """Return if this pipette type is a 96 channel."""
104
+ if pipette is not None:
105
+ return pipette in [PipetteNameType.P1000_96, PipetteNameType.P200_96]
106
+ return False
107
+
108
+
102
109
  def ensure_mount_for_pipette(
103
110
  mount: Union[str, Mount, None], pipette: PipetteNameType
104
111
  ) -> Mount:
105
112
  """Ensure that an input value represents a valid mount, and is valid for the given pipette."""
106
- if pipette in [PipetteNameType.P1000_96, PipetteNameType.P200_96]:
113
+ if is_pipette_96_channel(pipette):
107
114
  # Always validate the raw mount input, even if the pipette is a 96-channel and we're not going
108
115
  # to use the mount value.
109
116
  if mount is not None:
@@ -370,7 +377,7 @@ def ensure_definition_is_not_lid_after_api_version(
370
377
  and api_version >= LID_STACK_VERSION_GATE
371
378
  ):
372
379
  raise APIVersionError(
373
- f"Labware Lids cannot be loaded like standard labware in Protocols written with an API version greater than {LID_STACK_VERSION_GATE}."
380
+ f"Labware Lids cannot be loaded like standard labware in Protocols written with an API version of {LID_STACK_VERSION_GATE} or higher."
374
381
  )
375
382
 
376
383
 
@@ -570,6 +577,40 @@ class LocationTypeError(TypeError):
570
577
  ValidTarget = Union[WellTarget, PointTarget, DisposalTarget]
571
578
 
572
579
 
580
+ def validate_dynamic_locations(
581
+ location: Optional[Union[Location, Well, TrashBin, WasteChute]],
582
+ end_location: Location,
583
+ ) -> None:
584
+ """Given that we have an end_location we check that they're a vaild dynamic pair."""
585
+ if location is None:
586
+ raise ValueError("Location must be supplied if using an End Location.")
587
+ if not isinstance(location, Location):
588
+ raise ValueError(
589
+ "Location must be a point within a well when dynamic pipetting."
590
+ )
591
+ # Shouldn't be true ever if using typing but a customer protocol may not check
592
+ if not isinstance(end_location, Location):
593
+ raise ValueError(
594
+ "End location must be a point within a well when dynamic pipetting."
595
+ )
596
+ if not location.labware.is_well:
597
+ raise ValueError("Start location must be within a well when dynamic pipetting")
598
+ if not end_location.labware.is_well:
599
+ raise ValueError("End location must be within a well when dynamic pipetting")
600
+ (
601
+ _,
602
+ start_well,
603
+ ) = location.labware.get_parent_labware_and_well()
604
+ (
605
+ _,
606
+ end_well,
607
+ ) = end_location.labware.get_parent_labware_and_well()
608
+ if start_well != end_well:
609
+ raise ValueError(
610
+ "Start and end locations must be within the same well when dynamic pipetting"
611
+ )
612
+
613
+
573
614
  def validate_location(
574
615
  location: Optional[Union[Location, Well, TrashBin, WasteChute]],
575
616
  last_location: Optional[Union[Location, TrashBin, WasteChute]],
@@ -41,6 +41,8 @@ from .types import (
41
41
  DeckType,
42
42
  DeckSlotLocation,
43
43
  InStackerHopperLocation,
44
+ WASTE_CHUTE_LOCATION,
45
+ AccessibleByGripperLocation,
44
46
  ModuleLocation,
45
47
  OnLabwareLocation,
46
48
  AddressableAreaLocation,
@@ -120,6 +122,8 @@ __all__ = [
120
122
  "ModuleLocation",
121
123
  "OnLabwareLocation",
122
124
  "AddressableAreaLocation",
125
+ "WASTE_CHUTE_LOCATION",
126
+ "AccessibleByGripperLocation",
123
127
  "InStackerHopperLocation",
124
128
  "OFF_DECK_LOCATION",
125
129
  "SYSTEM_LOCATION",
@@ -21,6 +21,7 @@ from .actions import (
21
21
  AddLabwareOffsetAction,
22
22
  AddLabwareDefinitionAction,
23
23
  AddLiquidAction,
24
+ AddCameraSettingsAction,
24
25
  SetDeckConfigurationAction,
25
26
  AddAddressableAreaAction,
26
27
  AddModuleAction,
@@ -51,6 +52,7 @@ __all__ = [
51
52
  "FailCommandAction",
52
53
  "AddLabwareOffsetAction",
53
54
  "AddLabwareDefinitionAction",
55
+ "AddCameraSettingsAction",
54
56
  "AddLiquidAction",
55
57
  "SetDeckConfigurationAction",
56
58
  "AddAddressableAreaAction",
@@ -24,6 +24,7 @@ from ..error_recovery_policy import ErrorRecoveryPolicy, ErrorRecoveryType
24
24
  from ..errors import ErrorOccurrence
25
25
  from ..notes.notes import CommandNote
26
26
  from ..state.update_types import StateUpdate
27
+ from ..resources.camera_provider import CameraSettings
27
28
  from ..types import (
28
29
  LabwareOffsetCreateInternal,
29
30
  ModuleDefinition,
@@ -236,6 +237,13 @@ class AddLabwareDefinitionAction:
236
237
  definition: LabwareDefinition
237
238
 
238
239
 
240
+ @dataclasses.dataclass(frozen=True)
241
+ class AddCameraSettingsAction:
242
+ """Add Camera settings to be used in place of the Camera Provider accessible settings."""
243
+
244
+ enablement_settings: CameraSettings
245
+
246
+
239
247
  @dataclasses.dataclass(frozen=True)
240
248
  class AddLiquidAction:
241
249
  """Add a liquid, to apply to subsequent `LoadLiquid`s."""
@@ -305,6 +313,7 @@ Action = Union[
305
313
  AddLabwareOffsetAction,
306
314
  AddLabwareDefinitionAction,
307
315
  AddModuleAction,
316
+ AddCameraSettingsAction,
308
317
  SetDeckConfigurationAction,
309
318
  AddAddressableAreaAction,
310
319
  AddLiquidAction,
@@ -451,6 +451,14 @@ from .identify_module import (
451
451
  IdentifyModuleCommandType,
452
452
  )
453
453
 
454
+ from .capture_image import (
455
+ CaptureImage,
456
+ CaptureImageParams,
457
+ CaptureImageCreate,
458
+ CaptureImageResult,
459
+ CaptureImageCommandType,
460
+ )
461
+
454
462
  __all__ = [
455
463
  # command type unions
456
464
  "Command",
@@ -796,4 +804,10 @@ __all__ = [
796
804
  "WaitForTasksParams",
797
805
  "WaitForTasksResult",
798
806
  "WaitForTasksCommandType",
807
+ # capture image command bundle
808
+ "CaptureImage",
809
+ "CaptureImageCreate",
810
+ "CaptureImageParams",
811
+ "CaptureImageResult",
812
+ "CaptureImageCommandType",
799
813
  ]
@@ -7,14 +7,15 @@ from typing_extensions import Literal, Type
7
7
  from pydantic import BaseModel, Field
8
8
  from pydantic.json_schema import SkipJsonSchema
9
9
 
10
+ from opentrons_shared_data.data_files import MimeType
10
11
  from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
11
- from ...errors import CannotPerformModuleAction, StorageLimitReachedError
12
+ from ...errors import CannotPerformModuleAction
12
13
  from ...errors.error_occurrence import ErrorOccurrence
13
14
 
14
15
  from ...resources.file_provider import (
15
16
  PlateReaderData,
16
17
  ReadData,
17
- MAXIMUM_CSV_FILE_LIMIT,
18
+ ReadCmdFileNameMetadata,
18
19
  )
19
20
  from ...resources import FileProvider
20
21
  from ...state import update_types
@@ -93,21 +94,6 @@ class ReadAbsorbanceImpl(
93
94
  "Absorbance Plate Reader can't read a plate with the lid open. Call `close_lid()` first."
94
95
  )
95
96
 
96
- # TODO: we need to return a file ID and increase the file count even when a moduel is not attached
97
- if (
98
- params.fileName is not None
99
- and abs_reader_substate.configured_wavelengths is not None
100
- ):
101
- # Validate that the amount of files we are about to generate does not put us higher than the limit
102
- if (
103
- self._state_view.files.get_filecount()
104
- + len(abs_reader_substate.configured_wavelengths)
105
- > MAXIMUM_CSV_FILE_LIMIT
106
- ):
107
- raise StorageLimitReachedError(
108
- message=f"Attempt to write file {params.fileName} exceeds file creation limit of {MAXIMUM_CSV_FILE_LIMIT} files."
109
- )
110
-
111
97
  asbsorbance_result: Dict[int, Dict[str, float]] = {}
112
98
  transform_results = []
113
99
  # Handle the measurement and begin building data for return
@@ -172,15 +158,28 @@ class ReadAbsorbanceImpl(
172
158
  )
173
159
 
174
160
  if isinstance(plate_read_result, PlateReaderData):
161
+ this_cmd_id = self._state_view.commands.get_running_command_id()
162
+ prev_cmd = (
163
+ self._state_view.commands.get_most_recently_finalized_command()
164
+ )
165
+ prev_cmd_id = prev_cmd.command.id if prev_cmd is not None else None
166
+
175
167
  # Write a CSV file for each of the measurements taken
176
168
  for measurement in plate_read_result.read_results:
177
- file_id = await self._file_provider.write_csv(
178
- write_data=plate_read_result.build_generic_csv(
179
- filename=params.fileName,
180
- measurement=measurement,
181
- )
169
+ csv_bytes = plate_read_result.build_csv_bytes(
170
+ measurement=measurement,
171
+ )
172
+ file_info = await self._file_provider.write_file(
173
+ data=csv_bytes,
174
+ mime_type=MimeType.TEXT_CSV,
175
+ command_metadata=ReadCmdFileNameMetadata(
176
+ base_filename=params.fileName,
177
+ wavelength=measurement.wavelength,
178
+ command_id=this_cmd_id or "",
179
+ prev_command_id=prev_cmd_id or "",
180
+ ),
182
181
  )
183
- file_ids.append(file_id)
182
+ file_ids.append(file_info.id)
184
183
 
185
184
  state_update.files_added = update_types.FilesAddedUpdate(
186
185
  file_ids=file_ids