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.
- 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
opentrons/_version.py
CHANGED
|
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
|
|
|
28
28
|
commit_id: COMMIT_ID
|
|
29
29
|
__commit_id__: COMMIT_ID
|
|
30
30
|
|
|
31
|
-
__version__ = version = '8.
|
|
32
|
-
__version_tuple__ = version_tuple = (8,
|
|
31
|
+
__version__ = version = '8.8.0a7'
|
|
32
|
+
__version_tuple__ = version_tuple = (8, 8, 0, 'a7')
|
|
33
33
|
|
|
34
34
|
__commit_id__ = commit_id = None
|
opentrons/cli/analyze.py
CHANGED
|
@@ -60,7 +60,7 @@ from opentrons.protocol_engine import (
|
|
|
60
60
|
StateSummary,
|
|
61
61
|
)
|
|
62
62
|
from opentrons.protocol_engine.protocol_engine import code_in_error_tree
|
|
63
|
-
from opentrons.protocol_engine.types import CommandAnnotation
|
|
63
|
+
from opentrons.protocol_engine.types import CommandAnnotation, CommandPreconditions
|
|
64
64
|
|
|
65
65
|
from opentrons_shared_data.robot.types import RobotType
|
|
66
66
|
|
|
@@ -367,6 +367,7 @@ async def _do_analyze(
|
|
|
367
367
|
),
|
|
368
368
|
parameters=[],
|
|
369
369
|
command_annotations=[],
|
|
370
|
+
command_preconditions=None,
|
|
370
371
|
)
|
|
371
372
|
return analysis
|
|
372
373
|
return await orchestrator.run(deck_configuration=[])
|
|
@@ -465,6 +466,7 @@ async def _analyze( # noqa: C901
|
|
|
465
466
|
liquids=analysis.state_summary.liquids,
|
|
466
467
|
commandAnnotations=analysis.command_annotations,
|
|
467
468
|
liquidClasses=analysis.state_summary.liquidClasses,
|
|
469
|
+
commandPreconditions=analysis.command_preconditions,
|
|
468
470
|
)
|
|
469
471
|
|
|
470
472
|
_call_for_output_of_kind(
|
|
@@ -555,3 +557,4 @@ class AnalyzeResults(BaseModel):
|
|
|
555
557
|
liquidClasses: List[LiquidClassRecordWithId]
|
|
556
558
|
errors: List[ErrorOccurrence]
|
|
557
559
|
commandAnnotations: List[CommandAnnotation]
|
|
560
|
+
commandPreconditions: Optional[CommandPreconditions]
|
opentrons/config/__init__.py
CHANGED
|
@@ -307,6 +307,13 @@ CONFIG_ELEMENTS = (
|
|
|
307
307
|
ConfigElementType.DIR,
|
|
308
308
|
"The dir where performance metrics are stored",
|
|
309
309
|
),
|
|
310
|
+
ConfigElement(
|
|
311
|
+
"live_stream_environment_file",
|
|
312
|
+
"Live Stream Configuration",
|
|
313
|
+
Path("opentrons-live-stream.env"),
|
|
314
|
+
ConfigElementType.FILE,
|
|
315
|
+
"The file storing the Opentrons Live Stream Configuration values.",
|
|
316
|
+
),
|
|
310
317
|
)
|
|
311
318
|
#: The available configuration file elements to modify. All of these can be
|
|
312
319
|
#: changed by editing opentrons.json, where the keys are the name elements,
|
|
@@ -483,7 +483,10 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
483
483
|
)
|
|
484
484
|
|
|
485
485
|
async def send_command(
|
|
486
|
-
self,
|
|
486
|
+
self,
|
|
487
|
+
command: CommandBuilder,
|
|
488
|
+
retries: int | None = None,
|
|
489
|
+
timeout: float | None = None,
|
|
487
490
|
) -> str:
|
|
488
491
|
"""
|
|
489
492
|
Send a command and return the response.
|
|
@@ -499,7 +502,7 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
499
502
|
"""
|
|
500
503
|
return await self.send_data(
|
|
501
504
|
data=command.build(),
|
|
502
|
-
retries=retries
|
|
505
|
+
retries=retries if retries is not None else self._number_of_retries,
|
|
503
506
|
timeout=timeout,
|
|
504
507
|
)
|
|
505
508
|
|
|
@@ -515,7 +518,7 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
515
518
|
)
|
|
516
519
|
|
|
517
520
|
async def send_data(
|
|
518
|
-
self, data: str, retries: int =
|
|
521
|
+
self, data: str, retries: int | None = None, timeout: float | None = None
|
|
519
522
|
) -> str:
|
|
520
523
|
"""
|
|
521
524
|
Send data and return the response.
|
|
@@ -533,7 +536,8 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
533
536
|
"timeout", timeout
|
|
534
537
|
):
|
|
535
538
|
return await self._send_data(
|
|
536
|
-
data=data,
|
|
539
|
+
data=data,
|
|
540
|
+
retries=retries if retries is not None else self._number_of_retries,
|
|
537
541
|
)
|
|
538
542
|
|
|
539
543
|
async def _consume_responses(
|
|
@@ -618,7 +622,6 @@ class AsyncResponseSerialConnection(SerialConnection):
|
|
|
618
622
|
|
|
619
623
|
Raises: SerialException from an error ack to this command or an async error.
|
|
620
624
|
"""
|
|
621
|
-
retries = retries or self._number_of_retries
|
|
622
625
|
responses: list[str] = []
|
|
623
626
|
|
|
624
627
|
for retry in range(retries + 1):
|
|
@@ -461,7 +461,12 @@ class FlexStackerDriver(AbstractFlexStackerDriver):
|
|
|
461
461
|
command = GCODE.GET_TOF_MEASUREMENT.build_command().add_element(sensor.name)
|
|
462
462
|
if resend:
|
|
463
463
|
command.add_element("R")
|
|
464
|
-
|
|
464
|
+
|
|
465
|
+
# Note: We DONT want to auto resend the request if it fails, because the
|
|
466
|
+
# firmware will send the next frame id instead of the current one missed.
|
|
467
|
+
# So lets set `retries=0` so we only send the frame once and we can
|
|
468
|
+
# use the retry mechanism of the `get_tof_histogram` method instead.
|
|
469
|
+
resp = await self._connection.send_command(command, retries=0)
|
|
465
470
|
return self.parse_get_tof_measurement(resp)
|
|
466
471
|
|
|
467
472
|
async def get_tof_histogram(self, sensor: TOFSensor) -> TOFMeasurementResult:
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from typing import Protocol, Optional
|
|
2
|
+
|
|
3
|
+
from .types import VacuumModuleInfo, LEDColor, LEDPattern
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AbstractVacuumModuleDriver(Protocol):
|
|
7
|
+
"""Protocol for the Vacuum Module driver."""
|
|
8
|
+
|
|
9
|
+
async def connect(self) -> None:
|
|
10
|
+
"""Connect to vacuum module."""
|
|
11
|
+
...
|
|
12
|
+
|
|
13
|
+
async def disconnect(self) -> None:
|
|
14
|
+
"""Disconnect from vacuum module."""
|
|
15
|
+
...
|
|
16
|
+
|
|
17
|
+
async def is_connected(self) -> bool:
|
|
18
|
+
"""Check connection to vacuum module."""
|
|
19
|
+
...
|
|
20
|
+
|
|
21
|
+
async def get_device_info(self) -> VacuumModuleInfo:
|
|
22
|
+
"""Get Device Info."""
|
|
23
|
+
...
|
|
24
|
+
|
|
25
|
+
async def set_serial_number(self, sn: str) -> None:
|
|
26
|
+
"""Set Serial Number."""
|
|
27
|
+
...
|
|
28
|
+
|
|
29
|
+
async def enable_pump(self) -> None:
|
|
30
|
+
"""Enable the vacuum pump."""
|
|
31
|
+
...
|
|
32
|
+
|
|
33
|
+
async def disable_pump(self) -> None:
|
|
34
|
+
"""Disable the vacuum pump."""
|
|
35
|
+
...
|
|
36
|
+
|
|
37
|
+
async def get_pump_motor_register(self) -> None:
|
|
38
|
+
"""Get the register value of the pump motor driver."""
|
|
39
|
+
...
|
|
40
|
+
|
|
41
|
+
async def get_pressure_sensor_register(self) -> None:
|
|
42
|
+
"""Get the register value of the pressure sensor driver."""
|
|
43
|
+
...
|
|
44
|
+
|
|
45
|
+
async def get_pressure_sensor_reading_psi(self) -> float:
|
|
46
|
+
"""Get a reading from the pressure sensor."""
|
|
47
|
+
...
|
|
48
|
+
|
|
49
|
+
async def set_vacuum_chamber_pressure(
|
|
50
|
+
self,
|
|
51
|
+
gage_pressure_mbarg: float,
|
|
52
|
+
duration: Optional[float],
|
|
53
|
+
rate: Optional[float],
|
|
54
|
+
) -> None:
|
|
55
|
+
"""Engage or release the vacuum until a desired internal pressure is reached."""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
async def get_gage_pressure_reading_mbarg(self) -> float:
|
|
59
|
+
"""Read each pressure sensor and return the pressure difference."""
|
|
60
|
+
return 0.0
|
|
61
|
+
|
|
62
|
+
# TODO: change pump power to be more specific when we find out how were gonna operate that
|
|
63
|
+
async def engage_vacuum(self, pump_power: Optional[float] = None) -> None:
|
|
64
|
+
"""Engage the vacuum without regard to chamber pressure."""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
async def disengage_vacuum_pump(self) -> None:
|
|
68
|
+
"""Stops the vacuum pump, doesn't vent air or disable the motor."""
|
|
69
|
+
...
|
|
70
|
+
|
|
71
|
+
async def vent(self) -> None:
|
|
72
|
+
"""Release the vacuum in the module chamber."""
|
|
73
|
+
...
|
|
74
|
+
|
|
75
|
+
async def set_led(
|
|
76
|
+
self,
|
|
77
|
+
power: float,
|
|
78
|
+
color: Optional[LEDColor] = None,
|
|
79
|
+
external: Optional[bool] = None,
|
|
80
|
+
pattern: Optional[LEDPattern] = None,
|
|
81
|
+
duration: Optional[int] = None, # Default firmware duration is 500ms
|
|
82
|
+
reps: Optional[int] = None, # Default firmware reps is 0
|
|
83
|
+
) -> None:
|
|
84
|
+
"""Set LED Status bar color and pattern."""
|
|
85
|
+
...
|
|
86
|
+
|
|
87
|
+
async def enter_programming_mode(self) -> None:
|
|
88
|
+
"""Reboot into programming mode"""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
async def reset_serial_buffers(self) -> None:
|
|
92
|
+
"""Reset the input and output serial buffers."""
|
|
93
|
+
...
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import re
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from opentrons.drivers.asyncio.communication import AsyncResponseSerialConnection
|
|
6
|
+
from .abstract import AbstractVacuumModuleDriver
|
|
7
|
+
from .types import LEDColor, LEDPattern, GCODE, VacuumModuleInfo, HardwareRevision
|
|
8
|
+
from .errors import VacuumModuleErrorCodes
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VM_BAUDRATE = 115200
|
|
12
|
+
DEFAULT_VM_TIMEOUT = 5
|
|
13
|
+
VM_ACK = "OK\n"
|
|
14
|
+
VM_ERROR_KEYWORD = "err"
|
|
15
|
+
VM_ASYNC_ERROR_ACK = "async"
|
|
16
|
+
DEFAULT_COMMAND_RETRIES = 2
|
|
17
|
+
GCODE_ROUNDING_PRECISION = 2
|
|
18
|
+
|
|
19
|
+
# LED animation range values
|
|
20
|
+
MIN_DURATION_MS = 25 # 25ms
|
|
21
|
+
MAX_DURATION_MS = 10000 # 10s
|
|
22
|
+
MAX_REPS = 10
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class VacuumModuleDriver(AbstractVacuumModuleDriver):
|
|
26
|
+
"""Driver for Opentrons Vacuum Module."""
|
|
27
|
+
|
|
28
|
+
@classmethod
|
|
29
|
+
def parse_device_info(cls, response: str) -> VacuumModuleInfo:
|
|
30
|
+
"""Parse vacuum module info."""
|
|
31
|
+
_RE = re.compile(
|
|
32
|
+
f"^{GCODE.GET_DEVICE_INFO} FW:(?P<fw>\\S+) HW:Opentrons-vacuum-module-(?P<hw>\\S+) SerialNo:(?P<sn>\\S+)$"
|
|
33
|
+
)
|
|
34
|
+
m = _RE.match(response)
|
|
35
|
+
if not m:
|
|
36
|
+
raise ValueError(f"Incorrect Response for device info: {response}")
|
|
37
|
+
return VacuumModuleInfo(
|
|
38
|
+
m.group("fw"), HardwareRevision(m.group("hw")), m.group("sn")
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@classmethod
|
|
42
|
+
def parse_reset_reason(cls, response: str) -> int:
|
|
43
|
+
"""Parse the reset reason"""
|
|
44
|
+
_RE = re.compile(rf"^{GCODE.GET_RESET_REASON} R:(?P<R>\d)$")
|
|
45
|
+
match = _RE.match(response)
|
|
46
|
+
if not match:
|
|
47
|
+
raise ValueError(f"Incorrect Response for reset reason: {response}")
|
|
48
|
+
return int(match.group("R"))
|
|
49
|
+
|
|
50
|
+
@classmethod
|
|
51
|
+
async def create(
|
|
52
|
+
cls, port: str, loop: Optional[asyncio.AbstractEventLoop]
|
|
53
|
+
) -> "VacuumModuleDriver":
|
|
54
|
+
"""Create a Vacuum Module driver."""
|
|
55
|
+
connection = await AsyncResponseSerialConnection.create(
|
|
56
|
+
port=port,
|
|
57
|
+
baud_rate=VM_BAUDRATE,
|
|
58
|
+
timeout=DEFAULT_VM_TIMEOUT,
|
|
59
|
+
number_of_retries=DEFAULT_COMMAND_RETRIES,
|
|
60
|
+
ack=VM_ACK,
|
|
61
|
+
loop=loop,
|
|
62
|
+
error_keyword=VM_ERROR_KEYWORD,
|
|
63
|
+
async_error_ack=VM_ASYNC_ERROR_ACK,
|
|
64
|
+
reset_buffer_before_write=True,
|
|
65
|
+
error_codes=VacuumModuleErrorCodes,
|
|
66
|
+
)
|
|
67
|
+
return cls(connection)
|
|
68
|
+
|
|
69
|
+
def __init__(self, connection: AsyncResponseSerialConnection) -> None:
|
|
70
|
+
"""
|
|
71
|
+
Constructor
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
connection: connection to the vacuum module
|
|
75
|
+
"""
|
|
76
|
+
self._connection = connection
|
|
77
|
+
|
|
78
|
+
async def connect(self) -> None:
|
|
79
|
+
"""Connect to vacuum module."""
|
|
80
|
+
await self._connection.open()
|
|
81
|
+
|
|
82
|
+
async def disconnect(self) -> None:
|
|
83
|
+
"""Disconnect from vacuum module."""
|
|
84
|
+
await self._connection.close()
|
|
85
|
+
|
|
86
|
+
async def is_connected(self) -> bool:
|
|
87
|
+
"""Check connection to vacuum module."""
|
|
88
|
+
return await self._connection.is_open()
|
|
89
|
+
|
|
90
|
+
async def reset_serial_buffers(self) -> None:
|
|
91
|
+
"""Reset the input and output serial buffers."""
|
|
92
|
+
self._connection._serial.reset_input_buffer()
|
|
93
|
+
self._connection._serial.reset_output_buffer()
|
|
94
|
+
|
|
95
|
+
async def get_device_info(self) -> VacuumModuleInfo:
|
|
96
|
+
"""Get Device Info."""
|
|
97
|
+
response = await self._connection.send_command(
|
|
98
|
+
GCODE.GET_DEVICE_INFO.build_command()
|
|
99
|
+
)
|
|
100
|
+
device_info = self.parse_device_info(response)
|
|
101
|
+
reason_resp = await self._connection.send_command(
|
|
102
|
+
GCODE.GET_RESET_REASON.build_command()
|
|
103
|
+
)
|
|
104
|
+
reason = self.parse_reset_reason(reason_resp)
|
|
105
|
+
device_info.rr = reason
|
|
106
|
+
return device_info
|
|
107
|
+
|
|
108
|
+
async def enter_programming_mode(self) -> None:
|
|
109
|
+
"""Reboot into programming mode"""
|
|
110
|
+
command = GCODE.ENTER_BOOTLOADER.build_command()
|
|
111
|
+
await self._connection.send_dfu_command(command)
|
|
112
|
+
await self._connection.close()
|
|
113
|
+
|
|
114
|
+
async def set_serial_number(self, sn: str) -> None:
|
|
115
|
+
"""Set Serial Number."""
|
|
116
|
+
if not re.match(r"^VM[\w]{1}[\d]{2}[\d]{8}[\d]+$", sn):
|
|
117
|
+
raise ValueError(
|
|
118
|
+
f"Invalid serial number: ({sn}) expected format: VMA1020250119001"
|
|
119
|
+
)
|
|
120
|
+
resp = await self._connection.send_command(
|
|
121
|
+
GCODE.SET_SERIAL_NUMBER.build_command().add_element(sn)
|
|
122
|
+
)
|
|
123
|
+
if not re.match(rf"^{GCODE.SET_SERIAL_NUMBER}$", resp):
|
|
124
|
+
raise ValueError(f"Incorrect Response for set serial number: {resp}")
|
|
125
|
+
|
|
126
|
+
async def set_led(
|
|
127
|
+
self,
|
|
128
|
+
power: float,
|
|
129
|
+
color: Optional[LEDColor] = None,
|
|
130
|
+
external: Optional[bool] = None,
|
|
131
|
+
pattern: Optional[LEDPattern] = None,
|
|
132
|
+
duration: Optional[int] = None,
|
|
133
|
+
reps: Optional[int] = None,
|
|
134
|
+
) -> None:
|
|
135
|
+
"""Set LED Status bar color and pattern.
|
|
136
|
+
|
|
137
|
+
:param power: Power of the LED (0-1.0), 0 is off, 1 is full power
|
|
138
|
+
:param color: Color of the LED
|
|
139
|
+
:param external: True if external LED, False if internal LED
|
|
140
|
+
:param pattern: Animation pattern of the LED status bar
|
|
141
|
+
:param duration: Animation duration in milliseconds (25-10000), 10s max
|
|
142
|
+
:param reps: Number of times to repeat the animation (-1 - 10), -1 is forever.
|
|
143
|
+
"""
|
|
144
|
+
power = max(0, min(power, 1.0))
|
|
145
|
+
command = GCODE.SET_LED.build_command().add_float(
|
|
146
|
+
"P", power, GCODE_ROUNDING_PRECISION
|
|
147
|
+
)
|
|
148
|
+
if color is not None:
|
|
149
|
+
command.add_int("C", color.value)
|
|
150
|
+
if external is not None:
|
|
151
|
+
command.add_int("K", int(external))
|
|
152
|
+
if pattern is not None:
|
|
153
|
+
command.add_int("A", pattern.value)
|
|
154
|
+
if duration is not None:
|
|
155
|
+
duration = max(MIN_DURATION_MS, min(duration, MAX_DURATION_MS))
|
|
156
|
+
command.add_int("D", duration)
|
|
157
|
+
if reps is not None:
|
|
158
|
+
command.add_int("R", max(-1, min(reps, MAX_REPS)))
|
|
159
|
+
resp = await self._connection.send_command(command)
|
|
160
|
+
if not re.match(rf"^{GCODE.SET_LED}$", resp):
|
|
161
|
+
raise ValueError(f"Incorrect Response for set led: {resp}")
|
|
162
|
+
|
|
163
|
+
async def enable_pump(self) -> None:
|
|
164
|
+
"""Enables the vacuum pump, does not turn it on."""
|
|
165
|
+
...
|
|
166
|
+
|
|
167
|
+
async def disable_pump(self) -> None:
|
|
168
|
+
"""Disable the vacuum pump, doesn't just turn it off."""
|
|
169
|
+
...
|
|
170
|
+
|
|
171
|
+
async def get_pump_motor_register(self) -> None:
|
|
172
|
+
"""Get the register value of the pump motor driver."""
|
|
173
|
+
...
|
|
174
|
+
|
|
175
|
+
async def get_pressure_sensor_register(self) -> None:
|
|
176
|
+
"""Get the register value of the pressure sensor driver."""
|
|
177
|
+
...
|
|
178
|
+
|
|
179
|
+
async def get_pressure_sensor_reading_psi(self) -> float:
|
|
180
|
+
"""Get a reading from the pressure sensor."""
|
|
181
|
+
return 0.0
|
|
182
|
+
|
|
183
|
+
async def get_gage_pressure_reading_mbarg(self) -> float:
|
|
184
|
+
"""Read each pressure sensor and return the pressure difference."""
|
|
185
|
+
return 0.0
|
|
186
|
+
|
|
187
|
+
async def set_vacuum_chamber_pressure(
|
|
188
|
+
self,
|
|
189
|
+
gage_pressure_mbarg: float,
|
|
190
|
+
duration: Optional[float],
|
|
191
|
+
rate: Optional[float],
|
|
192
|
+
) -> None:
|
|
193
|
+
"""Engage or release the vacuum until a desired internal pressure is reached."""
|
|
194
|
+
...
|
|
195
|
+
|
|
196
|
+
# TODO: change pump power to be more specific when we find out how were gonna operate that
|
|
197
|
+
async def engage_vacuum(self, pump_power: Optional[float] = None) -> None:
|
|
198
|
+
"""Engage the vacuum without regard to chamber pressure."""
|
|
199
|
+
...
|
|
200
|
+
|
|
201
|
+
async def disengage_vacuum_pump(self) -> None:
|
|
202
|
+
"""Stops the vacuum pump, doesn't vent air or disable the motor."""
|
|
203
|
+
...
|
|
204
|
+
|
|
205
|
+
# turns off motor, then releases, takes a timeout for buffer between turn off and vent
|
|
206
|
+
async def vent(self) -> None:
|
|
207
|
+
"""Release the vacuum in the module chamber."""
|
|
208
|
+
...
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Vacuum Module-specific errors and exceptions."""
|
|
2
|
+
|
|
3
|
+
from opentrons.drivers.asyncio.communication.errors import (
|
|
4
|
+
BaseErrorCode,
|
|
5
|
+
ErrorResponse,
|
|
6
|
+
GCodeCacheFull,
|
|
7
|
+
TaskNotReady,
|
|
8
|
+
UnhandledGcode,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class EStopTriggered(ErrorResponse):
|
|
13
|
+
"""Raised when the estop is triggered during a module action."""
|
|
14
|
+
|
|
15
|
+
def __init__(self, port: str, response: str, command: str) -> None:
|
|
16
|
+
super().__init__(port, response, command)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PumpMotorError(ErrorResponse):
|
|
20
|
+
"""Raised when pump motor error is received."""
|
|
21
|
+
|
|
22
|
+
def __init__(self, port: str, response: str, command: str) -> None:
|
|
23
|
+
super().__init__(port, response, command)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StopRequested(ErrorResponse):
|
|
27
|
+
"""Raised when a stop is requested during a module action."""
|
|
28
|
+
|
|
29
|
+
def __init__(self, port: str, response: str, command: str) -> None:
|
|
30
|
+
super().__init__(port, response, command)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class VacuumModuleErrorCodes(BaseErrorCode):
|
|
34
|
+
"""Vacuum Module Error Codes."""
|
|
35
|
+
|
|
36
|
+
UNHANDLED_GCODE = ("ERR003", UnhandledGcode)
|
|
37
|
+
GCODE_CACHE_FULL = ("ERR004", GCodeCacheFull)
|
|
38
|
+
TASK_NOT_READY = ("ERR007", TaskNotReady)
|
|
39
|
+
STOP_REQUESTED = ("ERR504", StopRequested)
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
from .abstract import AbstractVacuumModuleDriver
|
|
4
|
+
from .types import VacuumModuleInfo, HardwareRevision, LEDPattern, LEDColor
|
|
5
|
+
from opentrons.util.async_helpers import ensure_yield
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimulatingDriver(AbstractVacuumModuleDriver):
|
|
9
|
+
def __init__(self, serial_number: str) -> None:
|
|
10
|
+
self._serial_number = serial_number
|
|
11
|
+
self.vacuum_on = False
|
|
12
|
+
self.pump_enabled = False
|
|
13
|
+
self.pressure_sensor_enabled = False
|
|
14
|
+
|
|
15
|
+
@ensure_yield
|
|
16
|
+
async def connect(self) -> None:
|
|
17
|
+
pass
|
|
18
|
+
|
|
19
|
+
@ensure_yield
|
|
20
|
+
async def disconnect(self) -> None:
|
|
21
|
+
pass
|
|
22
|
+
|
|
23
|
+
@ensure_yield
|
|
24
|
+
async def is_connected(self) -> bool:
|
|
25
|
+
return True
|
|
26
|
+
|
|
27
|
+
async def get_device_info(self) -> VacuumModuleInfo:
|
|
28
|
+
return VacuumModuleInfo(
|
|
29
|
+
fw="vacuum-fw", hw=HardwareRevision.NFF, sn=self._serial_number
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
async def set_serial_number(self, sn: str) -> None:
|
|
33
|
+
self._serial_number = sn
|
|
34
|
+
|
|
35
|
+
async def enable_pump(self) -> None:
|
|
36
|
+
self.pump_enabled = True
|
|
37
|
+
|
|
38
|
+
async def disable_pump(self) -> None:
|
|
39
|
+
self.pump_enabled = False
|
|
40
|
+
|
|
41
|
+
async def get_pump_motor_register(self) -> None:
|
|
42
|
+
"""Get the register value of the pump motor driver."""
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
async def get_pressure_sensor_register(self) -> None:
|
|
46
|
+
"""Get the register value of the pressure sensor driver."""
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
async def get_pressure_sensor_reading(self) -> float:
|
|
50
|
+
"""Get a reading from the pressure sensor."""
|
|
51
|
+
return 0.0
|
|
52
|
+
|
|
53
|
+
# TODO: update the pressure arg with the units when we find out which unit
|
|
54
|
+
async def set_vacuum_chamber_pressure(
|
|
55
|
+
self,
|
|
56
|
+
guage_pressure_mbar: float,
|
|
57
|
+
duration: Optional[float],
|
|
58
|
+
rate: Optional[float],
|
|
59
|
+
vent_after: bool = False,
|
|
60
|
+
) -> None:
|
|
61
|
+
"""Engage or release the vacuum until a desired internal pressure is reached."""
|
|
62
|
+
pass
|
|
63
|
+
|
|
64
|
+
async def engage_vacuum(self, pump_power: Optional[float] = None) -> None:
|
|
65
|
+
self.vacuum_on = True
|
|
66
|
+
|
|
67
|
+
async def vent(self, delay_s: float = 0.0) -> None:
|
|
68
|
+
self.vacuum_on = False
|
|
69
|
+
|
|
70
|
+
async def set_led(
|
|
71
|
+
self,
|
|
72
|
+
power: float,
|
|
73
|
+
color: Optional[LEDColor] = None,
|
|
74
|
+
external: Optional[bool] = None,
|
|
75
|
+
pattern: Optional[LEDPattern] = None,
|
|
76
|
+
duration: Optional[int] = None, # Default firmware duration is 500ms
|
|
77
|
+
reps: Optional[int] = None, # Default firmware reps is 0
|
|
78
|
+
) -> None:
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
async def enter_programming_mode(self) -> None:
|
|
82
|
+
pass
|
|
83
|
+
|
|
84
|
+
async def reset_serial_buffers(self) -> None:
|
|
85
|
+
pass
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from typing import Dict
|
|
4
|
+
|
|
5
|
+
from opentrons.drivers.command_builder import CommandBuilder
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class GCODE(str, Enum):
|
|
9
|
+
|
|
10
|
+
GET_RESET_REASON = "M114"
|
|
11
|
+
GET_DEVICE_INFO = "M115"
|
|
12
|
+
SET_SERIAL_NUMBER = "M996"
|
|
13
|
+
ENTER_BOOTLOADER = "dfu"
|
|
14
|
+
SET_LED = "M200"
|
|
15
|
+
|
|
16
|
+
def build_command(self) -> CommandBuilder:
|
|
17
|
+
"""Build command."""
|
|
18
|
+
return CommandBuilder().add_gcode(self)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class HardwareRevision(Enum):
|
|
22
|
+
"""Hardware Revision."""
|
|
23
|
+
|
|
24
|
+
NFF = "nff"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@dataclass
|
|
28
|
+
class VacuumModuleInfo:
|
|
29
|
+
"""Vacuum module info."""
|
|
30
|
+
|
|
31
|
+
fw: str
|
|
32
|
+
hw: HardwareRevision
|
|
33
|
+
sn: str
|
|
34
|
+
rr: int = 0
|
|
35
|
+
|
|
36
|
+
def to_dict(self) -> Dict[str, str]:
|
|
37
|
+
"""Build vacuum module info."""
|
|
38
|
+
return {
|
|
39
|
+
"serial": self.sn,
|
|
40
|
+
"version": self.fw,
|
|
41
|
+
"model": self.hw.value,
|
|
42
|
+
"reset_reason": str(self.rr),
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LEDColor(Enum):
|
|
47
|
+
"""Vacuum Module LED Color."""
|
|
48
|
+
|
|
49
|
+
WHITE = 0
|
|
50
|
+
RED = 1
|
|
51
|
+
GREEN = 2
|
|
52
|
+
BLUE = 3
|
|
53
|
+
YELLOW = 4
|
|
54
|
+
|
|
55
|
+
@classmethod
|
|
56
|
+
def from_name(cls, name: str) -> "LEDColor":
|
|
57
|
+
match (name.lower()):
|
|
58
|
+
case "red":
|
|
59
|
+
return cls.RED
|
|
60
|
+
case "green":
|
|
61
|
+
return cls.GREEN
|
|
62
|
+
case "blue":
|
|
63
|
+
return cls.BLUE
|
|
64
|
+
case "yellow":
|
|
65
|
+
return cls.YELLOW
|
|
66
|
+
case _:
|
|
67
|
+
return cls.WHITE
|
|
68
|
+
|
|
69
|
+
def to_name(self) -> "str":
|
|
70
|
+
return self.name.lower()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class LEDPattern(Enum):
|
|
74
|
+
"""Vacuum Module LED Pattern."""
|
|
75
|
+
|
|
76
|
+
STATIC = 0
|
|
77
|
+
FLASH = 1
|
|
78
|
+
PULSE = 2
|
|
79
|
+
CONFIRM = 3
|
opentrons/execute.py
CHANGED
|
@@ -555,6 +555,7 @@ def _create_live_context_pe(
|
|
|
555
555
|
load_fixed_trash=should_load_fixed_trash_labware_for_python_protocol(
|
|
556
556
|
api_version
|
|
557
557
|
),
|
|
558
|
+
camera_provider=None,
|
|
558
559
|
)
|
|
559
560
|
)
|
|
560
561
|
|
|
@@ -644,6 +645,7 @@ def _run_file_pe(
|
|
|
644
645
|
hardware_api=hardware_api_wrapped,
|
|
645
646
|
)
|
|
646
647
|
|
|
648
|
+
# todo (chb, 2025-09-30): The Camera Provider is provided in to a run by the robot server, no analog exists for execute
|
|
647
649
|
orchestrator = RunOrchestrator(
|
|
648
650
|
hardware_api=hardware_api_wrapped,
|
|
649
651
|
protocol_engine=protocol_engine,
|
|
@@ -657,6 +659,7 @@ def _run_file_pe(
|
|
|
657
659
|
protocol_live_runner=LiveRunner(
|
|
658
660
|
protocol_engine=protocol_engine, hardware_api=hardware_api_wrapped
|
|
659
661
|
),
|
|
662
|
+
camera_provider=None,
|
|
660
663
|
)
|
|
661
664
|
|
|
662
665
|
unsubscribe = protocol_runner.broker.subscribe(
|
|
@@ -176,6 +176,7 @@ class FlexBackend(Protocol):
|
|
|
176
176
|
speed: float,
|
|
177
177
|
stop_condition: HWStopCondition = HWStopCondition.none,
|
|
178
178
|
nodes_in_moves_only: bool = True,
|
|
179
|
+
delay: Optional[Tuple[List[Axis], float]] = None,
|
|
179
180
|
) -> None:
|
|
180
181
|
"""Move to a position.
|
|
181
182
|
|
|
@@ -451,6 +452,7 @@ class FlexBackend(Protocol):
|
|
|
451
452
|
max_allowed_grip_error: float,
|
|
452
453
|
hard_limit_lower: float,
|
|
453
454
|
hard_limit_upper: float,
|
|
455
|
+
disable_geometry_grip_check: bool = False,
|
|
454
456
|
) -> None:
|
|
455
457
|
...
|
|
456
458
|
|