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.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +4 -1
- opentrons/config/__init__.py +7 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
- opentrons/drivers/heater_shaker/abstract.py +5 -0
- opentrons/drivers/heater_shaker/driver.py +10 -0
- opentrons/drivers/heater_shaker/simulator.py +4 -0
- opentrons/drivers/thermocycler/abstract.py +6 -0
- opentrons/drivers/thermocycler/driver.py +61 -10
- opentrons/drivers/thermocycler/simulator.py +6 -0
- 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/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +3 -1
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
- opentrons/hardware_control/emulation/connection_handler.py +8 -5
- opentrons/hardware_control/emulation/heater_shaker.py +12 -3
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +67 -15
- opentrons/hardware_control/module_control.py +105 -10
- opentrons/hardware_control/modules/__init__.py +3 -0
- opentrons/hardware_control/modules/absorbance_reader.py +11 -4
- opentrons/hardware_control/modules/flex_stacker.py +38 -9
- opentrons/hardware_control/modules/heater_shaker.py +42 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +14 -6
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +68 -11
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/motion_utilities.py +6 -6
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +92 -17
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/protocols/liquid_handler.py +12 -4
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +43 -2
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +52 -0
- opentrons/legacy_commands/protocol_commands.py +53 -1
- opentrons/legacy_commands/types.py +155 -1
- opentrons/motion_planning/deck_conflict.py +17 -12
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -1
- opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +8 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +95 -4
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +4 -18
- opentrons/protocol_api/core/engine/protocol.py +51 -2
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -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 +33 -2
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +58 -2
- opentrons/protocol_api/core/protocol.py +23 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +10 -2
- opentrons/protocol_api/module_contexts.py +170 -6
- opentrons/protocol_api/protocol_context.py +87 -21
- opentrons/protocol_api/robot_context.py +41 -25
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +49 -3
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +6 -2
- opentrons/protocol_engine/actions/actions.py +31 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +56 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- 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 +2 -0
- opentrons/protocol_engine/commands/command_unions.py +62 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- 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/__init__.py +14 -0
- opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
- 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 +31 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
- opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
- opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
- opentrons/protocol_engine/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +12 -0
- opentrons/protocol_engine/errors/exceptions.py +119 -0
- opentrons/protocol_engine/execution/__init__.py +4 -0
- opentrons/protocol_engine/execution/command_executor.py +62 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
- opentrons/protocol_engine/execution/labware_movement.py +13 -15
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/pipetting.py +26 -25
- opentrons/protocol_engine/execution/queue_worker.py +4 -0
- opentrons/protocol_engine/execution/run_control.py +8 -0
- opentrons/protocol_engine/execution/task_handler.py +157 -0
- opentrons/protocol_engine/protocol_engine.py +137 -36
- opentrons/protocol_engine/resources/__init__.py +4 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +37 -14
- opentrons/protocol_engine/state/geometry.py +276 -379
- opentrons/protocol_engine/state/labware.py +62 -108
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +30 -8
- opentrons/protocol_engine/state/motion.py +60 -18
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +44 -0
- opentrons/protocol_engine/state/state_summary.py +4 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +26 -9
- opentrons/protocol_engine/types/__init__.py +23 -4
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- opentrons/protocol_engine/types/deck_configuration.py +5 -1
- opentrons/protocol_engine/types/instrument.py +8 -1
- opentrons/protocol_engine/types/labware.py +1 -13
- opentrons/protocol_engine/types/location.py +26 -2
- opentrons/protocol_engine/types/module.py +11 -1
- opentrons/protocol_engine/types/tasks.py +38 -0
- opentrons/protocol_engine/types/tip.py +9 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
- opentrons/protocol_runner/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +49 -2
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +51 -15
- opentrons/system/camera.py +334 -4
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection
|
|
6
|
+
from .abstract import AbstractVacuumModuleDriver
|
|
7
|
+
from .types import LEDColor, LEDPattern, GCODE, VacuumModuleInfo, HardwareRevision
|
|
8
|
+
from .errors import VacuumModuleErrorCodes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VM_BAUDRATE = 115200
|
|
12
|
+
DEFAULT_VM_TIMEOUT = 5
|
|
13
|
+
VM_ACK = "OK\n"
|
|
14
|
+
VM_ERROR_KEYWORD = "err"
|
|
15
|
+
VM_ASYNC_ERROR_ACK = "async"
|
|
16
|
+
DEFAULT_COMMAND_RETRIES = 2
|
|
17
|
+
GCODE_ROUNDING_PRECISION = 2
|
|
18
|
+
|
|
19
|
+
# LED animation range values
|
|
20
|
+
MIN_DURATION_MS = 25 # 25ms
|
|
21
|
+
MAX_DURATION_MS = 10000 # 10s
|
|
22
|
+
MAX_REPS = 10
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VacuumModuleDriver(AbstractVacuumModuleDriver):
|
|
26
|
+
"""Driver for Opentrons Vacuum Module."""
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def parse_device_info(cls, response: str) -> VacuumModuleInfo:
|
|
30
|
+
"""Parse vacuum module info."""
|
|
31
|
+
_RE = re.compile(
|
|
32
|
+
f"^{GCODE.GET_DEVICE_INFO} FW:(?P<fw>\\S+) HW:Opentrons-vacuum-module-(?P<hw>\\S+) SerialNo:(?P<sn>\\S+)$"
|
|
33
|
+
)
|
|
34
|
+
m = _RE.match(response)
|
|
35
|
+
if not m:
|
|
36
|
+
raise ValueError(f"Incorrect Response for device info: {response}")
|
|
37
|
+
return VacuumModuleInfo(
|
|
38
|
+
m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def parse_reset_reason(cls, response: str) -> int:
|
|
43
|
+
"""Parse the reset reason"""
|
|
44
|
+
_RE = re.compile(rf"^{GCODE.GET_RESET_REASON} R:(?P<R>\d)$")
|
|
45
|
+
match = _RE.match(response)
|
|
46
|
+
if not match:
|
|
47
|
+
raise ValueError(f"Incorrect Response for reset reason: {response}")
|
|
48
|
+
return int(match.group("R"))
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
async def create(
|
|
52
|
+
cls, port: str, loop: Optional[asyncio.AbstractEventLoop]
|
|
53
|
+
) -> "VacuumModuleDriver":
|
|
54
|
+
"""Create a Vacuum Module driver."""
|
|
55
|
+
connection = await AsyncResponseSerialConnection.create(
|
|
56
|
+
port=port,
|
|
57
|
+
baud_rate=VM_BAUDRATE,
|
|
58
|
+
timeout=DEFAULT_VM_TIMEOUT,
|
|
59
|
+
number_of_retries=DEFAULT_COMMAND_RETRIES,
|
|
60
|
+
ack=VM_ACK,
|
|
61
|
+
loop=loop,
|
|
62
|
+
error_keyword=VM_ERROR_KEYWORD,
|
|
63
|
+
async_error_ack=VM_ASYNC_ERROR_ACK,
|
|
64
|
+
reset_buffer_before_write=True,
|
|
65
|
+
error_codes=VacuumModuleErrorCodes,
|
|
66
|
+
)
|
|
67
|
+
return cls(connection)
|
|
68
|
+
|
|
69
|
+
def __init__(self, connection: AsyncResponseSerialConnection) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Constructor
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
connection: connection to the vacuum module
|
|
75
|
+
"""
|
|
76
|
+
self._connection = connection
|
|
77
|
+
|
|
78
|
+
async def connect(self) -> None:
|
|
79
|
+
"""Connect to vacuum module."""
|
|
80
|
+
await self._connection.open()
|
|
81
|
+
|
|
82
|
+
async def disconnect(self) -> None:
|
|
83
|
+
"""Disconnect from vacuum module."""
|
|
84
|
+
await self._connection.close()
|
|
85
|
+
|
|
86
|
+
async def is_connected(self) -> bool:
|
|
87
|
+
"""Check connection to vacuum module."""
|
|
88
|
+
return await self._connection.is_open()
|
|
89
|
+
|
|
90
|
+
async def reset_serial_buffers(self) -> None:
|
|
91
|
+
"""Reset the input and output serial buffers."""
|
|
92
|
+
self._connection._serial.reset_input_buffer()
|
|
93
|
+
self._connection._serial.reset_output_buffer()
|
|
94
|
+
|
|
95
|
+
async def get_device_info(self) -> VacuumModuleInfo:
|
|
96
|
+
"""Get Device Info."""
|
|
97
|
+
response = await self._connection.send_command(
|
|
98
|
+
GCODE.GET_DEVICE_INFO.build_command()
|
|
99
|
+
)
|
|
100
|
+
device_info = self.parse_device_info(response)
|
|
101
|
+
reason_resp = await self._connection.send_command(
|
|
102
|
+
GCODE.GET_RESET_REASON.build_command()
|
|
103
|
+
)
|
|
104
|
+
reason = self.parse_reset_reason(reason_resp)
|
|
105
|
+
device_info.rr = reason
|
|
106
|
+
return device_info
|
|
107
|
+
|
|
108
|
+
async def enter_programming_mode(self) -> None:
|
|
109
|
+
"""Reboot into programming mode"""
|
|
110
|
+
command = GCODE.ENTER_BOOTLOADER.build_command()
|
|
111
|
+
await self._connection.send_dfu_command(command)
|
|
112
|
+
await self._connection.close()
|
|
113
|
+
|
|
114
|
+
async def set_serial_number(self, sn: str) -> None:
|
|
115
|
+
"""Set Serial Number."""
|
|
116
|
+
if not re.match(r"^VM[\w]{1}[\d]{2}[\d]{8}[\d]+$", sn):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Invalid serial number: ({sn}) expected format: VMA1020250119001"
|
|
119
|
+
)
|
|
120
|
+
resp = await self._connection.send_command(
|
|
121
|
+
GCODE.SET_SERIAL_NUMBER.build_command().add_element(sn)
|
|
122
|
+
)
|
|
123
|
+
if not re.match(rf"^{GCODE.SET_SERIAL_NUMBER}$", resp):
|
|
124
|
+
raise ValueError(f"Incorrect Response for set serial number: {resp}")
|
|
125
|
+
|
|
126
|
+
async def set_led(
|
|
127
|
+
self,
|
|
128
|
+
power: float,
|
|
129
|
+
color: Optional[LEDColor] = None,
|
|
130
|
+
external: Optional[bool] = None,
|
|
131
|
+
pattern: Optional[LEDPattern] = None,
|
|
132
|
+
duration: Optional[int] = None,
|
|
133
|
+
reps: Optional[int] = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Set LED Status bar color and pattern.
|
|
136
|
+
|
|
137
|
+
:param power: Power of the LED (0-1.0), 0 is off, 1 is full power
|
|
138
|
+
:param color: Color of the LED
|
|
139
|
+
:param external: True if external LED, False if internal LED
|
|
140
|
+
:param pattern: Animation pattern of the LED status bar
|
|
141
|
+
:param duration: Animation duration in milliseconds (25-10000), 10s max
|
|
142
|
+
:param reps: Number of times to repeat the animation (-1 - 10), -1 is forever.
|
|
143
|
+
"""
|
|
144
|
+
power = max(0, min(power, 1.0))
|
|
145
|
+
command = GCODE.SET_LED.build_command().add_float(
|
|
146
|
+
"P", power, GCODE_ROUNDING_PRECISION
|
|
147
|
+
)
|
|
148
|
+
if color is not None:
|
|
149
|
+
command.add_int("C", color.value)
|
|
150
|
+
if external is not None:
|
|
151
|
+
command.add_int("K", int(external))
|
|
152
|
+
if pattern is not None:
|
|
153
|
+
command.add_int("A", pattern.value)
|
|
154
|
+
if duration is not None:
|
|
155
|
+
duration = max(MIN_DURATION_MS, min(duration, MAX_DURATION_MS))
|
|
156
|
+
command.add_int("D", duration)
|
|
157
|
+
if reps is not None:
|
|
158
|
+
command.add_int("R", max(-1, min(reps, MAX_REPS)))
|
|
159
|
+
resp = await self._connection.send_command(command)
|
|
160
|
+
if not re.match(rf"^{GCODE.SET_LED}$", resp):
|
|
161
|
+
raise ValueError(f"Incorrect Response for set led: {resp}")
|
|
162
|
+
|
|
163
|
+
async def enable_pump(self) -> None:
|
|
164
|
+
"""Enables the vacuum pump, does not turn it on."""
|
|
165
|
+
...
|
|
166
|
+
|
|
167
|
+
async def disable_pump(self) -> None:
|
|
168
|
+
"""Disable the vacuum pump, doesn't just turn it off."""
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
async def get_pump_motor_register(self) -> None:
|
|
172
|
+
"""Get the register value of the pump motor driver."""
|
|
173
|
+
...
|
|
174
|
+
|
|
175
|
+
async def get_pressure_sensor_register(self) -> None:
|
|
176
|
+
"""Get the register value of the pressure sensor driver."""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
async def get_pressure_sensor_reading_psi(self) -> float:
|
|
180
|
+
"""Get a reading from the pressure sensor."""
|
|
181
|
+
return 0.0
|
|
182
|
+
|
|
183
|
+
async def get_gage_pressure_reading_mbarg(self) -> float:
|
|
184
|
+
"""Read each pressure sensor and return the pressure difference."""
|
|
185
|
+
return 0.0
|
|
186
|
+
|
|
187
|
+
async def set_vacuum_chamber_pressure(
|
|
188
|
+
self,
|
|
189
|
+
gage_pressure_mbarg: float,
|
|
190
|
+
duration: Optional[float],
|
|
191
|
+
rate: Optional[float],
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Engage or release the vacuum until a desired internal pressure is reached."""
|
|
194
|
+
...
|
|
195
|
+
|
|
196
|
+
# TODO: change pump power to be more specific when we find out how were gonna operate that
|
|
197
|
+
async def engage_vacuum(self, pump_power: Optional[float] = None) -> None:
|
|
198
|
+
"""Engage the vacuum without regard to chamber pressure."""
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
async def disengage_vacuum_pump(self) -> None:
|
|
202
|
+
"""Stops the vacuum pump, doesn't vent air or disable the motor."""
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
# turns off motor, then releases, takes a timeout for buffer between turn off and vent
|
|
206
|
+
async def vent(self) -> None:
|
|
207
|
+
"""Release the vacuum in the module chamber."""
|
|
208
|
+
...
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Vacuum Module-specific errors and exceptions."""
|
|
2
|
+
|
|
3
|
+
from opentrons.drivers.asyncio.communication.errors import (
|
|
4
|
+
BaseErrorCode,
|
|
5
|
+
ErrorResponse,
|
|
6
|
+
GCodeCacheFull,
|
|
7
|
+
TaskNotReady,
|
|
8
|
+
UnhandledGcode,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EStopTriggered(ErrorResponse):
|
|
13
|
+
"""Raised when the estop is triggered during a module action."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, port: str, response: str, command: str) -> None:
|
|
16
|
+
super().__init__(port, response, command)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PumpMotorError(ErrorResponse):
|
|
20
|
+
"""Raised when pump motor error is received."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, port: str, response: str, command: str) -> None:
|
|
23
|
+
super().__init__(port, response, command)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StopRequested(ErrorResponse):
|
|
27
|
+
"""Raised when a stop is requested during a module action."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, port: str, response: str, command: str) -> None:
|
|
30
|
+
super().__init__(port, response, command)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VacuumModuleErrorCodes(BaseErrorCode):
|
|
34
|
+
"""Vacuum Module Error Codes."""
|
|
35
|
+
|
|
36
|
+
UNHANDLED_GCODE = ("ERR003", UnhandledGcode)
|
|
37
|
+
GCODE_CACHE_FULL = ("ERR004", GCodeCacheFull)
|
|
38
|
+
TASK_NOT_READY = ("ERR007", TaskNotReady)
|
|
39
|
+
STOP_REQUESTED = ("ERR504", StopRequested)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from .abstract import AbstractVacuumModuleDriver
|
|
4
|
+
from .types import VacuumModuleInfo, HardwareRevision, LEDPattern, LEDColor
|
|
5
|
+
from opentrons.util.async_helpers import ensure_yield
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimulatingDriver(AbstractVacuumModuleDriver):
|
|
9
|
+
def __init__(self, serial_number: str) -> None:
|
|
10
|
+
self._serial_number = serial_number
|
|
11
|
+
self.vacuum_on = False
|
|
12
|
+
self.pump_enabled = False
|
|
13
|
+
self.pressure_sensor_enabled = False
|
|
14
|
+
|
|
15
|
+
@ensure_yield
|
|
16
|
+
async def connect(self) -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@ensure_yield
|
|
20
|
+
async def disconnect(self) -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@ensure_yield
|
|
24
|
+
async def is_connected(self) -> bool:
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
async def get_device_info(self) -> VacuumModuleInfo:
|
|
28
|
+
return VacuumModuleInfo(
|
|
29
|
+
fw="vacuum-fw", hw=HardwareRevision.NFF, sn=self._serial_number
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
async def set_serial_number(self, sn: str) -> None:
|
|
33
|
+
self._serial_number = sn
|
|
34
|
+
|
|
35
|
+
async def enable_pump(self) -> None:
|
|
36
|
+
self.pump_enabled = True
|
|
37
|
+
|
|
38
|
+
async def disable_pump(self) -> None:
|
|
39
|
+
self.pump_enabled = False
|
|
40
|
+
|
|
41
|
+
async def get_pump_motor_register(self) -> None:
|
|
42
|
+
"""Get the register value of the pump motor driver."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
async def get_pressure_sensor_register(self) -> None:
|
|
46
|
+
"""Get the register value of the pressure sensor driver."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
async def get_pressure_sensor_reading(self) -> float:
|
|
50
|
+
"""Get a reading from the pressure sensor."""
|
|
51
|
+
return 0.0
|
|
52
|
+
|
|
53
|
+
# TODO: update the pressure arg with the units when we find out which unit
|
|
54
|
+
async def set_vacuum_chamber_pressure(
|
|
55
|
+
self,
|
|
56
|
+
guage_pressure_mbar: float,
|
|
57
|
+
duration: Optional[float],
|
|
58
|
+
rate: Optional[float],
|
|
59
|
+
vent_after: bool = False,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Engage or release the vacuum until a desired internal pressure is reached."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
async def engage_vacuum(self, pump_power: Optional[float] = None) -> None:
|
|
65
|
+
self.vacuum_on = True
|
|
66
|
+
|
|
67
|
+
async def vent(self, delay_s: float = 0.0) -> None:
|
|
68
|
+
self.vacuum_on = False
|
|
69
|
+
|
|
70
|
+
async def set_led(
|
|
71
|
+
self,
|
|
72
|
+
power: float,
|
|
73
|
+
color: Optional[LEDColor] = None,
|
|
74
|
+
external: Optional[bool] = None,
|
|
75
|
+
pattern: Optional[LEDPattern] = None,
|
|
76
|
+
duration: Optional[int] = None, # Default firmware duration is 500ms
|
|
77
|
+
reps: Optional[int] = None, # Default firmware reps is 0
|
|
78
|
+
) -> None:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
async def enter_programming_mode(self) -> None:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
async def reset_serial_buffers(self) -> None:
|
|
85
|
+
pass
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from opentrons.drivers.command_builder import CommandBuilder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GCODE(str, Enum):
|
|
9
|
+
|
|
10
|
+
GET_RESET_REASON = "M114"
|
|
11
|
+
GET_DEVICE_INFO = "M115"
|
|
12
|
+
SET_SERIAL_NUMBER = "M996"
|
|
13
|
+
ENTER_BOOTLOADER = "dfu"
|
|
14
|
+
SET_LED = "M200"
|
|
15
|
+
|
|
16
|
+
def build_command(self) -> CommandBuilder:
|
|
17
|
+
"""Build command."""
|
|
18
|
+
return CommandBuilder().add_gcode(self)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HardwareRevision(Enum):
|
|
22
|
+
"""Hardware Revision."""
|
|
23
|
+
|
|
24
|
+
NFF = "nff"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class VacuumModuleInfo:
|
|
29
|
+
"""Vacuum module info."""
|
|
30
|
+
|
|
31
|
+
fw: str
|
|
32
|
+
hw: HardwareRevision
|
|
33
|
+
sn: str
|
|
34
|
+
rr: int = 0
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, str]:
|
|
37
|
+
"""Build vacuum module info."""
|
|
38
|
+
return {
|
|
39
|
+
"serial": self.sn,
|
|
40
|
+
"version": self.fw,
|
|
41
|
+
"model": self.hw.value,
|
|
42
|
+
"reset_reason": str(self.rr),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LEDColor(Enum):
|
|
47
|
+
"""Vacuum Module LED Color."""
|
|
48
|
+
|
|
49
|
+
WHITE = 0
|
|
50
|
+
RED = 1
|
|
51
|
+
GREEN = 2
|
|
52
|
+
BLUE = 3
|
|
53
|
+
YELLOW = 4
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_name(cls, name: str) -> "LEDColor":
|
|
57
|
+
match (name.lower()):
|
|
58
|
+
case "red":
|
|
59
|
+
return cls.RED
|
|
60
|
+
case "green":
|
|
61
|
+
return cls.GREEN
|
|
62
|
+
case "blue":
|
|
63
|
+
return cls.BLUE
|
|
64
|
+
case "yellow":
|
|
65
|
+
return cls.YELLOW
|
|
66
|
+
case _:
|
|
67
|
+
return cls.WHITE
|
|
68
|
+
|
|
69
|
+
def to_name(self) -> "str":
|
|
70
|
+
return self.name.lower()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LEDPattern(Enum):
|
|
74
|
+
"""Vacuum Module LED Pattern."""
|
|
75
|
+
|
|
76
|
+
STATIC = 0
|
|
77
|
+
FLASH = 1
|
|
78
|
+
PULSE = 2
|
|
79
|
+
CONFIRM = 3
|
opentrons/execute.py
CHANGED
|
@@ -555,6 +555,7 @@ def _create_live_context_pe(
|
|
|
555
555
|
load_fixed_trash=should_load_fixed_trash_labware_for_python_protocol(
|
|
556
556
|
api_version
|
|
557
557
|
),
|
|
558
|
+
camera_provider=None,
|
|
558
559
|
)
|
|
559
560
|
)
|
|
560
561
|
|
|
@@ -644,6 +645,7 @@ def _run_file_pe(
|
|
|
644
645
|
hardware_api=hardware_api_wrapped,
|
|
645
646
|
)
|
|
646
647
|
|
|
648
|
+
# todo (chb, 2025-09-30): The Camera Provider is provided in to a run by the robot server, no analog exists for execute
|
|
647
649
|
orchestrator = RunOrchestrator(
|
|
648
650
|
hardware_api=hardware_api_wrapped,
|
|
649
651
|
protocol_engine=protocol_engine,
|
|
@@ -657,6 +659,7 @@ def _run_file_pe(
|
|
|
657
659
|
protocol_live_runner=LiveRunner(
|
|
658
660
|
protocol_engine=protocol_engine, hardware_api=hardware_api_wrapped
|
|
659
661
|
),
|
|
662
|
+
camera_provider=None,
|
|
660
663
|
)
|
|
661
664
|
|
|
662
665
|
unsubscribe = protocol_runner.broker.subscribe(
|
|
@@ -44,6 +44,7 @@ from .execution_manager import ExecutionManagerProvider
|
|
|
44
44
|
from .pause_manager import PauseManager
|
|
45
45
|
from .module_control import AttachedModulesControl
|
|
46
46
|
from .types import (
|
|
47
|
+
AsynchronousModuleErrorNotification,
|
|
47
48
|
Axis,
|
|
48
49
|
CriticalPoint,
|
|
49
50
|
DoorState,
|
|
@@ -51,6 +52,7 @@ from .types import (
|
|
|
51
52
|
ErrorMessageNotification,
|
|
52
53
|
HardwareEventHandler,
|
|
53
54
|
HardwareAction,
|
|
55
|
+
HardwareEvent,
|
|
54
56
|
MotionChecks,
|
|
55
57
|
PauseType,
|
|
56
58
|
StatusBarState,
|
|
@@ -167,6 +169,18 @@ class API(
|
|
|
167
169
|
except Exception:
|
|
168
170
|
mod_log.exception("Errored during door state event callback")
|
|
169
171
|
|
|
172
|
+
def _send_module_notification(self, event: HardwareEvent) -> None:
|
|
173
|
+
if not isinstance(event, AsynchronousModuleErrorNotification):
|
|
174
|
+
return
|
|
175
|
+
mod_log.info(
|
|
176
|
+
f"Forwarding module event {event.event} for {event.module_model} {event.module_serial} at {event.port}"
|
|
177
|
+
)
|
|
178
|
+
for cb in self._callbacks:
|
|
179
|
+
try:
|
|
180
|
+
cb(event)
|
|
181
|
+
except Exception:
|
|
182
|
+
mod_log.exception("Errored during module asynchronous callback")
|
|
183
|
+
|
|
170
184
|
def _reset_last_mount(self) -> None:
|
|
171
185
|
self._last_moved_mount = None
|
|
172
186
|
|
|
@@ -247,7 +261,9 @@ class API(
|
|
|
247
261
|
)
|
|
248
262
|
await api_instance.cache_instruments()
|
|
249
263
|
module_controls = await AttachedModulesControl.build(
|
|
250
|
-
api_instance,
|
|
264
|
+
api_instance,
|
|
265
|
+
board_revision=backend.board_revision,
|
|
266
|
+
event_callback=api_instance._send_module_notification,
|
|
251
267
|
)
|
|
252
268
|
backend.module_controls = module_controls
|
|
253
269
|
checked_loop.create_task(backend.watch(loop=checked_loop))
|
|
@@ -306,7 +322,9 @@ class API(
|
|
|
306
322
|
)
|
|
307
323
|
await api_instance.cache_instruments()
|
|
308
324
|
module_controls = await AttachedModulesControl.build(
|
|
309
|
-
api_instance,
|
|
325
|
+
api_instance,
|
|
326
|
+
board_revision=backend.board_revision,
|
|
327
|
+
event_callback=api_instance._send_module_notification,
|
|
310
328
|
)
|
|
311
329
|
backend.module_controls = module_controls
|
|
312
330
|
await backend.watch()
|
|
@@ -1312,9 +1330,10 @@ class API(
|
|
|
1312
1330
|
self.is_simulator
|
|
1313
1331
|
), "Cannot build simulating module from non-simulating hardware control API"
|
|
1314
1332
|
|
|
1315
|
-
return await self._backend.module_controls.
|
|
1316
|
-
|
|
1317
|
-
|
|
1333
|
+
return await self._backend.module_controls.register_simulated_module(
|
|
1334
|
+
simulated_usb_port=USBPort(
|
|
1335
|
+
name="", port_number=1, port_group=PortGroup.MAIN
|
|
1336
|
+
),
|
|
1318
1337
|
type=modules.ModuleType.from_model(model),
|
|
1319
1338
|
sim_model=model.value,
|
|
1320
1339
|
)
|
|
@@ -360,13 +360,19 @@ class Controller:
|
|
|
360
360
|
"""Run a probe and return the new position dict"""
|
|
361
361
|
return await self._smoothie_driver.probe_axis(axis, distance)
|
|
362
362
|
|
|
363
|
-
async def clean_up(self) -> None:
|
|
363
|
+
async def clean_up(self) -> None: # noqa: C901
|
|
364
364
|
try:
|
|
365
365
|
loop = asyncio.get_event_loop()
|
|
366
366
|
except RuntimeError:
|
|
367
367
|
return
|
|
368
|
+
if hasattr(self, "_module_controls") and self._module_controls is not None:
|
|
369
|
+
await self._module_controls.clean_up()
|
|
368
370
|
if hasattr(self, "_event_watcher"):
|
|
369
|
-
if
|
|
371
|
+
if (
|
|
372
|
+
loop.is_running()
|
|
373
|
+
and self._event_watcher
|
|
374
|
+
and not self._event_watcher.closed
|
|
375
|
+
):
|
|
370
376
|
self._event_watcher.close()
|
|
371
377
|
if hasattr(self, "gpio_chardev"):
|
|
372
378
|
try:
|
|
@@ -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,
|
|
@@ -1398,6 +1428,9 @@ class OT3Controller(FlexBackend):
|
|
|
1398
1428
|
except RuntimeError:
|
|
1399
1429
|
return
|
|
1400
1430
|
|
|
1431
|
+
if hasattr(self, "_module_controls") and self._module_controls is not None:
|
|
1432
|
+
await self._module_controls.clean_up()
|
|
1433
|
+
|
|
1401
1434
|
if hasattr(self, "_event_watcher"):
|
|
1402
1435
|
if (
|
|
1403
1436
|
loop.is_running()
|
|
@@ -367,6 +367,7 @@ class OT3Simulator(FlexBackend):
|
|
|
367
367
|
speed: Optional[float] = None,
|
|
368
368
|
stop_condition: HWStopCondition = HWStopCondition.none,
|
|
369
369
|
nodes_in_moves_only: bool = True,
|
|
370
|
+
delay: Optional[Tuple[List[Axis], float]] = None,
|
|
370
371
|
) -> None:
|
|
371
372
|
"""Move to a position.
|
|
372
373
|
|
|
@@ -728,7 +729,8 @@ class OT3Simulator(FlexBackend):
|
|
|
728
729
|
@ensure_yield
|
|
729
730
|
async def clean_up(self) -> None:
|
|
730
731
|
"""Clean up."""
|
|
731
|
-
|
|
732
|
+
if hasattr(self, "_module_controls") and self._module_controls is not None:
|
|
733
|
+
await self._module_controls.clean_up()
|
|
732
734
|
|
|
733
735
|
@staticmethod
|
|
734
736
|
def _get_home_position() -> Dict[Axis, float]:
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Shared utilities for ot3 hardware control."""
|
|
2
|
+
import copy
|
|
2
3
|
from typing import Dict, Iterable, List, Set, Tuple, TypeVar, cast, Sequence, Optional
|
|
3
4
|
from typing_extensions import Literal
|
|
4
5
|
from logging import getLogger
|
|
@@ -57,6 +58,8 @@ from opentrons_hardware.hardware_control.motion import (
|
|
|
57
58
|
MoveStopCondition,
|
|
58
59
|
create_gripper_jaw_step,
|
|
59
60
|
create_tip_action_step,
|
|
61
|
+
SingleMoveStep,
|
|
62
|
+
MoveGroupSingleAxisStep,
|
|
60
63
|
)
|
|
61
64
|
from opentrons_hardware.hardware_control.constants import interrupts_per_sec
|
|
62
65
|
|
|
@@ -376,6 +379,40 @@ def motor_nodes(devices: Set[FirmwareTarget]) -> Set[NodeId]:
|
|
|
376
379
|
return {NodeId(target) for target in motor_nodes if target in NodeId}
|
|
377
380
|
|
|
378
381
|
|
|
382
|
+
def add_delay_to_move_group(
|
|
383
|
+
group: MoveGroup,
|
|
384
|
+
present_nodes: Iterable[NodeId],
|
|
385
|
+
delay: Tuple[List[NodeId], float],
|
|
386
|
+
) -> MoveGroup:
|
|
387
|
+
delay_nodes, delay_time = delay
|
|
388
|
+
if delay_time == 0.0:
|
|
389
|
+
return group
|
|
390
|
+
|
|
391
|
+
as_single_moves: Dict[NodeId, List[SingleMoveStep]] = {}
|
|
392
|
+
for node in present_nodes:
|
|
393
|
+
as_single_moves[node] = [step[node] for step in group]
|
|
394
|
+
|
|
395
|
+
delay_step = MoveGroupSingleAxisStep(
|
|
396
|
+
distance_mm=np.float64(0),
|
|
397
|
+
velocity_mm_sec=np.float64(0),
|
|
398
|
+
duration_sec=np.float64(delay_time),
|
|
399
|
+
)
|
|
400
|
+
for node in present_nodes:
|
|
401
|
+
if node in delay_nodes:
|
|
402
|
+
# Add the delay at the beginning
|
|
403
|
+
as_single_moves[node] = [copy.deepcopy(delay_step)] + as_single_moves[node]
|
|
404
|
+
else:
|
|
405
|
+
# Add the delay at the end.
|
|
406
|
+
as_single_moves[node] = as_single_moves[node] + [copy.deepcopy(delay_step)]
|
|
407
|
+
|
|
408
|
+
new_move_group: MoveGroup = []
|
|
409
|
+
for i in range(len(group) + 1):
|
|
410
|
+
new_move_group.append(
|
|
411
|
+
{node: as_single_moves[node][i] for node in present_nodes}
|
|
412
|
+
)
|
|
413
|
+
return new_move_group
|
|
414
|
+
|
|
415
|
+
|
|
379
416
|
def create_move_group(
|
|
380
417
|
origin: Coordinates[Axis, CoordinateValue],
|
|
381
418
|
moves: List[Move[Axis]],
|