opentrons 8.7.0a5__py3-none-any.whl → 8.7.0a7__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of opentrons might be problematic. Click here for more details.
- opentrons/_version.py +2 -2
- opentrons/drivers/asyncio/communication/serial_connection.py +129 -52
- 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/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/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 +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 +42 -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 +68 -11
- 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/poller.py +22 -8
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +31 -2
- opentrons/legacy_commands/module_commands.py +23 -0
- opentrons/legacy_commands/protocol_commands.py +20 -0
- opentrons/legacy_commands/types.py +80 -0
- 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/_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/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +75 -8
- opentrons/protocol_api/core/engine/protocol.py +18 -1
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/legacy/legacy_module_core.py +24 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +11 -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_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +37 -4
- opentrons/protocol_api/core/protocol.py +11 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/labware.py +5 -0
- opentrons/protocol_api/module_contexts.py +117 -11
- 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 +42 -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 +49 -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/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/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/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 +40 -6
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +29 -5
- 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/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 +75 -34
- 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 +14 -11
- opentrons/protocol_engine/state/geometry.py +213 -374
- 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 +21 -8
- 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.0a5.dist-info → opentrons-8.7.0a7.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/RECORD +143 -127
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a5.dist-info → opentrons-8.7.0a7.dist-info}/licenses/LICENSE +0 -0
|
@@ -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
|
|
@@ -21,6 +22,7 @@ from opentrons.drivers.thermocycler import (
|
|
|
21
22
|
ThermocyclerDriverV2,
|
|
22
23
|
ThermocyclerDriverFactory,
|
|
23
24
|
)
|
|
25
|
+
from opentrons.drivers.asyncio.communication.errors import UnhandledGcode
|
|
24
26
|
|
|
25
27
|
|
|
26
28
|
log = logging.getLogger(__name__)
|
|
@@ -36,6 +38,8 @@ DFU_PID = "df11"
|
|
|
36
38
|
_TC_PLATE_LIFT_OPEN_DEGREES = 20
|
|
37
39
|
_TC_PLATE_LIFT_RETURN_DEGREES = 23
|
|
38
40
|
|
|
41
|
+
_TC_RAMP_RATE_ADDED_VERSION = (1, 0, 8) # v1.0.8
|
|
42
|
+
|
|
39
43
|
|
|
40
44
|
class ThermocyclerError(Exception):
|
|
41
45
|
pass
|
|
@@ -62,12 +66,13 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
62
66
|
port: str,
|
|
63
67
|
usb_port: USBPort,
|
|
64
68
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
65
|
-
execution_manager:
|
|
69
|
+
execution_manager: ExecutionManager,
|
|
70
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
71
|
+
error_callback: ModuleErrorCallback,
|
|
66
72
|
poll_interval_seconds: Optional[float] = None,
|
|
67
73
|
simulating: bool = False,
|
|
68
74
|
sim_model: Optional[str] = None,
|
|
69
75
|
sim_serial_number: Optional[str] = None,
|
|
70
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
71
76
|
) -> "Thermocycler":
|
|
72
77
|
"""
|
|
73
78
|
Build and connect to a Thermocycler
|
|
@@ -108,6 +113,7 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
108
113
|
hw_control_loop=hw_control_loop,
|
|
109
114
|
execution_manager=execution_manager,
|
|
110
115
|
disconnected_callback=disconnected_callback,
|
|
116
|
+
error_callback=error_callback,
|
|
111
117
|
)
|
|
112
118
|
|
|
113
119
|
try:
|
|
@@ -126,8 +132,9 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
126
132
|
poller: Poller,
|
|
127
133
|
device_info: Dict[str, str],
|
|
128
134
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
129
|
-
execution_manager:
|
|
130
|
-
disconnected_callback: ModuleDisconnectedCallback
|
|
135
|
+
execution_manager: ExecutionManager,
|
|
136
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
137
|
+
error_callback: ModuleErrorCallback,
|
|
131
138
|
) -> None:
|
|
132
139
|
"""
|
|
133
140
|
Constructor
|
|
@@ -150,6 +157,7 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
150
157
|
hw_control_loop=hw_control_loop,
|
|
151
158
|
execution_manager=execution_manager,
|
|
152
159
|
disconnected_callback=disconnected_callback,
|
|
160
|
+
error_callback=error_callback,
|
|
153
161
|
)
|
|
154
162
|
self._device_info = device_info
|
|
155
163
|
self._reader = reader
|
|
@@ -159,10 +167,13 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
159
167
|
self._total_step_count: Optional[int] = None
|
|
160
168
|
self._current_step_index: Optional[int] = None
|
|
161
169
|
self._error: Optional[str] = None
|
|
162
|
-
self._reader.register_error_handler(
|
|
170
|
+
self._unsubscribe_reader = self._reader.register_error_handler(
|
|
171
|
+
self._enter_error_state
|
|
172
|
+
)
|
|
163
173
|
|
|
164
174
|
async def cleanup(self) -> None:
|
|
165
175
|
"""Stop the poller task."""
|
|
176
|
+
self._unsubscribe_reader()
|
|
166
177
|
await self._poller.stop()
|
|
167
178
|
await self._driver.disconnect()
|
|
168
179
|
|
|
@@ -265,6 +276,19 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
265
276
|
await self.open()
|
|
266
277
|
await self._wait_for_lid_status(ThermocyclerLidStatus.OPEN)
|
|
267
278
|
|
|
279
|
+
def can_use_ramp_rate(self) -> bool:
|
|
280
|
+
version_string = self._device_info.get("version", "v")
|
|
281
|
+
if version_string.startswith("v"):
|
|
282
|
+
version_string = version_string[1:]
|
|
283
|
+
try:
|
|
284
|
+
version_tuple = tuple(int(c) for c in version_string.split("."))
|
|
285
|
+
return version_tuple >= _TC_RAMP_RATE_ADDED_VERSION
|
|
286
|
+
except (ValueError, IndexError):
|
|
287
|
+
log.error(
|
|
288
|
+
f"Invalid version from device: {self._device_info.get('version', '')}"
|
|
289
|
+
)
|
|
290
|
+
return False
|
|
291
|
+
|
|
268
292
|
async def set_temperature(
|
|
269
293
|
self,
|
|
270
294
|
temperature: float,
|
|
@@ -290,6 +314,11 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
290
314
|
|
|
291
315
|
Returns: None
|
|
292
316
|
"""
|
|
317
|
+
if ramp_rate and not self.can_use_ramp_rate():
|
|
318
|
+
raise ThermocyclerError(
|
|
319
|
+
"Ramp rate is not supported by this thermocycler's firmware version, please update."
|
|
320
|
+
)
|
|
321
|
+
|
|
293
322
|
await self.wait_for_is_running()
|
|
294
323
|
await self._set_temperature_no_pause(
|
|
295
324
|
temperature=temperature,
|
|
@@ -312,11 +341,13 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
312
341
|
total_seconds = seconds + (minutes * 60)
|
|
313
342
|
hold_time = total_seconds if total_seconds > 0 else 0
|
|
314
343
|
|
|
315
|
-
if ramp_rate
|
|
316
|
-
|
|
344
|
+
if ramp_rate and not self.can_use_ramp_rate():
|
|
345
|
+
raise ThermocyclerError(
|
|
346
|
+
"Ramp rate is not supported by this thermocycler's firmware version, please update."
|
|
347
|
+
)
|
|
317
348
|
|
|
318
349
|
await self._driver.set_plate_temperature(
|
|
319
|
-
temp=temperature, hold_time=hold_time, volume=volume
|
|
350
|
+
temp=temperature, hold_time=hold_time, volume=volume, ramp_rate=ramp_rate
|
|
320
351
|
)
|
|
321
352
|
|
|
322
353
|
task = self._loop.create_task(self._wait_for_block_target())
|
|
@@ -419,6 +450,7 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
419
450
|
celsius: float,
|
|
420
451
|
hold_time_seconds: Optional[float] = None,
|
|
421
452
|
volume: Optional[float] = None,
|
|
453
|
+
ramp_rate: Optional[float] = None,
|
|
422
454
|
) -> None:
|
|
423
455
|
"""Set the Thermocycler's target block temperature.
|
|
424
456
|
|
|
@@ -428,10 +460,17 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
428
460
|
celsius: The target block temperature, in degrees celsius.
|
|
429
461
|
"""
|
|
430
462
|
await self.wait_for_is_running()
|
|
463
|
+
|
|
464
|
+
if ramp_rate and not self.can_use_ramp_rate():
|
|
465
|
+
raise ThermocyclerError(
|
|
466
|
+
"Ramp rate is not supported by this thermocycler's firmware version, please update."
|
|
467
|
+
)
|
|
468
|
+
|
|
431
469
|
await self._driver.set_plate_temperature(
|
|
432
470
|
temp=celsius,
|
|
433
471
|
hold_time=hold_time_seconds,
|
|
434
472
|
volume=volume,
|
|
473
|
+
ramp_rate=ramp_rate,
|
|
435
474
|
)
|
|
436
475
|
await self._reader.read_block_temperature()
|
|
437
476
|
|
|
@@ -596,11 +635,12 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
596
635
|
temperature = step.get("temperature")
|
|
597
636
|
hold_time_minutes = step.get("hold_time_minutes", None)
|
|
598
637
|
hold_time_seconds = step.get("hold_time_seconds", None)
|
|
638
|
+
ramp_rate = step.get("ramp_rate", None)
|
|
599
639
|
await self._set_temperature_no_pause(
|
|
600
640
|
temperature=temperature, # type: ignore
|
|
601
641
|
hold_time_minutes=hold_time_minutes,
|
|
602
642
|
hold_time_seconds=hold_time_seconds,
|
|
603
|
-
ramp_rate=
|
|
643
|
+
ramp_rate=ramp_rate,
|
|
604
644
|
volume=volume,
|
|
605
645
|
)
|
|
606
646
|
|
|
@@ -672,7 +712,7 @@ class Thermocycler(mod_abc.AbstractModule):
|
|
|
672
712
|
f"https://support.opentrons.com/en/articles/3469797-thermocycler-module"
|
|
673
713
|
f" for troubleshooting."
|
|
674
714
|
)
|
|
675
|
-
|
|
715
|
+
self.error_callback(error)
|
|
676
716
|
|
|
677
717
|
|
|
678
718
|
class ThermocyclerReader(Reader):
|
|
@@ -710,14 +750,31 @@ 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."""
|
|
718
764
|
await self.read_lid_status()
|
|
719
765
|
await self.read_lid_temperature()
|
|
720
766
|
await self.read_block_temperature()
|
|
767
|
+
await self._read_errors()
|
|
768
|
+
|
|
769
|
+
async def _read_errors(self) -> None:
|
|
770
|
+
try:
|
|
771
|
+
await self._driver.get_error_state()
|
|
772
|
+
except UnhandledGcode:
|
|
773
|
+
# This device's firmware cannot accept this command, because it
|
|
774
|
+
# hasn't been updated or because it's a gen1. Ignore the result.
|
|
775
|
+
pass
|
|
776
|
+
# If the error is one we should let pass, raise it so the top level
|
|
777
|
+
# error handler can take it.
|
|
721
778
|
|
|
722
779
|
async def read_lid_status(self) -> None:
|
|
723
780
|
self.lid_status = await self._driver.get_lid_status()
|
|
@@ -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
|
)
|
|
@@ -5,6 +5,7 @@ from abc import ABC, abstractmethod
|
|
|
5
5
|
from typing import AsyncGenerator, List, Optional
|
|
6
6
|
from opentrons.hardware_control.modules.errors import AbsorbanceReaderDisconnectedError
|
|
7
7
|
from opentrons_shared_data.errors.exceptions import ModuleCommunicationError
|
|
8
|
+
from opentrons.drivers.asyncio.communication.errors import SerialException
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
log = logging.getLogger(__name__)
|
|
@@ -88,6 +89,18 @@ class Poller:
|
|
|
88
89
|
except asyncio.InvalidStateError:
|
|
89
90
|
log.warning("Poller waiter was already cancelled")
|
|
90
91
|
|
|
92
|
+
def _error_callback(self, exc: Exception) -> None:
|
|
93
|
+
try:
|
|
94
|
+
self._reader.on_error(exc)
|
|
95
|
+
except Exception:
|
|
96
|
+
log.exception("Exception in reader callback")
|
|
97
|
+
|
|
98
|
+
def _complete_all(
|
|
99
|
+
self, exc: Exception | None, previous: List["asyncio.Future[None]"]
|
|
100
|
+
) -> None:
|
|
101
|
+
for waiter in previous:
|
|
102
|
+
Poller._set_waiter_complete(waiter, exc)
|
|
103
|
+
|
|
91
104
|
async def _poll_once(self) -> None:
|
|
92
105
|
"""Trigger a single read, notifying listeners of success or error."""
|
|
93
106
|
previous_waiters = self._poll_waiters
|
|
@@ -99,14 +112,15 @@ class Poller:
|
|
|
99
112
|
except asyncio.CancelledError:
|
|
100
113
|
raise
|
|
101
114
|
except AbsorbanceReaderDisconnectedError as e:
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
115
|
+
self._error_callback(e)
|
|
116
|
+
self._complete_all(e, previous_waiters)
|
|
117
|
+
except SerialException as se:
|
|
118
|
+
log.error(f"Polling gcode error: {se}")
|
|
119
|
+
self._error_callback(se)
|
|
120
|
+
self._complete_all(se, previous_waiters)
|
|
105
121
|
except Exception as e:
|
|
106
122
|
log.exception("Polling exception")
|
|
107
|
-
self.
|
|
108
|
-
|
|
109
|
-
Poller._set_waiter_complete(waiter, e)
|
|
123
|
+
self._error_callback(e)
|
|
124
|
+
self._complete_all(e, previous_waiters)
|
|
110
125
|
else:
|
|
111
|
-
|
|
112
|
-
Poller._set_waiter_complete(waiter)
|
|
126
|
+
self._complete_all(None, previous_waiters)
|
|
@@ -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
|
)
|