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.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +4 -1
- opentrons/config/__init__.py +7 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +8 -5
- opentrons/drivers/flex_stacker/driver.py +6 -1
- opentrons/drivers/vacuum_module/__init__.py +5 -0
- opentrons/drivers/vacuum_module/abstract.py +93 -0
- opentrons/drivers/vacuum_module/driver.py +208 -0
- opentrons/drivers/vacuum_module/errors.py +39 -0
- opentrons/drivers/vacuum_module/simulator.py +85 -0
- opentrons/drivers/vacuum_module/types.py +79 -0
- opentrons/execute.py +3 -0
- opentrons/hardware_control/backends/flex_protocol.py +2 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +2 -0
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- opentrons/hardware_control/module_control.py +23 -2
- opentrons/hardware_control/modules/mod_abc.py +1 -1
- opentrons/hardware_control/modules/types.py +1 -1
- opentrons/hardware_control/motion_utilities.py +6 -6
- opentrons/hardware_control/ot3api.py +62 -13
- opentrons/hardware_control/protocols/gripper_controller.py +1 -0
- opentrons/hardware_control/protocols/liquid_handler.py +6 -2
- opentrons/hardware_control/types.py +12 -0
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +29 -0
- opentrons/legacy_commands/protocol_commands.py +33 -1
- opentrons/legacy_commands/types.py +75 -1
- opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +2 -0
- opentrons/protocol_api/core/engine/_default_labware_versions.py +1 -0
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/module_core.py +27 -3
- opentrons/protocol_api/core/engine/protocol.py +33 -1
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/instrument.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_module_core.py +15 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +12 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/module.py +25 -2
- opentrons/protocol_api/core/protocol.py +12 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +5 -2
- opentrons/protocol_api/module_contexts.py +133 -30
- opentrons/protocol_api/protocol_context.py +61 -17
- opentrons/protocol_api/robot_context.py +3 -4
- opentrons/protocol_api/validation.py +43 -2
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +2 -0
- opentrons/protocol_engine/actions/actions.py +9 -0
- opentrons/protocol_engine/commands/__init__.py +14 -0
- opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
- opentrons/protocol_engine/commands/capture_image.py +302 -0
- opentrons/protocol_engine/commands/command.py +1 -0
- opentrons/protocol_engine/commands/command_unions.py +13 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +1 -1
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +1 -1
- opentrons/protocol_engine/commands/move_labware.py +3 -4
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
- opentrons/protocol_engine/commands/movement_common.py +29 -2
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +12 -9
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +17 -12
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +1 -1
- opentrons/protocol_engine/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +8 -0
- opentrons/protocol_engine/errors/exceptions.py +64 -0
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +54 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +4 -1
- opentrons/protocol_engine/execution/labware_movement.py +13 -4
- opentrons/protocol_engine/execution/pipetting.py +19 -25
- opentrons/protocol_engine/protocol_engine.py +62 -2
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +24 -4
- opentrons/protocol_engine/state/geometry.py +68 -10
- opentrons/protocol_engine/state/labware.py +10 -6
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +6 -1
- opentrons/protocol_engine/state/modules.py +9 -0
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +30 -0
- opentrons/protocol_engine/state/state_summary.py +2 -0
- opentrons/protocol_engine/state/update_types.py +10 -0
- opentrons/protocol_engine/types/__init__.py +14 -1
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- opentrons/protocol_engine/types/location.py +26 -2
- opentrons/protocol_engine/types/module.py +1 -1
- opentrons/protocol_runner/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +31 -0
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/simulate.py +3 -0
- opentrons/system/camera.py +333 -3
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/RECORD +109 -97
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a7.dist-info → opentrons-8.8.0a7.dist-info}/licenses/LICENSE +0 -0
|
@@ -49,6 +49,7 @@ from .ot3utils import (
|
|
|
49
49
|
gripper_jaw_state_from_fw,
|
|
50
50
|
get_system_constraints,
|
|
51
51
|
get_system_constraints_for_plunger_acceleration,
|
|
52
|
+
add_delay_to_move_group,
|
|
52
53
|
)
|
|
53
54
|
from .tip_presence_manager import TipPresenceManager
|
|
54
55
|
|
|
@@ -657,11 +658,17 @@ class OT3Controller(FlexBackend):
|
|
|
657
658
|
speed: float,
|
|
658
659
|
stop_condition: HWStopCondition,
|
|
659
660
|
nodes_in_moves_only: bool,
|
|
661
|
+
delay: Optional[Tuple[List[Axis], float]] = None,
|
|
660
662
|
) -> Tuple[Optional[MoveGroupRunner], bool]:
|
|
661
663
|
if not target:
|
|
662
664
|
return None, False
|
|
663
|
-
|
|
665
|
+
# Create a target that doesn't incorporate the plunger into a joint axis with the gantry
|
|
666
|
+
plunger_axes = [Axis.P_L, Axis.P_R]
|
|
667
|
+
|
|
664
668
|
try:
|
|
669
|
+
move_target = self._move_manager.devectorize_axes(
|
|
670
|
+
origin, target, speed, plunger_axes
|
|
671
|
+
)
|
|
665
672
|
_, movelist = self._move_manager.plan_motion(
|
|
666
673
|
origin=origin, target_list=[move_target]
|
|
667
674
|
)
|
|
@@ -683,6 +690,28 @@ class OT3Controller(FlexBackend):
|
|
|
683
690
|
move_group, _ = create_move_group(
|
|
684
691
|
origin, moves, ordered_nodes, MoveStopCondition[stop_condition.name]
|
|
685
692
|
)
|
|
693
|
+
|
|
694
|
+
if delay is not None:
|
|
695
|
+
delay_axes, delay_time = delay
|
|
696
|
+
delay_nodes = [axis_to_node(ax) for ax in delay_axes]
|
|
697
|
+
move_group = add_delay_to_move_group(
|
|
698
|
+
move_group, ordered_nodes, (delay_nodes, delay_time)
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
(
|
|
702
|
+
plunger_slowed,
|
|
703
|
+
error_str,
|
|
704
|
+
) = self._move_manager.ensure_pipette_flow_rate_unchanged(
|
|
705
|
+
[node_to_axis(node) for node in ordered_nodes],
|
|
706
|
+
origin,
|
|
707
|
+
target,
|
|
708
|
+
speed,
|
|
709
|
+
move_group,
|
|
710
|
+
[(ax, axis_to_node(ax)) for ax in plunger_axes],
|
|
711
|
+
)
|
|
712
|
+
if plunger_slowed:
|
|
713
|
+
log.error(error_str)
|
|
714
|
+
|
|
686
715
|
return (
|
|
687
716
|
MoveGroupRunner(
|
|
688
717
|
move_groups=[move_group],
|
|
@@ -728,6 +757,7 @@ class OT3Controller(FlexBackend):
|
|
|
728
757
|
speed: float,
|
|
729
758
|
stop_condition: HWStopCondition = HWStopCondition.none,
|
|
730
759
|
nodes_in_moves_only: bool = True,
|
|
760
|
+
delay: Optional[Tuple[List[Axis], float]] = None,
|
|
731
761
|
) -> None:
|
|
732
762
|
"""Move to a position.
|
|
733
763
|
|
|
@@ -750,7 +780,7 @@ class OT3Controller(FlexBackend):
|
|
|
750
780
|
|
|
751
781
|
maybe_runners = (
|
|
752
782
|
self._build_move_node_axis_runner(
|
|
753
|
-
origin, target, speed, stop_condition, nodes_in_moves_only
|
|
783
|
+
origin, target, speed, stop_condition, nodes_in_moves_only, delay
|
|
754
784
|
),
|
|
755
785
|
self._build_move_gear_axis_runner(
|
|
756
786
|
possible_q_axis_origin,
|
|
@@ -1766,6 +1796,7 @@ class OT3Controller(FlexBackend):
|
|
|
1766
1796
|
max_allowed_grip_error: float,
|
|
1767
1797
|
hard_limit_lower: float,
|
|
1768
1798
|
hard_limit_upper: float,
|
|
1799
|
+
disable_geometry_grip_check: bool = False,
|
|
1769
1800
|
) -> None:
|
|
1770
1801
|
"""
|
|
1771
1802
|
Check if the gripper is at the expected location.
|
|
@@ -1808,6 +1839,7 @@ class OT3Controller(FlexBackend):
|
|
|
1808
1839
|
if (
|
|
1809
1840
|
current_gripper_position - expected_gripper_position_min
|
|
1810
1841
|
< -max_allowed_grip_error
|
|
1842
|
+
and not disable_geometry_grip_check
|
|
1811
1843
|
):
|
|
1812
1844
|
raise FailedGripperPickupError(
|
|
1813
1845
|
message="Failed to grip: jaws closed too far",
|
|
@@ -1821,6 +1853,7 @@ class OT3Controller(FlexBackend):
|
|
|
1821
1853
|
if (
|
|
1822
1854
|
current_gripper_position - expected_gripper_position_max
|
|
1823
1855
|
> max_allowed_grip_error
|
|
1856
|
+
and not disable_geometry_grip_check
|
|
1824
1857
|
):
|
|
1825
1858
|
raise FailedGripperPickupError(
|
|
1826
1859
|
message="Failed to grip: jaws could not close far enough",
|
|
@@ -367,6 +367,7 @@ class OT3Simulator(FlexBackend):
|
|
|
367
367
|
speed: Optional[float] = None,
|
|
368
368
|
stop_condition: HWStopCondition = HWStopCondition.none,
|
|
369
369
|
nodes_in_moves_only: bool = True,
|
|
370
|
+
delay: Optional[Tuple[List[Axis], float]] = None,
|
|
370
371
|
) -> None:
|
|
371
372
|
"""Move to a position.
|
|
372
373
|
|
|
@@ -849,6 +850,7 @@ class OT3Simulator(FlexBackend):
|
|
|
849
850
|
max_allowed_grip_error: float,
|
|
850
851
|
hard_limit_lower: float,
|
|
851
852
|
hard_limit_upper: float,
|
|
853
|
+
disable_geometry_grip_check: bool = False,
|
|
852
854
|
) -> None:
|
|
853
855
|
# This is a (pretty bad) simulation of the gripper actually gripping something,
|
|
854
856
|
# but it should work.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Shared utilities for ot3 hardware control."""
|
|
2
|
+
import copy
|
|
2
3
|
from typing import Dict, Iterable, List, Set, Tuple, TypeVar, cast, Sequence, Optional
|
|
3
4
|
from typing_extensions import Literal
|
|
4
5
|
from logging import getLogger
|
|
@@ -57,6 +58,8 @@ from opentrons_hardware.hardware_control.motion import (
|
|
|
57
58
|
MoveStopCondition,
|
|
58
59
|
create_gripper_jaw_step,
|
|
59
60
|
create_tip_action_step,
|
|
61
|
+
SingleMoveStep,
|
|
62
|
+
MoveGroupSingleAxisStep,
|
|
60
63
|
)
|
|
61
64
|
from opentrons_hardware.hardware_control.constants import interrupts_per_sec
|
|
62
65
|
|
|
@@ -376,6 +379,40 @@ def motor_nodes(devices: Set[FirmwareTarget]) -> Set[NodeId]:
|
|
|
376
379
|
return {NodeId(target) for target in motor_nodes if target in NodeId}
|
|
377
380
|
|
|
378
381
|
|
|
382
|
+
def add_delay_to_move_group(
|
|
383
|
+
group: MoveGroup,
|
|
384
|
+
present_nodes: Iterable[NodeId],
|
|
385
|
+
delay: Tuple[List[NodeId], float],
|
|
386
|
+
) -> MoveGroup:
|
|
387
|
+
delay_nodes, delay_time = delay
|
|
388
|
+
if delay_time == 0.0:
|
|
389
|
+
return group
|
|
390
|
+
|
|
391
|
+
as_single_moves: Dict[NodeId, List[SingleMoveStep]] = {}
|
|
392
|
+
for node in present_nodes:
|
|
393
|
+
as_single_moves[node] = [step[node] for step in group]
|
|
394
|
+
|
|
395
|
+
delay_step = MoveGroupSingleAxisStep(
|
|
396
|
+
distance_mm=np.float64(0),
|
|
397
|
+
velocity_mm_sec=np.float64(0),
|
|
398
|
+
duration_sec=np.float64(delay_time),
|
|
399
|
+
)
|
|
400
|
+
for node in present_nodes:
|
|
401
|
+
if node in delay_nodes:
|
|
402
|
+
# Add the delay at the beginning
|
|
403
|
+
as_single_moves[node] = [copy.deepcopy(delay_step)] + as_single_moves[node]
|
|
404
|
+
else:
|
|
405
|
+
# Add the delay at the end.
|
|
406
|
+
as_single_moves[node] = as_single_moves[node] + [copy.deepcopy(delay_step)]
|
|
407
|
+
|
|
408
|
+
new_move_group: MoveGroup = []
|
|
409
|
+
for i in range(len(group) + 1):
|
|
410
|
+
new_move_group.append(
|
|
411
|
+
{node: as_single_moves[node][i] for node in present_nodes}
|
|
412
|
+
)
|
|
413
|
+
return new_move_group
|
|
414
|
+
|
|
415
|
+
|
|
379
416
|
def create_move_group(
|
|
380
417
|
origin: Coordinates[Axis, CoordinateValue],
|
|
381
418
|
moves: List[Move[Axis]],
|
|
@@ -30,6 +30,7 @@ from .types import (
|
|
|
30
30
|
StatusBarUpdateEvent,
|
|
31
31
|
HardwareEvent,
|
|
32
32
|
AsynchronousModuleErrorNotification,
|
|
33
|
+
ModuleDisconnectedNotification,
|
|
33
34
|
)
|
|
34
35
|
from . import modules
|
|
35
36
|
|
|
@@ -164,13 +165,28 @@ class AttachedModulesControl:
|
|
|
164
165
|
self.subscribe_to_api_event(mod)
|
|
165
166
|
return mod
|
|
166
167
|
|
|
167
|
-
def _disconnected_callback(
|
|
168
|
+
def _disconnected_callback(
|
|
169
|
+
self, model: str, port: str, serial: Optional[str]
|
|
170
|
+
) -> None:
|
|
168
171
|
"""Used by the module to indicate that it was disconnected and should be deleted."""
|
|
169
172
|
mod = ModuleAtPort(port=port, serial=serial, name="")
|
|
170
173
|
asyncio.run_coroutine_threadsafe(
|
|
171
174
|
self.unregister_modules([mod]),
|
|
172
175
|
self._api.loop,
|
|
173
176
|
)
|
|
177
|
+
try:
|
|
178
|
+
self._api.loop.call_soon(
|
|
179
|
+
self._event_callback,
|
|
180
|
+
ModuleDisconnectedNotification(
|
|
181
|
+
module_serial=serial,
|
|
182
|
+
module_model=modules.module_model_from_string(model),
|
|
183
|
+
port=port,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
except Exception:
|
|
187
|
+
log.exception(
|
|
188
|
+
f"Module disconnect callback for module {model} {serial} at {port} failed"
|
|
189
|
+
)
|
|
174
190
|
|
|
175
191
|
def _async_error_callback(
|
|
176
192
|
self,
|
|
@@ -218,10 +234,15 @@ class AttachedModulesControl:
|
|
|
218
234
|
for removed_mod in removed_modules:
|
|
219
235
|
try:
|
|
220
236
|
self._available_modules.remove(removed_mod)
|
|
237
|
+
# Important: this wants to be after the remove because this may trigger
|
|
238
|
+
# recursion back to here; we therefore want the module to already be
|
|
239
|
+
# removed so that the recursion terminates next loop
|
|
240
|
+
removed_mod.disconnected_callback()
|
|
221
241
|
except ValueError:
|
|
222
|
-
log.
|
|
242
|
+
log.warning(
|
|
223
243
|
f"Removed Module {removed_mod} not found in attached modules"
|
|
224
244
|
)
|
|
245
|
+
|
|
225
246
|
for removed_mod in removed_modules:
|
|
226
247
|
log.info(
|
|
227
248
|
f"Module {removed_mod.name()} detached from port {removed_mod.port}"
|
|
@@ -105,7 +105,7 @@ class AbstractModule(abc.ABC):
|
|
|
105
105
|
def disconnected_callback(self) -> None:
|
|
106
106
|
"""Called from within the module object to signify the object is no longer connected"""
|
|
107
107
|
if self._disconnected_callback is not None:
|
|
108
|
-
self._disconnected_callback(self.port, self.serial_number)
|
|
108
|
+
self._disconnected_callback(self.model(), self.port, self.serial_number)
|
|
109
109
|
|
|
110
110
|
def error_callback(self, exc: Exception) -> None:
|
|
111
111
|
"""Called from within the module object when an asynchronous hardware error occurrs."""
|
|
@@ -59,7 +59,7 @@ UploadFunction = Callable[[str, str, Dict[str, Any]], Awaitable[Tuple[bool, str]
|
|
|
59
59
|
class ModuleDisconnectedCallback(Protocol):
|
|
60
60
|
"""Protocol for the callback when the module should be disconnected."""
|
|
61
61
|
|
|
62
|
-
def __call__(self, port: str, serial: str | None) -> None:
|
|
62
|
+
def __call__(self, model: str, port: str, serial: str | None) -> None:
|
|
63
63
|
...
|
|
64
64
|
|
|
65
65
|
|
|
@@ -195,8 +195,7 @@ def target_position_from_plunger(
|
|
|
195
195
|
def target_positions_from_plunger_tracking(
|
|
196
196
|
mount: Union[Mount, OT3Mount],
|
|
197
197
|
plunger_delta: float,
|
|
198
|
-
|
|
199
|
-
current_position: Dict[Axis, float],
|
|
198
|
+
end_position: OrderedDict[Axis, float],
|
|
200
199
|
) -> "OrderedDict[Axis, float]":
|
|
201
200
|
"""Create a target position for machine axes including plungers for dynamic liquid tracking.
|
|
202
201
|
|
|
@@ -206,10 +205,11 @@ def target_positions_from_plunger_tracking(
|
|
|
206
205
|
volume to aspirate/dispense.
|
|
207
206
|
z_delta: the distance to move the z axis- should be determined based on volume and well geometry.
|
|
208
207
|
"""
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
208
|
+
plunger_pos = OrderedDict()
|
|
209
|
+
plunger = Axis.of_main_tool_actuator(mount)
|
|
210
|
+
plunger_pos[plunger] = plunger_delta
|
|
211
|
+
end_position.update(plunger_pos)
|
|
212
|
+
return end_position
|
|
213
213
|
|
|
214
214
|
|
|
215
215
|
def deck_point_from_machine_point(
|
|
@@ -52,6 +52,7 @@ from opentrons_shared_data.errors.exceptions import (
|
|
|
52
52
|
InvalidActuator,
|
|
53
53
|
FirmwareUpdateFailedError,
|
|
54
54
|
PipetteLiquidNotFoundError,
|
|
55
|
+
PipetteOverpressureError,
|
|
55
56
|
)
|
|
56
57
|
|
|
57
58
|
from .util import use_or_initialize_loop, check_motion_bounds
|
|
@@ -75,6 +76,7 @@ from .types import (
|
|
|
75
76
|
ErrorMessageNotification,
|
|
76
77
|
HardwareEvent,
|
|
77
78
|
AsynchronousModuleErrorNotification,
|
|
79
|
+
ModuleDisconnectedNotification,
|
|
78
80
|
HardwareEventHandler,
|
|
79
81
|
HardwareAction,
|
|
80
82
|
HepaFanState,
|
|
@@ -371,7 +373,10 @@ class OT3API(
|
|
|
371
373
|
def _send_module_notification(self, event: HardwareEvent) -> None:
|
|
372
374
|
if not isinstance(
|
|
373
375
|
event,
|
|
374
|
-
|
|
376
|
+
(
|
|
377
|
+
AsynchronousModuleErrorNotification,
|
|
378
|
+
ModuleDisconnectedNotification,
|
|
379
|
+
),
|
|
375
380
|
):
|
|
376
381
|
return
|
|
377
382
|
mod_log.info(
|
|
@@ -1483,6 +1488,7 @@ class OT3API(
|
|
|
1483
1488
|
expected_grip_width: float,
|
|
1484
1489
|
grip_width_uncertainty_wider: float,
|
|
1485
1490
|
grip_width_uncertainty_narrower: float,
|
|
1491
|
+
disable_geometry_grip_check: bool = False,
|
|
1486
1492
|
) -> None:
|
|
1487
1493
|
"""Ensure that a gripper pickup succeeded.
|
|
1488
1494
|
|
|
@@ -1503,6 +1509,7 @@ class OT3API(
|
|
|
1503
1509
|
gripper.max_allowed_grip_error,
|
|
1504
1510
|
gripper.min_jaw_width,
|
|
1505
1511
|
gripper.max_jaw_width,
|
|
1512
|
+
disable_geometry_grip_check,
|
|
1506
1513
|
)
|
|
1507
1514
|
|
|
1508
1515
|
def gripper_jaw_can_home(self) -> bool:
|
|
@@ -1518,6 +1525,7 @@ class OT3API(
|
|
|
1518
1525
|
acquire_lock: bool = True,
|
|
1519
1526
|
check_bounds: MotionChecks = MotionChecks.NONE,
|
|
1520
1527
|
expect_stalls: bool = False,
|
|
1528
|
+
delay: Optional[Tuple[List[Axis], float]] = None,
|
|
1521
1529
|
) -> None:
|
|
1522
1530
|
"""Worker function to apply robot motion."""
|
|
1523
1531
|
machine_pos = machine_from_deck(
|
|
@@ -1551,6 +1559,7 @@ class OT3API(
|
|
|
1551
1559
|
machine_pos,
|
|
1552
1560
|
speed or 400.0,
|
|
1553
1561
|
HWStopCondition.stall if expect_stalls else HWStopCondition.none,
|
|
1562
|
+
delay=delay,
|
|
1554
1563
|
)
|
|
1555
1564
|
except Exception:
|
|
1556
1565
|
self._log.exception("Move failed")
|
|
@@ -3076,9 +3085,10 @@ class OT3API(
|
|
|
3076
3085
|
async def aspirate_while_tracking(
|
|
3077
3086
|
self,
|
|
3078
3087
|
mount: Union[top_types.Mount, OT3Mount],
|
|
3079
|
-
|
|
3088
|
+
end_point: top_types.Point,
|
|
3080
3089
|
volume: float,
|
|
3081
|
-
|
|
3090
|
+
rate: float = 1.0,
|
|
3091
|
+
movement_delay: Optional[float] = None,
|
|
3082
3092
|
) -> None:
|
|
3083
3093
|
"""
|
|
3084
3094
|
Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
|
|
@@ -3086,20 +3096,33 @@ class OT3API(
|
|
|
3086
3096
|
:param mount: A robot mount that the instrument is on.
|
|
3087
3097
|
:param z_distance: The distance the z axis will move during apsiration.
|
|
3088
3098
|
:param volume: The volume of liquid to be aspirated.
|
|
3089
|
-
:param
|
|
3099
|
+
:param rate: The rate multiplier to aspirate with.
|
|
3100
|
+
:param movement_delay: Time to wait after the pipette starts aspirating before x/y/z movement.
|
|
3090
3101
|
"""
|
|
3091
3102
|
realmount = OT3Mount.from_mount(mount)
|
|
3092
3103
|
aspirate_spec = self._pipette_handler.plan_check_aspirate(
|
|
3093
|
-
realmount, volume,
|
|
3104
|
+
realmount, volume, rate
|
|
3094
3105
|
)
|
|
3095
3106
|
if not aspirate_spec:
|
|
3096
3107
|
return
|
|
3108
|
+
end_position = target_position_from_absolute(
|
|
3109
|
+
realmount,
|
|
3110
|
+
end_point,
|
|
3111
|
+
self.critical_point_for,
|
|
3112
|
+
top_types.Point(*self._config.left_mount_offset),
|
|
3113
|
+
top_types.Point(*self._config.right_mount_offset),
|
|
3114
|
+
top_types.Point(*self._config.gripper_mount_offset),
|
|
3115
|
+
)
|
|
3116
|
+
|
|
3097
3117
|
target_pos = target_positions_from_plunger_tracking(
|
|
3098
3118
|
realmount,
|
|
3099
3119
|
aspirate_spec.plunger_distance,
|
|
3100
|
-
|
|
3101
|
-
self._current_position,
|
|
3120
|
+
end_position,
|
|
3102
3121
|
)
|
|
3122
|
+
|
|
3123
|
+
delay: Optional[Tuple[List[Axis], float]] = None
|
|
3124
|
+
if movement_delay is not None:
|
|
3125
|
+
delay = ([Axis.X, Axis.Y, Axis.Z_L, Axis.Z_R], movement_delay)
|
|
3103
3126
|
try:
|
|
3104
3127
|
await self._backend.set_active_current(
|
|
3105
3128
|
{aspirate_spec.axis: aspirate_spec.current}
|
|
@@ -3112,7 +3135,13 @@ class OT3API(
|
|
|
3112
3135
|
target_pos,
|
|
3113
3136
|
speed=aspirate_spec.speed,
|
|
3114
3137
|
home_flagged_axes=False,
|
|
3138
|
+
delay=delay,
|
|
3115
3139
|
)
|
|
3140
|
+
except PipetteOverpressureError:
|
|
3141
|
+
self._log.exception("Aspirate failed with overpressure")
|
|
3142
|
+
# refresh positions during an over pressure here so we know where the gantry stopped.
|
|
3143
|
+
await self.refresh_positions()
|
|
3144
|
+
raise
|
|
3116
3145
|
except Exception:
|
|
3117
3146
|
self._log.exception("Aspirate failed")
|
|
3118
3147
|
aspirate_spec.instr.set_current_volume(0)
|
|
@@ -3123,11 +3152,12 @@ class OT3API(
|
|
|
3123
3152
|
async def dispense_while_tracking(
|
|
3124
3153
|
self,
|
|
3125
3154
|
mount: Union[top_types.Mount, OT3Mount],
|
|
3126
|
-
|
|
3155
|
+
end_point: top_types.Point,
|
|
3127
3156
|
volume: float,
|
|
3128
3157
|
push_out: Optional[float],
|
|
3129
|
-
|
|
3158
|
+
rate: float = 1.0,
|
|
3130
3159
|
is_full_dispense: bool = False,
|
|
3160
|
+
movement_delay: Optional[float] = None,
|
|
3131
3161
|
) -> None:
|
|
3132
3162
|
"""
|
|
3133
3163
|
Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
|
|
@@ -3135,21 +3165,34 @@ class OT3API(
|
|
|
3135
3165
|
:param mount: A robot mount that the instrument is on.
|
|
3136
3166
|
:param z_distance: The distance the z axis will move during dispensing.
|
|
3137
3167
|
:param volume: The volume of liquid to be dispensed.
|
|
3138
|
-
:param
|
|
3168
|
+
:param rate: The rate multiplier to dispense with.
|
|
3169
|
+
:param movement_delay: Time to wait after the pipette starts dispensing before x/y/z movement.
|
|
3139
3170
|
"""
|
|
3140
3171
|
realmount = OT3Mount.from_mount(mount)
|
|
3141
3172
|
dispense_spec = self._pipette_handler.plan_check_dispense(
|
|
3142
|
-
realmount, volume,
|
|
3173
|
+
realmount, volume, rate, push_out, is_full_dispense
|
|
3143
3174
|
)
|
|
3144
3175
|
if not dispense_spec:
|
|
3145
3176
|
return
|
|
3177
|
+
end_position = target_position_from_absolute(
|
|
3178
|
+
realmount,
|
|
3179
|
+
end_point,
|
|
3180
|
+
self.critical_point_for,
|
|
3181
|
+
top_types.Point(*self._config.left_mount_offset),
|
|
3182
|
+
top_types.Point(*self._config.right_mount_offset),
|
|
3183
|
+
top_types.Point(*self._config.gripper_mount_offset),
|
|
3184
|
+
)
|
|
3185
|
+
|
|
3146
3186
|
target_pos = target_positions_from_plunger_tracking(
|
|
3147
3187
|
realmount,
|
|
3148
3188
|
dispense_spec.plunger_distance,
|
|
3149
|
-
|
|
3150
|
-
self._current_position,
|
|
3189
|
+
end_position,
|
|
3151
3190
|
)
|
|
3152
3191
|
|
|
3192
|
+
delay: Optional[Tuple[List[Axis], float]] = None
|
|
3193
|
+
if movement_delay is not None:
|
|
3194
|
+
delay = ([Axis.X, Axis.Y, Axis.Z_L, Axis.Z_R], movement_delay)
|
|
3195
|
+
|
|
3153
3196
|
try:
|
|
3154
3197
|
await self._backend.set_active_current(
|
|
3155
3198
|
{dispense_spec.axis: dispense_spec.current}
|
|
@@ -3162,7 +3205,13 @@ class OT3API(
|
|
|
3162
3205
|
target_pos,
|
|
3163
3206
|
speed=dispense_spec.speed,
|
|
3164
3207
|
home_flagged_axes=False,
|
|
3208
|
+
delay=delay,
|
|
3165
3209
|
)
|
|
3210
|
+
except PipetteOverpressureError:
|
|
3211
|
+
self._log.exception("Aspirate failed with overpressure")
|
|
3212
|
+
# refresh positions during an over pressure here so we know where the gantry stopped.
|
|
3213
|
+
await self.refresh_positions()
|
|
3214
|
+
raise
|
|
3166
3215
|
except Exception:
|
|
3167
3216
|
self._log.exception("dispense failed")
|
|
3168
3217
|
dispense_spec.instr.set_current_volume(0)
|
|
@@ -125,9 +125,10 @@ class LiquidHandler(
|
|
|
125
125
|
async def aspirate_while_tracking(
|
|
126
126
|
self,
|
|
127
127
|
mount: MountArgType,
|
|
128
|
-
|
|
128
|
+
end_point: Point,
|
|
129
129
|
volume: float,
|
|
130
130
|
flow_rate: float = 1.0,
|
|
131
|
+
movement_delay: Optional[float] = None,
|
|
131
132
|
) -> None:
|
|
132
133
|
"""
|
|
133
134
|
Aspirate a volume of liquid (in microliters/uL) while moving the z axis synchronously.
|
|
@@ -136,6 +137,7 @@ class LiquidHandler(
|
|
|
136
137
|
:param z_distance: The distance the z axis will move during apsiration.
|
|
137
138
|
:param volume: The volume of liquid to be aspirated.
|
|
138
139
|
:param flow_rate: The flow rate to aspirate with.
|
|
140
|
+
:param movement_delay: Time to wait after the pipette starts aspirating before x/y/z movement.
|
|
139
141
|
"""
|
|
140
142
|
...
|
|
141
143
|
|
|
@@ -164,11 +166,12 @@ class LiquidHandler(
|
|
|
164
166
|
async def dispense_while_tracking(
|
|
165
167
|
self,
|
|
166
168
|
mount: MountArgType,
|
|
167
|
-
|
|
169
|
+
end_point: Point,
|
|
168
170
|
volume: float,
|
|
169
171
|
push_out: Optional[float],
|
|
170
172
|
flow_rate: float = 1.0,
|
|
171
173
|
is_full_dispense: bool = False,
|
|
174
|
+
movement_delay: Optional[float] = None,
|
|
172
175
|
) -> None:
|
|
173
176
|
"""
|
|
174
177
|
Dispense a volume of liquid (in microliters/uL) while moving the z axis synchronously.
|
|
@@ -177,6 +180,7 @@ class LiquidHandler(
|
|
|
177
180
|
:param z_distance: The distance the z axis will move during dispensing.
|
|
178
181
|
:param volume: The volume of liquid to be dispensed.
|
|
179
182
|
:param flow_rate: The flow rate to dispense with.
|
|
183
|
+
:param movement_delay: Time to wait after the pipette starts dispensing before x/y/z movement.
|
|
180
184
|
"""
|
|
181
185
|
...
|
|
182
186
|
|
|
@@ -399,6 +399,7 @@ class HardwareEventType(enum.Enum):
|
|
|
399
399
|
ERROR_MESSAGE = enum.auto()
|
|
400
400
|
ESTOP_CHANGE = enum.auto()
|
|
401
401
|
ASYNCHRONOUS_MODULE_ERROR = enum.auto()
|
|
402
|
+
MODULE_DISCONNECTED = enum.auto()
|
|
402
403
|
|
|
403
404
|
|
|
404
405
|
@dataclass
|
|
@@ -454,6 +455,16 @@ class AsynchronousModuleErrorNotification:
|
|
|
454
455
|
] = HardwareEventType.ASYNCHRONOUS_MODULE_ERROR
|
|
455
456
|
|
|
456
457
|
|
|
458
|
+
@dataclass(frozen=True)
|
|
459
|
+
class ModuleDisconnectedNotification:
|
|
460
|
+
module_serial: str | None
|
|
461
|
+
module_model: "ModuleModel"
|
|
462
|
+
port: str
|
|
463
|
+
event: Literal[
|
|
464
|
+
HardwareEventType.MODULE_DISCONNECTED
|
|
465
|
+
] = HardwareEventType.MODULE_DISCONNECTED
|
|
466
|
+
|
|
467
|
+
|
|
457
468
|
# new event types get new dataclasses
|
|
458
469
|
# when we add more event types we add them here
|
|
459
470
|
HardwareEvent = Union[
|
|
@@ -461,6 +472,7 @@ HardwareEvent = Union[
|
|
|
461
472
|
ErrorMessageNotification,
|
|
462
473
|
EstopStateNotification,
|
|
463
474
|
AsynchronousModuleErrorNotification,
|
|
475
|
+
ModuleDisconnectedNotification,
|
|
464
476
|
]
|
|
465
477
|
|
|
466
478
|
HardwareEventHandler = Callable[[HardwareEvent], None]
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
|
-
from typing import TYPE_CHECKING, List, Sequence, Union, overload
|
|
2
|
+
from typing import TYPE_CHECKING, List, Sequence, Union, overload, Optional
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
from .helpers import (
|
|
@@ -31,10 +31,21 @@ def aspirate(
|
|
|
31
31
|
location: Location,
|
|
32
32
|
flow_rate: float,
|
|
33
33
|
rate: float,
|
|
34
|
+
end_location: Optional[Location],
|
|
34
35
|
) -> command_types.AspirateCommand:
|
|
35
36
|
location_text = stringify_location(location)
|
|
36
|
-
|
|
37
|
-
|
|
37
|
+
end_location_text = (
|
|
38
|
+
f" while moving to {stringify_location(end_location)}"
|
|
39
|
+
if end_location is not None
|
|
40
|
+
else ""
|
|
41
|
+
)
|
|
42
|
+
template = "Aspirating {volume} uL from {location} at {flow} uL/sec{end}"
|
|
43
|
+
text = template.format(
|
|
44
|
+
volume=float(volume),
|
|
45
|
+
location=location_text,
|
|
46
|
+
flow=flow_rate,
|
|
47
|
+
end=end_location_text,
|
|
48
|
+
)
|
|
38
49
|
|
|
39
50
|
return {
|
|
40
51
|
"name": command_types.ASPIRATE,
|
|
@@ -44,6 +55,7 @@ def aspirate(
|
|
|
44
55
|
"location": location,
|
|
45
56
|
"rate": rate,
|
|
46
57
|
"text": text,
|
|
58
|
+
"end_location": end_location,
|
|
47
59
|
},
|
|
48
60
|
}
|
|
49
61
|
|
|
@@ -54,10 +66,21 @@ def dispense(
|
|
|
54
66
|
location: Location,
|
|
55
67
|
flow_rate: float,
|
|
56
68
|
rate: float,
|
|
69
|
+
end_location: Optional[Location],
|
|
57
70
|
) -> command_types.DispenseCommand:
|
|
58
71
|
location_text = stringify_location(location)
|
|
59
|
-
|
|
60
|
-
|
|
72
|
+
end_location_text = (
|
|
73
|
+
f" while moving to {stringify_location(end_location)}"
|
|
74
|
+
if end_location is not None
|
|
75
|
+
else ""
|
|
76
|
+
)
|
|
77
|
+
template = "Dispensing {volume} uL into {location} at {flow} uL/sec{end}"
|
|
78
|
+
text = template.format(
|
|
79
|
+
volume=float(volume),
|
|
80
|
+
location=location_text,
|
|
81
|
+
flow=flow_rate,
|
|
82
|
+
end=end_location_text,
|
|
83
|
+
)
|
|
61
84
|
|
|
62
85
|
return {
|
|
63
86
|
"name": command_types.DISPENSE,
|
|
@@ -67,6 +90,7 @@ def dispense(
|
|
|
67
90
|
"location": location,
|
|
68
91
|
"rate": rate,
|
|
69
92
|
"text": text,
|
|
93
|
+
"end_location": end_location,
|
|
70
94
|
},
|
|
71
95
|
}
|
|
72
96
|
|
|
@@ -208,6 +232,35 @@ def mix(
|
|
|
208
232
|
}
|
|
209
233
|
|
|
210
234
|
|
|
235
|
+
def dynamic_mix(
|
|
236
|
+
instrument: InstrumentContext,
|
|
237
|
+
repetitions: int,
|
|
238
|
+
volume: float,
|
|
239
|
+
aspirate_start_location: Location,
|
|
240
|
+
aspirate_end_location: Union[Location, None],
|
|
241
|
+
dispense_start_location: Location,
|
|
242
|
+
dispense_end_location: Union[Location, None],
|
|
243
|
+
movement_delay: float,
|
|
244
|
+
) -> command_types.DynamicMixCommand:
|
|
245
|
+
text = "Dynamically mixing {repetitions} times with a volume of {volume} ul".format(
|
|
246
|
+
repetitions=repetitions, volume=float(volume)
|
|
247
|
+
)
|
|
248
|
+
return {
|
|
249
|
+
"name": command_types.MIX,
|
|
250
|
+
"payload": {
|
|
251
|
+
"instrument": instrument,
|
|
252
|
+
"aspirate_start_location": aspirate_start_location,
|
|
253
|
+
"aspirate_end_location": aspirate_end_location,
|
|
254
|
+
"dispense_start_location": dispense_start_location,
|
|
255
|
+
"dispense_end_location": dispense_end_location,
|
|
256
|
+
"volume": volume,
|
|
257
|
+
"repetitions": repetitions,
|
|
258
|
+
"text": text,
|
|
259
|
+
"movement_delay": movement_delay,
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
211
264
|
def blow_out(
|
|
212
265
|
instrument: InstrumentContext, location: Location
|
|
213
266
|
) -> command_types.BlowOutCommand:
|