opentrons 8.7.0a0__py3-none-any.whl → 8.7.0a2__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/drivers/thermocycler/abstract.py +1 -0
- opentrons/drivers/thermocycler/driver.py +33 -4
- opentrons/drivers/thermocycler/simulator.py +2 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/ot3controller.py +3 -0
- opentrons/hardware_control/backends/ot3simulator.py +2 -1
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/module_control.py +82 -8
- 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 +30 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +13 -5
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +56 -10
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +26 -5
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +31 -2
- opentrons/legacy_commands/protocol_commands.py +20 -0
- opentrons/legacy_commands/types.py +42 -0
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -0
- opentrons/protocol_api/_types.py +6 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +32 -11
- opentrons/protocol_api/core/engine/_default_liquid_class_versions.py +2 -0
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +4 -0
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +77 -17
- opentrons/protocol_api/core/engine/protocol.py +18 -1
- opentrons/protocol_api/core/engine/tasks.py +35 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +2 -0
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -1
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- 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 +1 -0
- opentrons/protocol_api/core/protocol.py +11 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/module_contexts.py +1 -0
- opentrons/protocol_api/protocol_context.py +26 -4
- opentrons/protocol_api/robot_context.py +38 -21
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +6 -1
- opentrons/protocol_engine/actions/__init__.py +4 -2
- opentrons/protocol_engine/actions/actions.py +22 -9
- opentrons/protocol_engine/clients/sync_client.py +6 -7
- opentrons/protocol_engine/commands/__init__.py +42 -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/aspirate.py +1 -0
- opentrons/protocol_engine/commands/command.py +1 -0
- opentrons/protocol_engine/commands/command_unions.py +39 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/movement_common.py +2 -0
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/set_tip_state.py +97 -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 +17 -1
- 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/errors/__init__.py +4 -0
- opentrons/protocol_engine/errors/exceptions.py +55 -0
- opentrons/protocol_engine/execution/__init__.py +2 -0
- opentrons/protocol_engine/execution/command_executor.py +8 -0
- opentrons/protocol_engine/execution/create_queue_worker.py +5 -1
- opentrons/protocol_engine/execution/labware_movement.py +9 -12
- opentrons/protocol_engine/execution/movement.py +2 -0
- 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 +67 -33
- opentrons/protocol_engine/resources/__init__.py +2 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/commands.py +7 -7
- opentrons/protocol_engine/state/geometry.py +237 -379
- opentrons/protocol_engine/state/labware.py +52 -102
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1331 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +26 -7
- opentrons/protocol_engine/state/motion.py +44 -0
- opentrons/protocol_engine/state/state.py +14 -0
- opentrons/protocol_engine/state/state_summary.py +2 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +16 -9
- opentrons/protocol_engine/types/__init__.py +9 -3
- 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/module.py +10 -0
- 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/run_orchestrator.py +18 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +48 -15
- opentrons/system/camera.py +1 -1
- {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/RECORD +120 -107
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a0.dist-info → opentrons-8.7.0a2.dist-info}/licenses/LICENSE +0 -0
|
@@ -2,7 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from typing import Optional, Mapping
|
|
5
|
+
from typing import Optional, Mapping, Callable
|
|
6
6
|
from typing_extensions import Final
|
|
7
7
|
|
|
8
8
|
from opentrons.drivers.rpi_drivers.types import USBPort
|
|
@@ -15,6 +15,7 @@ from opentrons.hardware_control.poller import Reader, Poller
|
|
|
15
15
|
from opentrons.hardware_control.modules import mod_abc, update
|
|
16
16
|
from opentrons.hardware_control.modules.types import (
|
|
17
17
|
ModuleDisconnectedCallback,
|
|
18
|
+
ModuleErrorCallback,
|
|
18
19
|
ModuleType,
|
|
19
20
|
TemperatureStatus,
|
|
20
21
|
SpeedStatus,
|
|
@@ -47,12 +48,13 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
47
48
|
port: str,
|
|
48
49
|
usb_port: USBPort,
|
|
49
50
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
50
|
-
execution_manager:
|
|
51
|
+
execution_manager: ExecutionManager,
|
|
52
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
53
|
+
error_callback: ModuleErrorCallback,
|
|
51
54
|
poll_interval_seconds: Optional[float] = None,
|
|
52
55
|
simulating: bool = False,
|
|
53
56
|
sim_model: Optional[str] = None,
|
|
54
57
|
sim_serial_number: Optional[str] = None,
|
|
55
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
56
58
|
) -> "HeaterShaker":
|
|
57
59
|
"""
|
|
58
60
|
Build a HeaterShaker
|
|
@@ -67,6 +69,7 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
67
69
|
loop: Loop
|
|
68
70
|
sim_model: The model name used by simulator
|
|
69
71
|
disconnected_callback: Callback to inform the module controller that the device was disconnected
|
|
72
|
+
error_callback: Callback to inform the module controller of an asynchronous error
|
|
70
73
|
|
|
71
74
|
Returns:
|
|
72
75
|
HeaterShaker instance
|
|
@@ -91,6 +94,7 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
91
94
|
hw_control_loop=hw_control_loop,
|
|
92
95
|
execution_manager=execution_manager,
|
|
93
96
|
disconnected_callback=disconnected_callback,
|
|
97
|
+
error_callback=error_callback,
|
|
94
98
|
)
|
|
95
99
|
|
|
96
100
|
try:
|
|
@@ -109,8 +113,9 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
109
113
|
poller: Poller,
|
|
110
114
|
device_info: Mapping[str, str],
|
|
111
115
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
112
|
-
execution_manager:
|
|
113
|
-
disconnected_callback: ModuleDisconnectedCallback
|
|
116
|
+
execution_manager: ExecutionManager,
|
|
117
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
118
|
+
error_callback: ModuleErrorCallback,
|
|
114
119
|
):
|
|
115
120
|
super().__init__(
|
|
116
121
|
port=port,
|
|
@@ -118,14 +123,22 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
118
123
|
hw_control_loop=hw_control_loop,
|
|
119
124
|
execution_manager=execution_manager,
|
|
120
125
|
disconnected_callback=disconnected_callback,
|
|
126
|
+
error_callback=error_callback,
|
|
121
127
|
)
|
|
122
128
|
self._device_info = device_info
|
|
123
129
|
self._driver = driver
|
|
124
130
|
self._reader = reader
|
|
125
131
|
self._poller = poller
|
|
132
|
+
self._unsubscribe_reader = self._reader.register_error_handler(
|
|
133
|
+
self._handle_error
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
def _handle_error(self, error: Exception) -> None:
|
|
137
|
+
self.error_callback(error)
|
|
126
138
|
|
|
127
139
|
async def cleanup(self) -> None:
|
|
128
140
|
"""Stop the poller task"""
|
|
141
|
+
self._unsubscribe_reader()
|
|
129
142
|
await self._poller.stop()
|
|
130
143
|
await self._driver.disconnect()
|
|
131
144
|
|
|
@@ -397,6 +410,16 @@ class HeaterShakerReader(Reader):
|
|
|
397
410
|
self.labware_latch = HeaterShakerLabwareLatchStatus.IDLE_UNKNOWN
|
|
398
411
|
self.error: Optional[str] = None
|
|
399
412
|
self._driver = driver
|
|
413
|
+
self._handle_error: Callable[[Exception], None] | None = None
|
|
414
|
+
|
|
415
|
+
def register_error_handler(
|
|
416
|
+
self, handle_error: Callable[[Exception], None]
|
|
417
|
+
) -> Callable[[], None]:
|
|
418
|
+
self._handle_error = handle_error
|
|
419
|
+
return self._unsubscribe_error_handler
|
|
420
|
+
|
|
421
|
+
def _unsubscribe_error_handler(self) -> None:
|
|
422
|
+
self._handle_error = None
|
|
400
423
|
|
|
401
424
|
async def read(self) -> None:
|
|
402
425
|
await self.read_temperature()
|
|
@@ -420,6 +443,8 @@ class HeaterShakerReader(Reader):
|
|
|
420
443
|
if exception is None:
|
|
421
444
|
self.error = None
|
|
422
445
|
else:
|
|
446
|
+
if self._handle_error:
|
|
447
|
+
self._handle_error(exception)
|
|
423
448
|
try:
|
|
424
449
|
self.error = str(exception.args[0])
|
|
425
450
|
except Exception:
|
|
@@ -49,12 +49,13 @@ class MagDeck(mod_abc.AbstractModule):
|
|
|
49
49
|
port: str,
|
|
50
50
|
usb_port: USBPort,
|
|
51
51
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
52
|
-
execution_manager:
|
|
52
|
+
execution_manager: ExecutionManager,
|
|
53
|
+
disconnected_callback: types.ModuleDisconnectedCallback,
|
|
54
|
+
error_callback: types.ModuleErrorCallback,
|
|
53
55
|
poll_interval_seconds: Optional[float] = None,
|
|
54
56
|
simulating: bool = False,
|
|
55
57
|
sim_model: Optional[str] = None,
|
|
56
58
|
sim_serial_number: Optional[str] = None,
|
|
57
|
-
disconnected_callback: types.ModuleDisconnectedCallback = None,
|
|
58
59
|
) -> "MagDeck":
|
|
59
60
|
"""Factory function."""
|
|
60
61
|
driver: AbstractMagDeckDriver
|
|
@@ -73,6 +74,7 @@ class MagDeck(mod_abc.AbstractModule):
|
|
|
73
74
|
device_info=await driver.get_device_info(),
|
|
74
75
|
driver=driver,
|
|
75
76
|
disconnected_callback=disconnected_callback,
|
|
77
|
+
error_callback=error_callback,
|
|
76
78
|
)
|
|
77
79
|
return mod
|
|
78
80
|
|
|
@@ -83,8 +85,9 @@ class MagDeck(mod_abc.AbstractModule):
|
|
|
83
85
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
84
86
|
driver: AbstractMagDeckDriver,
|
|
85
87
|
device_info: Dict[str, str],
|
|
86
|
-
execution_manager:
|
|
87
|
-
disconnected_callback: types.ModuleDisconnectedCallback
|
|
88
|
+
execution_manager: ExecutionManager,
|
|
89
|
+
disconnected_callback: types.ModuleDisconnectedCallback,
|
|
90
|
+
error_callback: types.ModuleErrorCallback,
|
|
88
91
|
) -> None:
|
|
89
92
|
"""Constructor"""
|
|
90
93
|
super().__init__(
|
|
@@ -93,6 +96,7 @@ class MagDeck(mod_abc.AbstractModule):
|
|
|
93
96
|
hw_control_loop=hw_control_loop,
|
|
94
97
|
execution_manager=execution_manager,
|
|
95
98
|
disconnected_callback=disconnected_callback,
|
|
99
|
+
error_callback=error_callback,
|
|
96
100
|
)
|
|
97
101
|
self._device_info = device_info
|
|
98
102
|
self._driver = driver
|
|
@@ -11,6 +11,7 @@ from ..execution_manager import ExecutionManager
|
|
|
11
11
|
from .types import (
|
|
12
12
|
BundledFirmware,
|
|
13
13
|
ModuleDisconnectedCallback,
|
|
14
|
+
ModuleErrorCallback,
|
|
14
15
|
UploadFunction,
|
|
15
16
|
LiveData,
|
|
16
17
|
ModuleType,
|
|
@@ -47,12 +48,13 @@ class AbstractModule(abc.ABC):
|
|
|
47
48
|
port: str,
|
|
48
49
|
usb_port: USBPort,
|
|
49
50
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
50
|
-
execution_manager:
|
|
51
|
-
|
|
51
|
+
execution_manager: ExecutionManager,
|
|
52
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
53
|
+
error_callback: ModuleErrorCallback,
|
|
54
|
+
poll_interval_seconds: float | None = None,
|
|
52
55
|
simulating: bool = False,
|
|
53
56
|
sim_model: Optional[str] = None,
|
|
54
57
|
sim_serial_number: Optional[str] = None,
|
|
55
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
56
58
|
) -> "AbstractModule":
|
|
57
59
|
"""Modules should always be created using this factory.
|
|
58
60
|
|
|
@@ -65,8 +67,9 @@ class AbstractModule(abc.ABC):
|
|
|
65
67
|
port: str,
|
|
66
68
|
usb_port: USBPort,
|
|
67
69
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
68
|
-
execution_manager:
|
|
69
|
-
disconnected_callback: ModuleDisconnectedCallback
|
|
70
|
+
execution_manager: ExecutionManager,
|
|
71
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
72
|
+
error_callback: ModuleErrorCallback,
|
|
70
73
|
) -> None:
|
|
71
74
|
self._port = port
|
|
72
75
|
self._usb_port = usb_port
|
|
@@ -75,6 +78,7 @@ class AbstractModule(abc.ABC):
|
|
|
75
78
|
self._bundled_fw: Optional[BundledFirmware] = self.get_bundled_fw()
|
|
76
79
|
self._disconnected_callback = disconnected_callback
|
|
77
80
|
self._updating = False
|
|
81
|
+
self._error_callback = error_callback
|
|
78
82
|
|
|
79
83
|
@staticmethod
|
|
80
84
|
def sort_key(inst: "AbstractModule") -> int:
|
|
@@ -103,6 +107,10 @@ class AbstractModule(abc.ABC):
|
|
|
103
107
|
if self._disconnected_callback is not None:
|
|
104
108
|
self._disconnected_callback(self.port, self.serial_number)
|
|
105
109
|
|
|
110
|
+
def error_callback(self, exc: Exception) -> None:
|
|
111
|
+
"""Called from within the module object when an asynchronous hardware error occurrs."""
|
|
112
|
+
self._error_callback(exc, self.model(), self.port, self.serial_number)
|
|
113
|
+
|
|
106
114
|
def get_bundled_fw(self) -> Optional[BundledFirmware]:
|
|
107
115
|
"""Get absolute path to bundled version of module fw if available."""
|
|
108
116
|
if not IS_ROBOT:
|
|
@@ -2,10 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from typing import Dict, Optional
|
|
5
|
+
from typing import Dict, Optional, Callable
|
|
6
6
|
|
|
7
7
|
from opentrons.hardware_control.modules.types import (
|
|
8
8
|
ModuleDisconnectedCallback,
|
|
9
|
+
ModuleErrorCallback,
|
|
9
10
|
TemperatureStatus,
|
|
10
11
|
)
|
|
11
12
|
from opentrons.hardware_control.poller import Reader, Poller
|
|
@@ -38,12 +39,13 @@ class TempDeck(mod_abc.AbstractModule):
|
|
|
38
39
|
port: str,
|
|
39
40
|
usb_port: USBPort,
|
|
40
41
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
41
|
-
execution_manager:
|
|
42
|
+
execution_manager: ExecutionManager,
|
|
43
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
44
|
+
error_callback: ModuleErrorCallback,
|
|
42
45
|
poll_interval_seconds: Optional[float] = None,
|
|
43
46
|
simulating: bool = False,
|
|
44
47
|
sim_model: Optional[str] = None,
|
|
45
48
|
sim_serial_number: Optional[str] = None,
|
|
46
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
47
49
|
) -> "TempDeck":
|
|
48
50
|
"""
|
|
49
51
|
Build a TempDeck
|
|
@@ -83,6 +85,7 @@ class TempDeck(mod_abc.AbstractModule):
|
|
|
83
85
|
device_info=await driver.get_device_info(),
|
|
84
86
|
hw_control_loop=hw_control_loop,
|
|
85
87
|
disconnected_callback=disconnected_callback,
|
|
88
|
+
error_callback=error_callback,
|
|
86
89
|
)
|
|
87
90
|
|
|
88
91
|
try:
|
|
@@ -101,8 +104,9 @@ class TempDeck(mod_abc.AbstractModule):
|
|
|
101
104
|
poller: Poller,
|
|
102
105
|
device_info: Dict[str, str],
|
|
103
106
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
104
|
-
execution_manager:
|
|
105
|
-
disconnected_callback: ModuleDisconnectedCallback
|
|
107
|
+
execution_manager: ExecutionManager,
|
|
108
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
109
|
+
error_callback: ModuleErrorCallback,
|
|
106
110
|
) -> None:
|
|
107
111
|
"""Constructor"""
|
|
108
112
|
super().__init__(
|
|
@@ -111,11 +115,13 @@ class TempDeck(mod_abc.AbstractModule):
|
|
|
111
115
|
hw_control_loop=hw_control_loop,
|
|
112
116
|
execution_manager=execution_manager,
|
|
113
117
|
disconnected_callback=disconnected_callback,
|
|
118
|
+
error_callback=error_callback,
|
|
114
119
|
)
|
|
115
120
|
self._device_info = device_info
|
|
116
121
|
self._driver = driver
|
|
117
122
|
self._reader = reader
|
|
118
123
|
self._poller = poller
|
|
124
|
+
self._reader.set_error_callback(self.error_callback)
|
|
119
125
|
|
|
120
126
|
async def cleanup(self) -> None:
|
|
121
127
|
"""Stop the poller task."""
|
|
@@ -293,7 +299,21 @@ class TempDeckReader(Reader):
|
|
|
293
299
|
def __init__(self, driver: AbstractTempDeckDriver) -> None:
|
|
294
300
|
self.temperature = Temperature(current=25, target=None)
|
|
295
301
|
self._driver = driver
|
|
302
|
+
self._error_callback: Optional[Callable[[Exception], None]] = None
|
|
296
303
|
|
|
297
304
|
async def read(self) -> None:
|
|
298
305
|
"""Read the module's current and target temperatures."""
|
|
299
306
|
self.temperature = await self._driver.get_temperature()
|
|
307
|
+
|
|
308
|
+
def set_error_callback(
|
|
309
|
+
self, error_callback: Callable[[Exception], None]
|
|
310
|
+
) -> Callable[[], None]:
|
|
311
|
+
self._error_callback = error_callback
|
|
312
|
+
return self._remove_error_callback
|
|
313
|
+
|
|
314
|
+
def _remove_error_callback(self) -> None:
|
|
315
|
+
self._error_callback = None
|
|
316
|
+
|
|
317
|
+
def on_error(self, exception: Exception) -> None:
|
|
318
|
+
if self._error_callback:
|
|
319
|
+
self._error_callback(exception)
|
|
@@ -9,6 +9,7 @@ from opentrons.hardware_control.modules.lid_temp_status import LidTemperatureSta
|
|
|
9
9
|
from opentrons.hardware_control.modules.plate_temp_status import PlateTemperatureStatus
|
|
10
10
|
from opentrons.hardware_control.modules.types import (
|
|
11
11
|
ModuleDisconnectedCallback,
|
|
12
|
+
ModuleErrorCallback,
|
|
12
13
|
TemperatureStatus,
|
|
13
14
|
)
|
|
14
15
|
from opentrons.hardware_control.poller import Reader, Poller
|
|
@@ -36,6 +37,8 @@ DFU_PID = "df11"
|
|
|
36
37
|
_TC_PLATE_LIFT_OPEN_DEGREES = 20
|
|
37
38
|
_TC_PLATE_LIFT_RETURN_DEGREES = 23
|
|
38
39
|
|
|
40
|
+
_TC_RAMP_RATE_ADDED_VERSION = (1, 0, 8) # v1.0.8
|
|
41
|
+
|
|
39
42
|
|
|
40
43
|
class ThermocyclerError(Exception):
|
|
41
44
|
pass
|
|
@@ -62,12 +65,13 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
62
65
|
port: str,
|
|
63
66
|
usb_port: USBPort,
|
|
64
67
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
65
|
-
execution_manager:
|
|
68
|
+
execution_manager: ExecutionManager,
|
|
69
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
70
|
+
error_callback: ModuleErrorCallback,
|
|
66
71
|
poll_interval_seconds: Optional[float] = None,
|
|
67
72
|
simulating: bool = False,
|
|
68
73
|
sim_model: Optional[str] = None,
|
|
69
74
|
sim_serial_number: Optional[str] = None,
|
|
70
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
71
75
|
) -> "Thermocycler":
|
|
72
76
|
"""
|
|
73
77
|
Build and connect to a Thermocycler
|
|
@@ -108,6 +112,7 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
108
112
|
hw_control_loop=hw_control_loop,
|
|
109
113
|
execution_manager=execution_manager,
|
|
110
114
|
disconnected_callback=disconnected_callback,
|
|
115
|
+
error_callback=error_callback,
|
|
111
116
|
)
|
|
112
117
|
|
|
113
118
|
try:
|
|
@@ -126,8 +131,9 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
126
131
|
poller: Poller,
|
|
127
132
|
device_info: Dict[str, str],
|
|
128
133
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
129
|
-
execution_manager:
|
|
130
|
-
disconnected_callback: ModuleDisconnectedCallback
|
|
134
|
+
execution_manager: ExecutionManager,
|
|
135
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
136
|
+
error_callback: ModuleErrorCallback,
|
|
131
137
|
) -> None:
|
|
132
138
|
"""
|
|
133
139
|
Constructor
|
|
@@ -150,6 +156,7 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
150
156
|
hw_control_loop=hw_control_loop,
|
|
151
157
|
execution_manager=execution_manager,
|
|
152
158
|
disconnected_callback=disconnected_callback,
|
|
159
|
+
error_callback=error_callback,
|
|
153
160
|
)
|
|
154
161
|
self._device_info = device_info
|
|
155
162
|
self._reader = reader
|
|
@@ -159,10 +166,13 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
159
166
|
self._total_step_count: Optional[int] = None
|
|
160
167
|
self._current_step_index: Optional[int] = None
|
|
161
168
|
self._error: Optional[str] = None
|
|
162
|
-
self._reader.register_error_handler(
|
|
169
|
+
self._unsubscribe_reader = self._reader.register_error_handler(
|
|
170
|
+
self._enter_error_state
|
|
171
|
+
)
|
|
163
172
|
|
|
164
173
|
async def cleanup(self) -> None:
|
|
165
174
|
"""Stop the poller task."""
|
|
175
|
+
self._unsubscribe_reader()
|
|
166
176
|
await self._poller.stop()
|
|
167
177
|
await self._driver.disconnect()
|
|
168
178
|
|
|
@@ -265,6 +275,19 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
265
275
|
await self.open()
|
|
266
276
|
await self._wait_for_lid_status(ThermocyclerLidStatus.OPEN)
|
|
267
277
|
|
|
278
|
+
def can_use_ramp_rate(self) -> bool:
|
|
279
|
+
version_string = self._device_info.get("version", "v")
|
|
280
|
+
if version_string.startswith("v"):
|
|
281
|
+
version_string = version_string[1:]
|
|
282
|
+
try:
|
|
283
|
+
version_tuple = tuple(int(c) for c in version_string.split("."))
|
|
284
|
+
return version_tuple >= _TC_RAMP_RATE_ADDED_VERSION
|
|
285
|
+
except (ValueError, IndexError):
|
|
286
|
+
log.error(
|
|
287
|
+
f"Invalid version from device: {self._device_info.get('version', '')}"
|
|
288
|
+
)
|
|
289
|
+
return False
|
|
290
|
+
|
|
268
291
|
async def set_temperature(
|
|
269
292
|
self,
|
|
270
293
|
temperature: float,
|
|
@@ -290,6 +313,11 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
290
313
|
|
|
291
314
|
Returns: None
|
|
292
315
|
"""
|
|
316
|
+
if ramp_rate and not self.can_use_ramp_rate():
|
|
317
|
+
raise ThermocyclerError(
|
|
318
|
+
"Ramp rate is not supported by this thermocycler's firmware version, please update."
|
|
319
|
+
)
|
|
320
|
+
|
|
293
321
|
await self.wait_for_is_running()
|
|
294
322
|
await self._set_temperature_no_pause(
|
|
295
323
|
temperature=temperature,
|
|
@@ -312,11 +340,13 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
312
340
|
total_seconds = seconds + (minutes * 60)
|
|
313
341
|
hold_time = total_seconds if total_seconds > 0 else 0
|
|
314
342
|
|
|
315
|
-
if ramp_rate
|
|
316
|
-
|
|
343
|
+
if ramp_rate and not self.can_use_ramp_rate():
|
|
344
|
+
raise ThermocyclerError(
|
|
345
|
+
"Ramp rate is not supported by this thermocycler's firmware version, please update."
|
|
346
|
+
)
|
|
317
347
|
|
|
318
348
|
await self._driver.set_plate_temperature(
|
|
319
|
-
temp=temperature, hold_time=hold_time, volume=volume
|
|
349
|
+
temp=temperature, hold_time=hold_time, volume=volume, ramp_rate=ramp_rate
|
|
320
350
|
)
|
|
321
351
|
|
|
322
352
|
task = self._loop.create_task(self._wait_for_block_target())
|
|
@@ -419,6 +449,7 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
419
449
|
celsius: float,
|
|
420
450
|
hold_time_seconds: Optional[float] = None,
|
|
421
451
|
volume: Optional[float] = None,
|
|
452
|
+
ramp_rate: Optional[float] = None,
|
|
422
453
|
) -> None:
|
|
423
454
|
"""Set the Thermocycler's target block temperature.
|
|
424
455
|
|
|
@@ -428,10 +459,17 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
428
459
|
celsius: The target block temperature, in degrees celsius.
|
|
429
460
|
"""
|
|
430
461
|
await self.wait_for_is_running()
|
|
462
|
+
|
|
463
|
+
if ramp_rate and not self.can_use_ramp_rate():
|
|
464
|
+
raise ThermocyclerError(
|
|
465
|
+
"Ramp rate is not supported by this thermocycler's firmware version, please update."
|
|
466
|
+
)
|
|
467
|
+
|
|
431
468
|
await self._driver.set_plate_temperature(
|
|
432
469
|
temp=celsius,
|
|
433
470
|
hold_time=hold_time_seconds,
|
|
434
471
|
volume=volume,
|
|
472
|
+
ramp_rate=ramp_rate,
|
|
435
473
|
)
|
|
436
474
|
await self._reader.read_block_temperature()
|
|
437
475
|
|
|
@@ -596,11 +634,12 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
596
634
|
temperature = step.get("temperature")
|
|
597
635
|
hold_time_minutes = step.get("hold_time_minutes", None)
|
|
598
636
|
hold_time_seconds = step.get("hold_time_seconds", None)
|
|
637
|
+
ramp_rate = step.get("ramp_rate", None)
|
|
599
638
|
await self._set_temperature_no_pause(
|
|
600
639
|
temperature=temperature, # type: ignore
|
|
601
640
|
hold_time_minutes=hold_time_minutes,
|
|
602
641
|
hold_time_seconds=hold_time_seconds,
|
|
603
|
-
ramp_rate=
|
|
642
|
+
ramp_rate=ramp_rate,
|
|
604
643
|
volume=volume,
|
|
605
644
|
)
|
|
606
645
|
|
|
@@ -673,6 +712,7 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
673
712
|
f" for troubleshooting."
|
|
674
713
|
)
|
|
675
714
|
asyncio.run_coroutine_threadsafe(self.cleanup(), self._loop)
|
|
715
|
+
self.error_callback(error)
|
|
676
716
|
|
|
677
717
|
|
|
678
718
|
class ThermocyclerReader(Reader):
|
|
@@ -710,8 +750,14 @@ class ThermocyclerReader(Reader):
|
|
|
710
750
|
if self._handle_error is not None:
|
|
711
751
|
self._handle_error(exception)
|
|
712
752
|
|
|
713
|
-
def register_error_handler(
|
|
753
|
+
def register_error_handler(
|
|
754
|
+
self, handle_error: Callable[[Exception], None]
|
|
755
|
+
) -> Callable[[], None]:
|
|
714
756
|
self._handle_error = handle_error
|
|
757
|
+
return self._unsubscribe_error_handler
|
|
758
|
+
|
|
759
|
+
def _unsubscribe_error_handler(self) -> None:
|
|
760
|
+
self._handle_error = None
|
|
715
761
|
|
|
716
762
|
async def read(self) -> None:
|
|
717
763
|
"""Poll the thermocycler."""
|
|
@@ -11,6 +11,7 @@ from typing import (
|
|
|
11
11
|
Awaitable,
|
|
12
12
|
Union,
|
|
13
13
|
Optional,
|
|
14
|
+
Protocol,
|
|
14
15
|
cast,
|
|
15
16
|
TYPE_CHECKING,
|
|
16
17
|
TypeGuard,
|
|
@@ -44,6 +45,7 @@ class ThermocyclerStepBase(TypedDict):
|
|
|
44
45
|
class ThermocyclerStep(ThermocyclerStepBase, total=False):
|
|
45
46
|
hold_time_seconds: float
|
|
46
47
|
hold_time_minutes: float
|
|
48
|
+
ramp_rate: Optional[float]
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
class ThermocyclerCycle(TypedDict):
|
|
@@ -54,7 +56,24 @@ class ThermocyclerCycle(TypedDict):
|
|
|
54
56
|
UploadFunction = Callable[[str, str, Dict[str, Any]], Awaitable[Tuple[bool, str]]]
|
|
55
57
|
|
|
56
58
|
|
|
57
|
-
ModuleDisconnectedCallback
|
|
59
|
+
class ModuleDisconnectedCallback(Protocol):
|
|
60
|
+
"""Protocol for the callback when the module should be disconnected."""
|
|
61
|
+
|
|
62
|
+
def __call__(self, port: str, serial: str | None) -> None:
|
|
63
|
+
...
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class ModuleErrorCallback(Protocol):
|
|
67
|
+
"""Protocol for the callback when the module sees a hardware error."""
|
|
68
|
+
|
|
69
|
+
def __call__(
|
|
70
|
+
self,
|
|
71
|
+
exc: Exception,
|
|
72
|
+
model: str,
|
|
73
|
+
port: str,
|
|
74
|
+
serial: str | None,
|
|
75
|
+
) -> None:
|
|
76
|
+
...
|
|
58
77
|
|
|
59
78
|
|
|
60
79
|
class MagneticModuleData(TypedDict):
|
|
@@ -6,7 +6,12 @@ from opentrons.drivers.rpi_drivers.types import USBPort
|
|
|
6
6
|
|
|
7
7
|
from ..execution_manager import ExecutionManager
|
|
8
8
|
|
|
9
|
-
from .types import
|
|
9
|
+
from .types import (
|
|
10
|
+
ModuleDisconnectedCallback,
|
|
11
|
+
ModuleType,
|
|
12
|
+
SpeedStatus,
|
|
13
|
+
ModuleErrorCallback,
|
|
14
|
+
)
|
|
10
15
|
from .mod_abc import AbstractModule
|
|
11
16
|
from .tempdeck import TempDeck
|
|
12
17
|
from .magdeck import MagDeck
|
|
@@ -46,10 +51,11 @@ async def build(
|
|
|
46
51
|
simulating: bool,
|
|
47
52
|
usb_port: USBPort,
|
|
48
53
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
49
|
-
execution_manager:
|
|
54
|
+
execution_manager: ExecutionManager,
|
|
55
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
56
|
+
error_callback: ModuleErrorCallback,
|
|
50
57
|
sim_model: Optional[str] = None,
|
|
51
58
|
sim_serial_number: Optional[str] = None,
|
|
52
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
53
59
|
) -> AbstractModule:
|
|
54
60
|
return await _MODULE_CLS_BY_TYPE[type].build(
|
|
55
61
|
port=port,
|
|
@@ -57,9 +63,10 @@ async def build(
|
|
|
57
63
|
simulating=simulating,
|
|
58
64
|
hw_control_loop=hw_control_loop,
|
|
59
65
|
execution_manager=execution_manager,
|
|
66
|
+
disconnected_callback=disconnected_callback,
|
|
67
|
+
error_callback=error_callback,
|
|
60
68
|
sim_model=sim_model,
|
|
61
69
|
sim_serial_number=sim_serial_number,
|
|
62
|
-
disconnected_callback=disconnected_callback,
|
|
63
70
|
)
|
|
64
71
|
|
|
65
72
|
|
|
@@ -77,6 +77,8 @@ class NozzleMap:
|
|
|
77
77
|
#: A map of all of the nozzles of an instrument
|
|
78
78
|
full_instrument_rows: Dict[str, List[str]]
|
|
79
79
|
#: A map of all the rows of an instrument
|
|
80
|
+
full_instrument_columns: Dict[str, List[str]]
|
|
81
|
+
#: A map of all the columns of an instrument
|
|
80
82
|
|
|
81
83
|
@classmethod
|
|
82
84
|
def determine_nozzle_configuration(
|
|
@@ -299,6 +301,7 @@ class NozzleMap:
|
|
|
299
301
|
rows=rows,
|
|
300
302
|
full_instrument_map_store=physical_nozzles,
|
|
301
303
|
full_instrument_rows=physical_rows,
|
|
304
|
+
full_instrument_columns=physical_columns,
|
|
302
305
|
columns=columns,
|
|
303
306
|
configuration=cls.determine_nozzle_configuration(
|
|
304
307
|
physical_rows, rows, physical_columns, columns
|
|
@@ -74,6 +74,7 @@ from .types import (
|
|
|
74
74
|
DoorStateNotification,
|
|
75
75
|
ErrorMessageNotification,
|
|
76
76
|
HardwareEvent,
|
|
77
|
+
AsynchronousModuleErrorNotification,
|
|
77
78
|
HardwareEventHandler,
|
|
78
79
|
HardwareAction,
|
|
79
80
|
HepaFanState,
|
|
@@ -367,6 +368,21 @@ class OT3API(
|
|
|
367
368
|
|
|
368
369
|
return futures
|
|
369
370
|
|
|
371
|
+
def _send_module_notification(self, event: HardwareEvent) -> None:
|
|
372
|
+
if not isinstance(
|
|
373
|
+
event,
|
|
374
|
+
AsynchronousModuleErrorNotification,
|
|
375
|
+
):
|
|
376
|
+
return
|
|
377
|
+
mod_log.info(
|
|
378
|
+
f"Forwarding module event {event.event} for {event.module_model} {event.module_serial} at {event.port}"
|
|
379
|
+
)
|
|
380
|
+
for cb in self._callbacks:
|
|
381
|
+
try:
|
|
382
|
+
cb(event)
|
|
383
|
+
except Exception:
|
|
384
|
+
mod_log.exception("Errored during module asynchronous callback")
|
|
385
|
+
|
|
370
386
|
def _reset_last_mount(self) -> None:
|
|
371
387
|
self._last_moved_mount = None
|
|
372
388
|
|
|
@@ -422,7 +438,9 @@ class OT3API(
|
|
|
422
438
|
|
|
423
439
|
await api_instance.set_status_bar_enabled(status_bar_enabled)
|
|
424
440
|
module_controls = await AttachedModulesControl.build(
|
|
425
|
-
api_instance,
|
|
441
|
+
api_instance,
|
|
442
|
+
board_revision=backend.board_revision,
|
|
443
|
+
event_callback=api_instance._send_module_notification,
|
|
426
444
|
)
|
|
427
445
|
backend.module_controls = module_controls
|
|
428
446
|
await backend.build_estop_detector()
|
|
@@ -484,7 +502,9 @@ class OT3API(
|
|
|
484
502
|
)
|
|
485
503
|
await api_instance.cache_instruments()
|
|
486
504
|
module_controls = await AttachedModulesControl.build(
|
|
487
|
-
api_instance,
|
|
505
|
+
api_instance,
|
|
506
|
+
board_revision=backend.board_revision,
|
|
507
|
+
event_callback=api_instance._send_module_notification,
|
|
488
508
|
)
|
|
489
509
|
backend.module_controls = module_controls
|
|
490
510
|
await backend.watch(api_instance.loop)
|
|
@@ -627,9 +647,10 @@ class OT3API(
|
|
|
627
647
|
self.is_simulator
|
|
628
648
|
), "Cannot build simulating module from non-simulating hardware control API"
|
|
629
649
|
|
|
630
|
-
return await self._backend.module_controls.
|
|
631
|
-
|
|
632
|
-
|
|
650
|
+
return await self._backend.module_controls.register_simulated_module(
|
|
651
|
+
simulated_usb_port=USBPort(
|
|
652
|
+
name="", port_number=1, port_group=PortGroup.LEFT
|
|
653
|
+
),
|
|
633
654
|
type=modules.ModuleType.from_model(model),
|
|
634
655
|
sim_model=model.value,
|
|
635
656
|
)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
"""Module Firmware update script."""
|
|
2
|
+
|
|
2
3
|
import argparse
|
|
3
4
|
import asyncio
|
|
4
5
|
from glob import glob
|
|
@@ -14,6 +15,7 @@ from opentrons.hardware_control import modules
|
|
|
14
15
|
from opentrons.hardware_control.modules.mod_abc import AbstractModule
|
|
15
16
|
from opentrons.hardware_control.modules.update import update_firmware
|
|
16
17
|
from opentrons.hardware_control.types import BoardRevision
|
|
18
|
+
from opentrons.hardware_control.execution_manager import ExecutionManager
|
|
17
19
|
|
|
18
20
|
|
|
19
21
|
# Constants for checking if module is back online
|
|
@@ -84,6 +86,9 @@ async def build_module(
|
|
|
84
86
|
port=port,
|
|
85
87
|
usb_port=mod.usb_port,
|
|
86
88
|
type=modules.MODULE_TYPE_BY_NAME[mod.name],
|
|
89
|
+
execution_manager=ExecutionManager(),
|
|
90
|
+
disconnected_callback=lambda *args: None,
|
|
91
|
+
error_callback=lambda *args: None,
|
|
87
92
|
simulating=False,
|
|
88
93
|
hw_control_loop=loop,
|
|
89
94
|
)
|