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
|
@@ -27,12 +27,15 @@ class ConnectionHandler:
|
|
|
27
27
|
try:
|
|
28
28
|
response = self._emulator.handle(line.decode().strip())
|
|
29
29
|
if response:
|
|
30
|
-
|
|
31
|
-
logger.debug("
|
|
32
|
-
writer.write(
|
|
30
|
+
response_bytes = response.encode() + self._emulator.get_terminator()
|
|
31
|
+
logger.debug(f"{emulator_name} Sending: {response_bytes!r}")
|
|
32
|
+
writer.write(response_bytes)
|
|
33
33
|
except Exception as e:
|
|
34
34
|
logger.exception("%s exception", emulator_name)
|
|
35
|
-
writer.write(
|
|
35
|
+
writer.write(
|
|
36
|
+
f"Error: {str(e)} ".encode() + self._emulator.get_terminator()
|
|
37
|
+
)
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
if self._emulator.get_autoack():
|
|
40
|
+
writer.write(self._emulator.get_ack())
|
|
38
41
|
await writer.drain()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The purpose is to provide a fake backend that responds to GCODE commands.
|
|
4
4
|
"""
|
|
5
|
+
|
|
5
6
|
import logging
|
|
6
7
|
from time import sleep
|
|
7
8
|
from typing import (
|
|
@@ -50,6 +51,7 @@ class HeaterShakerEmulator(AbstractEmulator):
|
|
|
50
51
|
GCODE.CLOSE_LABWARE_LATCH.value: self._close_labware_latch,
|
|
51
52
|
GCODE.GET_LABWARE_LATCH_STATE.value: self._get_labware_latch_state,
|
|
52
53
|
GCODE.DEACTIVATE_HEATER.value: self._deactivate_heater,
|
|
54
|
+
GCODE.GET_ERROR_STATE.value: self._get_error_state,
|
|
53
55
|
}
|
|
54
56
|
self.reset()
|
|
55
57
|
|
|
@@ -60,7 +62,6 @@ class HeaterShakerEmulator(AbstractEmulator):
|
|
|
60
62
|
return None if not joined else joined
|
|
61
63
|
|
|
62
64
|
def reset(self) -> None:
|
|
63
|
-
|
|
64
65
|
self._temperature = Temperature(
|
|
65
66
|
per_tick=self._settings.temperature.degrees_per_tick,
|
|
66
67
|
current=self._settings.temperature.starting,
|
|
@@ -145,6 +146,14 @@ class HeaterShakerEmulator(AbstractEmulator):
|
|
|
145
146
|
self._temperature.deactivate(TEMPERATURE_ROOM)
|
|
146
147
|
return "M106"
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
|
|
149
|
+
def _get_error_state(self, command: Command) -> str:
|
|
150
|
+
return f"M411 {HS_ACK}M411"
|
|
151
|
+
|
|
152
|
+
def get_terminator(self) -> bytes:
|
|
150
153
|
return b"\n"
|
|
154
|
+
|
|
155
|
+
def get_ack(self) -> bytes:
|
|
156
|
+
return HS_ACK.encode()
|
|
157
|
+
|
|
158
|
+
def get_autoack(self) -> bool:
|
|
159
|
+
return False
|
|
@@ -90,7 +90,7 @@ class Settings(BaseSettings):
|
|
|
90
90
|
)
|
|
91
91
|
thermocycler: ThermocyclerSettings = ThermocyclerSettings(
|
|
92
92
|
serial_number="thermocycler_emulator",
|
|
93
|
-
model="
|
|
93
|
+
model="thermocyclerModuleV2",
|
|
94
94
|
version="v1.1.0",
|
|
95
95
|
lid_temperature=TemperatureModelSettings(),
|
|
96
96
|
plate_temperature=TemperatureModelSettings(),
|
|
@@ -5,8 +5,15 @@ The purpose is to provide a fake backend that responds to GCODE commands.
|
|
|
5
5
|
|
|
6
6
|
import logging
|
|
7
7
|
from typing import Optional
|
|
8
|
-
from opentrons.drivers.thermocycler.driver import
|
|
8
|
+
from opentrons.drivers.thermocycler.driver import (
|
|
9
|
+
GCODE,
|
|
10
|
+
TC_GEN2_ACK,
|
|
11
|
+
TC_GEN2_SERIAL_ACK,
|
|
12
|
+
TC_ACK as TC_GEN1_ACK,
|
|
13
|
+
SERIAL_ACK as TC_GEN1_SERIAL_ACK,
|
|
14
|
+
)
|
|
9
15
|
from opentrons.drivers.types import ThermocyclerLidStatus
|
|
16
|
+
from opentrons.hardware_control.modules.types import ThermocyclerModuleModel
|
|
10
17
|
from opentrons.hardware_control.emulation.parser import Parser, Command
|
|
11
18
|
from opentrons.hardware_control.emulation.settings import ThermocyclerSettings
|
|
12
19
|
|
|
@@ -29,13 +36,31 @@ class ThermocyclerEmulator(AbstractEmulator):
|
|
|
29
36
|
def __init__(self, parser: Parser, settings: ThermocyclerSettings) -> None:
|
|
30
37
|
self._parser = parser
|
|
31
38
|
self._settings = settings
|
|
39
|
+
# I hate this. These modules do not return anything like this for their actual versions
|
|
40
|
+
# (gen2 returns "Opentrons-thermocycler-gen2" for instance) and this is not what any of
|
|
41
|
+
# the settings anywhere use.
|
|
42
|
+
self._model = (
|
|
43
|
+
ThermocyclerModuleModel.THERMOCYCLER_V1
|
|
44
|
+
if settings.model in ["thermocyclerModuleV1", "v1", "v01"]
|
|
45
|
+
else ThermocyclerModuleModel.THERMOCYCLER_V2
|
|
46
|
+
)
|
|
47
|
+
self._terminator = (
|
|
48
|
+
TC_GEN1_SERIAL_ACK
|
|
49
|
+
if self._model is ThermocyclerModuleModel.THERMOCYCLER_V1
|
|
50
|
+
else TC_GEN2_SERIAL_ACK
|
|
51
|
+
)
|
|
52
|
+
self._ack = (
|
|
53
|
+
TC_GEN1_ACK
|
|
54
|
+
if self._model is ThermocyclerModuleModel.THERMOCYCLER_V1
|
|
55
|
+
else TC_GEN2_ACK
|
|
56
|
+
)
|
|
32
57
|
self.reset()
|
|
33
58
|
|
|
34
59
|
def handle(self, line: str) -> Optional[str]:
|
|
35
60
|
"""Handle a line"""
|
|
36
61
|
results = (self._handle(c) for c in self._parser.parse(line))
|
|
37
|
-
joined = " ".join(r for r in results if r)
|
|
38
|
-
return
|
|
62
|
+
joined = " ".join(f"{r} {self._ack}" for r in results if r)
|
|
63
|
+
return self._ack if not joined else joined
|
|
39
64
|
|
|
40
65
|
def reset(self) -> None:
|
|
41
66
|
self._lid_temperature = Temperature(
|
|
@@ -50,6 +75,12 @@ class ThermocyclerEmulator(AbstractEmulator):
|
|
|
50
75
|
self.plate_volume = util.OptionalValue[float]()
|
|
51
76
|
self.plate_ramp_rate = util.OptionalValue[float]()
|
|
52
77
|
|
|
78
|
+
def _pref(self, command: Command) -> str:
|
|
79
|
+
if self._model is ThermocyclerModuleModel.THERMOCYCLER_V1:
|
|
80
|
+
return ""
|
|
81
|
+
else:
|
|
82
|
+
return f"{command.gcode} "
|
|
83
|
+
|
|
53
84
|
def _handle(self, command: Command) -> Optional[str]: # noqa: C901
|
|
54
85
|
"""
|
|
55
86
|
Handle a command.
|
|
@@ -62,7 +93,7 @@ class ThermocyclerEmulator(AbstractEmulator):
|
|
|
62
93
|
elif command.gcode == GCODE.CLOSE_LID:
|
|
63
94
|
self.lid_status = ThermocyclerLidStatus.CLOSED
|
|
64
95
|
elif command.gcode == GCODE.GET_LID_STATUS:
|
|
65
|
-
return f"Lid:{self.lid_status}"
|
|
96
|
+
return self._pref(command) + f"Lid:{self.lid_status}"
|
|
66
97
|
elif command.gcode == GCODE.SET_LID_TEMP:
|
|
67
98
|
temperature = command.params["S"]
|
|
68
99
|
assert isinstance(
|
|
@@ -76,7 +107,7 @@ class ThermocyclerEmulator(AbstractEmulator):
|
|
|
76
107
|
f"H:none Total_H:none"
|
|
77
108
|
)
|
|
78
109
|
self._lid_temperature.tick()
|
|
79
|
-
return res
|
|
110
|
+
return self._pref(command) + res
|
|
80
111
|
elif command.gcode == GCODE.EDIT_PID_PARAMS:
|
|
81
112
|
pass
|
|
82
113
|
elif command.gcode == GCODE.SET_PLATE_TEMP:
|
|
@@ -105,7 +136,7 @@ class ThermocyclerEmulator(AbstractEmulator):
|
|
|
105
136
|
f"Total_H:{plate_total_hold_time} "
|
|
106
137
|
)
|
|
107
138
|
self._plate_temperature.tick()
|
|
108
|
-
return res
|
|
139
|
+
return self._pref(command) + res
|
|
109
140
|
elif command.gcode == GCODE.SET_RAMP_RATE:
|
|
110
141
|
self.plate_ramp_rate.val = command.params["S"]
|
|
111
142
|
elif command.gcode == GCODE.DEACTIVATE_ALL:
|
|
@@ -116,13 +147,34 @@ class ThermocyclerEmulator(AbstractEmulator):
|
|
|
116
147
|
elif command.gcode == GCODE.DEACTIVATE_BLOCK:
|
|
117
148
|
self._plate_temperature.deactivate(temperature=util.TEMPERATURE_ROOM)
|
|
118
149
|
elif command.gcode == GCODE.DEVICE_INFO:
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
150
|
+
# the gen2 returns a completely different device info format than the
|
|
151
|
+
# gen1 which is pretty cool
|
|
152
|
+
if self._model == ThermocyclerModuleModel.THERMOCYCLER_V1:
|
|
153
|
+
return (
|
|
154
|
+
f"serial:{self._settings.serial_number} "
|
|
155
|
+
f"model:{self._settings.model} "
|
|
156
|
+
f"version:{self._settings.version}"
|
|
157
|
+
)
|
|
158
|
+
else:
|
|
159
|
+
return (
|
|
160
|
+
command.gcode
|
|
161
|
+
+ " "
|
|
162
|
+
+ (
|
|
163
|
+
f"FW:{self._settings.version} "
|
|
164
|
+
f"HW:{self._settings.model} "
|
|
165
|
+
f"SerialNo:{self._settings.serial_number}"
|
|
166
|
+
)
|
|
167
|
+
)
|
|
168
|
+
elif command.gcode == GCODE.GET_ERROR_STATE:
|
|
169
|
+
if self._model is ThermocyclerModuleModel.THERMOCYCLER_V2:
|
|
170
|
+
return self._pref(command) + self._ack + self._pref(command)
|
|
171
|
+
return self._pref(command)
|
|
172
|
+
|
|
173
|
+
def get_terminator(self) -> bytes:
|
|
174
|
+
return self._terminator.encode()
|
|
175
|
+
|
|
176
|
+
def get_ack(self) -> bytes:
|
|
177
|
+
return self._ack.encode()
|
|
125
178
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return b"\r\n"
|
|
179
|
+
def get_autoack(self) -> bool:
|
|
180
|
+
return False
|
|
@@ -2,9 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
import asyncio
|
|
3
3
|
import logging
|
|
4
4
|
import re
|
|
5
|
-
from typing import TYPE_CHECKING, List, Optional, Union
|
|
5
|
+
from typing import TYPE_CHECKING, List, Optional, Union, Callable
|
|
6
6
|
from glob import glob
|
|
7
7
|
|
|
8
|
+
from opentrons_shared_data.errors.exceptions import EnumeratedError
|
|
9
|
+
|
|
8
10
|
from opentrons.config import IS_ROBOT, IS_LINUX
|
|
9
11
|
from opentrons.drivers.rpi_drivers import types, interfaces, usb, usb_simulator
|
|
10
12
|
from opentrons.hardware_control.emulation.module_server.helpers import (
|
|
@@ -19,8 +21,16 @@ from opentrons.hardware_control.modules.module_calibration import (
|
|
|
19
21
|
from opentrons.hardware_control.modules.types import ModuleAtPort, ModuleType
|
|
20
22
|
from opentrons.hardware_control.modules import SimulatingModuleAtPort
|
|
21
23
|
|
|
24
|
+
|
|
22
25
|
from opentrons.types import Point
|
|
23
|
-
from .types import
|
|
26
|
+
from .types import (
|
|
27
|
+
AionotifyEvent,
|
|
28
|
+
BoardRevision,
|
|
29
|
+
OT3Mount,
|
|
30
|
+
StatusBarUpdateEvent,
|
|
31
|
+
HardwareEvent,
|
|
32
|
+
AsynchronousModuleErrorNotification,
|
|
33
|
+
)
|
|
24
34
|
from . import modules
|
|
25
35
|
|
|
26
36
|
if TYPE_CHECKING:
|
|
@@ -51,10 +61,21 @@ class AttachedModulesControl:
|
|
|
51
61
|
self,
|
|
52
62
|
api: Union["API", "OT3API"],
|
|
53
63
|
usb: interfaces.USBDriverInterface,
|
|
64
|
+
event_callback: Callable[[HardwareEvent], None],
|
|
54
65
|
) -> None:
|
|
55
66
|
self._available_modules: List[modules.AbstractModule] = []
|
|
56
67
|
self._api = api
|
|
57
68
|
self._usb = usb
|
|
69
|
+
self._event_callback = event_callback
|
|
70
|
+
if not IS_ROBOT and not api.is_simulator:
|
|
71
|
+
# Start task that registers emulated modules.
|
|
72
|
+
self._emulation_listen_task: asyncio.Task[
|
|
73
|
+
None
|
|
74
|
+
] | None = api.loop.create_task(
|
|
75
|
+
listen_module_connection(self.register_modules)
|
|
76
|
+
)
|
|
77
|
+
else:
|
|
78
|
+
self._emulation_listen_task = None
|
|
58
79
|
|
|
59
80
|
def subscribe_to_api_event(self, module: modules.AbstractModule) -> None:
|
|
60
81
|
self._api.add_status_bar_listener(module.event_listener)
|
|
@@ -64,22 +85,20 @@ class AttachedModulesControl:
|
|
|
64
85
|
cls,
|
|
65
86
|
api_instance: Union["API", "OT3API"],
|
|
66
87
|
board_revision: BoardRevision,
|
|
88
|
+
event_callback: Callable[[HardwareEvent], None],
|
|
67
89
|
) -> AttachedModulesControl:
|
|
68
90
|
usb_instance = (
|
|
69
91
|
usb.USBBus(board_revision)
|
|
70
92
|
if not api_instance.is_simulator and IS_ROBOT
|
|
71
93
|
else usb_simulator.USBBusSimulator()
|
|
72
94
|
)
|
|
73
|
-
mc_instance = cls(
|
|
95
|
+
mc_instance = cls(
|
|
96
|
+
api=api_instance, usb=usb_instance, event_callback=event_callback
|
|
97
|
+
)
|
|
74
98
|
|
|
75
99
|
if not api_instance.is_simulator:
|
|
76
100
|
# Do an initial scan of modules.
|
|
77
101
|
await mc_instance.register_modules(mc_instance.scan())
|
|
78
|
-
if not IS_ROBOT:
|
|
79
|
-
# Start task that registers emulated modules.
|
|
80
|
-
api_instance.loop.create_task(
|
|
81
|
-
listen_module_connection(mc_instance.register_modules)
|
|
82
|
-
)
|
|
83
102
|
|
|
84
103
|
return mc_instance
|
|
85
104
|
|
|
@@ -87,6 +106,37 @@ class AttachedModulesControl:
|
|
|
87
106
|
def available_modules(self) -> List[modules.AbstractModule]:
|
|
88
107
|
return self._available_modules
|
|
89
108
|
|
|
109
|
+
async def clean_up(self) -> None:
|
|
110
|
+
"""Clean up all registered modules and emulator scanning tasks (if any)."""
|
|
111
|
+
for module in self._available_modules:
|
|
112
|
+
await module.cleanup()
|
|
113
|
+
if self._emulation_listen_task is not None:
|
|
114
|
+
self._emulation_listen_task.cancel("cleanup")
|
|
115
|
+
try:
|
|
116
|
+
await self._emulation_listen_task
|
|
117
|
+
except asyncio.CancelledError:
|
|
118
|
+
pass
|
|
119
|
+
except Exception:
|
|
120
|
+
log.exception("Exception cleaning up emulation listen task")
|
|
121
|
+
finally:
|
|
122
|
+
self._emulation_listen_task = None
|
|
123
|
+
|
|
124
|
+
async def register_simulated_module(
|
|
125
|
+
self,
|
|
126
|
+
simulated_usb_port: types.USBPort,
|
|
127
|
+
type: modules.ModuleType,
|
|
128
|
+
sim_model: str,
|
|
129
|
+
) -> modules.AbstractModule:
|
|
130
|
+
"""Register a simulated module."""
|
|
131
|
+
module = await self.build_module(
|
|
132
|
+
"", simulated_usb_port, type, sim_model, sim_serial_number=None
|
|
133
|
+
)
|
|
134
|
+
self._available_modules.append(module)
|
|
135
|
+
self._available_modules = sorted(
|
|
136
|
+
self._available_modules, key=modules.AbstractModule.sort_key
|
|
137
|
+
)
|
|
138
|
+
return module
|
|
139
|
+
|
|
90
140
|
async def build_module(
|
|
91
141
|
self,
|
|
92
142
|
port: str,
|
|
@@ -105,6 +155,7 @@ class AttachedModulesControl:
|
|
|
105
155
|
sim_model=sim_model,
|
|
106
156
|
sim_serial_number=sim_serial_number,
|
|
107
157
|
disconnected_callback=self._disconnected_callback,
|
|
158
|
+
error_callback=self._async_error_callback,
|
|
108
159
|
)
|
|
109
160
|
last_event = StatusBarUpdateEvent(
|
|
110
161
|
self._api.get_status_bar_state(), self._api.get_status_bar_enabled()
|
|
@@ -121,6 +172,29 @@ class AttachedModulesControl:
|
|
|
121
172
|
self._api.loop,
|
|
122
173
|
)
|
|
123
174
|
|
|
175
|
+
def _async_error_callback(
|
|
176
|
+
self,
|
|
177
|
+
exc: Exception,
|
|
178
|
+
model: str,
|
|
179
|
+
port: str,
|
|
180
|
+
serial: str | None,
|
|
181
|
+
) -> None:
|
|
182
|
+
"""Used by the module to indicate it saw an error from its data poller."""
|
|
183
|
+
try:
|
|
184
|
+
self._api.loop.call_soon(
|
|
185
|
+
self._event_callback,
|
|
186
|
+
AsynchronousModuleErrorNotification(
|
|
187
|
+
exception=EnumeratedError.ensure(exc),
|
|
188
|
+
module_serial=serial,
|
|
189
|
+
module_model=modules.module_model_from_string(model),
|
|
190
|
+
port=port,
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
except Exception:
|
|
194
|
+
log.exception(
|
|
195
|
+
f"Async error callback for module {model} {serial} at {port} for exc {exc} failed"
|
|
196
|
+
)
|
|
197
|
+
|
|
124
198
|
async def unregister_modules(
|
|
125
199
|
self,
|
|
126
200
|
mods_at_ports: Union[
|
|
@@ -27,7 +27,9 @@ from .types import (
|
|
|
27
27
|
LiveData,
|
|
28
28
|
ModuleData,
|
|
29
29
|
ModuleDataValidator,
|
|
30
|
+
module_model_from_string,
|
|
30
31
|
)
|
|
32
|
+
|
|
31
33
|
from .errors import (
|
|
32
34
|
UpdateError,
|
|
33
35
|
AbsorbanceReaderDisconnectedError,
|
|
@@ -66,4 +68,5 @@ __all__ = [
|
|
|
66
68
|
"FlexStackerStatus",
|
|
67
69
|
"PlatformState",
|
|
68
70
|
"StackerAxisState",
|
|
71
|
+
"module_model_from_string",
|
|
69
72
|
]
|
|
@@ -21,6 +21,7 @@ from opentrons.hardware_control.poller import Poller, Reader
|
|
|
21
21
|
from opentrons.hardware_control.modules import mod_abc
|
|
22
22
|
from opentrons.hardware_control.modules.types import (
|
|
23
23
|
ModuleDisconnectedCallback,
|
|
24
|
+
ModuleErrorCallback,
|
|
24
25
|
ModuleType,
|
|
25
26
|
AbsorbanceReaderStatus,
|
|
26
27
|
LiveData,
|
|
@@ -104,12 +105,13 @@ class AbsorbanceReader(mod_abc.AbstractModule):
|
|
|
104
105
|
port: str,
|
|
105
106
|
usb_port: USBPort,
|
|
106
107
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
107
|
-
execution_manager:
|
|
108
|
+
execution_manager: ExecutionManager,
|
|
109
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
110
|
+
error_callback: ModuleErrorCallback,
|
|
108
111
|
poll_interval_seconds: Optional[float] = None,
|
|
109
112
|
simulating: bool = False,
|
|
110
113
|
sim_model: Optional[str] = None,
|
|
111
114
|
sim_serial_number: Optional[str] = None,
|
|
112
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
113
115
|
) -> "AbsorbanceReader":
|
|
114
116
|
"""
|
|
115
117
|
Build and connect to an AbsorbanceReader
|
|
@@ -152,6 +154,7 @@ class AbsorbanceReader(mod_abc.AbstractModule):
|
|
|
152
154
|
hw_control_loop=hw_control_loop,
|
|
153
155
|
execution_manager=execution_manager,
|
|
154
156
|
disconnected_callback=disconnected_callback,
|
|
157
|
+
error_callback=error_callback,
|
|
155
158
|
)
|
|
156
159
|
|
|
157
160
|
try:
|
|
@@ -170,8 +173,9 @@ class AbsorbanceReader(mod_abc.AbstractModule):
|
|
|
170
173
|
poller: Poller,
|
|
171
174
|
device_info: Mapping[str, str],
|
|
172
175
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
173
|
-
execution_manager:
|
|
174
|
-
disconnected_callback: ModuleDisconnectedCallback
|
|
176
|
+
execution_manager: ExecutionManager,
|
|
177
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
178
|
+
error_callback: ModuleErrorCallback,
|
|
175
179
|
) -> None:
|
|
176
180
|
"""
|
|
177
181
|
Constructor
|
|
@@ -193,6 +197,7 @@ class AbsorbanceReader(mod_abc.AbstractModule):
|
|
|
193
197
|
hw_control_loop=hw_control_loop,
|
|
194
198
|
execution_manager=execution_manager,
|
|
195
199
|
disconnected_callback=disconnected_callback,
|
|
200
|
+
error_callback=error_callback,
|
|
196
201
|
)
|
|
197
202
|
self._device_info = device_info
|
|
198
203
|
self._reader = reader
|
|
@@ -371,3 +376,5 @@ class AbsorbanceReader(mod_abc.AbstractModule):
|
|
|
371
376
|
self._error = str(error)
|
|
372
377
|
if isinstance(error, AbsorbanceReaderDisconnectedError):
|
|
373
378
|
self.disconnected_callback()
|
|
379
|
+
else:
|
|
380
|
+
self.error_callback(error)
|
|
@@ -44,6 +44,7 @@ from opentrons.hardware_control.modules.types import (
|
|
|
44
44
|
FlexStackerStatus,
|
|
45
45
|
HopperDoorState,
|
|
46
46
|
LatchState,
|
|
47
|
+
ModuleErrorCallback,
|
|
47
48
|
ModuleDisconnectedCallback,
|
|
48
49
|
ModuleType,
|
|
49
50
|
PlatformState,
|
|
@@ -215,12 +216,13 @@ class FlexStacker(mod_abc.AbstractModule):
|
|
|
215
216
|
port: str,
|
|
216
217
|
usb_port: USBPort,
|
|
217
218
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
218
|
-
execution_manager:
|
|
219
|
-
|
|
219
|
+
execution_manager: ExecutionManager,
|
|
220
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
221
|
+
error_callback: ModuleErrorCallback,
|
|
222
|
+
poll_interval_seconds: float | None = None,
|
|
220
223
|
simulating: bool = False,
|
|
221
224
|
sim_model: Optional[str] = None,
|
|
222
225
|
sim_serial_number: Optional[str] = None,
|
|
223
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
224
226
|
) -> "FlexStacker":
|
|
225
227
|
"""
|
|
226
228
|
Build a FlexStacker
|
|
@@ -259,11 +261,9 @@ class FlexStacker(mod_abc.AbstractModule):
|
|
|
259
261
|
hw_control_loop=hw_control_loop,
|
|
260
262
|
execution_manager=execution_manager,
|
|
261
263
|
disconnected_callback=disconnected_callback,
|
|
264
|
+
error_callback=error_callback,
|
|
262
265
|
)
|
|
263
266
|
|
|
264
|
-
# Set initialized callback
|
|
265
|
-
reader.set_initialized_callback(module._initialized_callback)
|
|
266
|
-
|
|
267
267
|
# Enable stallguard
|
|
268
268
|
for axis, config in STALLGUARD_CONFIG.items():
|
|
269
269
|
await driver.set_stallguard_threshold(
|
|
@@ -286,8 +286,9 @@ class FlexStacker(mod_abc.AbstractModule):
|
|
|
286
286
|
poller: Poller,
|
|
287
287
|
device_info: Mapping[str, str],
|
|
288
288
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
289
|
-
execution_manager:
|
|
290
|
-
disconnected_callback: ModuleDisconnectedCallback
|
|
289
|
+
execution_manager: ExecutionManager,
|
|
290
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
291
|
+
error_callback: ModuleErrorCallback,
|
|
291
292
|
):
|
|
292
293
|
super().__init__(
|
|
293
294
|
port=port,
|
|
@@ -295,6 +296,7 @@ class FlexStacker(mod_abc.AbstractModule):
|
|
|
295
296
|
hw_control_loop=hw_control_loop,
|
|
296
297
|
execution_manager=execution_manager,
|
|
297
298
|
disconnected_callback=disconnected_callback,
|
|
299
|
+
error_callback=error_callback,
|
|
298
300
|
)
|
|
299
301
|
self._device_info = device_info
|
|
300
302
|
self._driver = driver
|
|
@@ -304,12 +306,20 @@ class FlexStacker(mod_abc.AbstractModule):
|
|
|
304
306
|
self._stacker_status = FlexStackerStatus.IDLE
|
|
305
307
|
self._last_status_bar_event: Optional[StatusBarUpdateEvent] = None
|
|
306
308
|
self._should_identify = False
|
|
309
|
+
# Set initialized callback
|
|
310
|
+
self._unsubscribe_init = reader.set_initialized_callback(
|
|
311
|
+
self._initialized_callback
|
|
312
|
+
)
|
|
313
|
+
self._unsubscribe_error = reader.set_error_callback(self._async_error_callback)
|
|
307
314
|
|
|
308
315
|
async def _initialized_callback(self) -> None:
|
|
309
316
|
"""Called by the reader once the module is initialized."""
|
|
310
317
|
if self._last_status_bar_event:
|
|
311
318
|
await self._handle_status_bar_event(self._last_status_bar_event)
|
|
312
319
|
|
|
320
|
+
def _async_error_callback(self, exception: Exception) -> None:
|
|
321
|
+
self.error_callback(exception)
|
|
322
|
+
|
|
313
323
|
async def cleanup(self) -> None:
|
|
314
324
|
"""Stop the poller task"""
|
|
315
325
|
await self._poller.stop()
|
|
@@ -864,10 +874,27 @@ class FlexStackerReader(Reader):
|
|
|
864
874
|
self.installation_detected = False
|
|
865
875
|
self._refresh_state = False
|
|
866
876
|
self._initialized_callback: Optional[Callable[[], Awaitable[None]]] = None
|
|
877
|
+
self._error_callback: Optional[Callable[[Exception], None]] = None
|
|
867
878
|
|
|
868
|
-
def set_initialized_callback(
|
|
879
|
+
def set_initialized_callback(
|
|
880
|
+
self, callback: Callable[[], Awaitable[None]]
|
|
881
|
+
) -> Callable[[], None]:
|
|
869
882
|
"""Sets the callback used when done initializing the module."""
|
|
870
883
|
self._initialized_callback = callback
|
|
884
|
+
return self._remove_init_callback
|
|
885
|
+
|
|
886
|
+
def _remove_init_callback(self) -> None:
|
|
887
|
+
self._initialized_callback = None
|
|
888
|
+
|
|
889
|
+
def set_error_callback(
|
|
890
|
+
self, error_callback: Callable[[Exception], None]
|
|
891
|
+
) -> Callable[[], None]:
|
|
892
|
+
"""Register a handler for asynchronous hardware errors."""
|
|
893
|
+
self._error_callback = error_callback
|
|
894
|
+
return self._remove_error_callback
|
|
895
|
+
|
|
896
|
+
def _remove_error_callback(self) -> None:
|
|
897
|
+
self._error_callback = None
|
|
871
898
|
|
|
872
899
|
async def read(self) -> None:
|
|
873
900
|
await self.get_door_closed()
|
|
@@ -942,6 +969,8 @@ class FlexStackerReader(Reader):
|
|
|
942
969
|
if exception is None:
|
|
943
970
|
self.error = None
|
|
944
971
|
else:
|
|
972
|
+
if self._error_callback:
|
|
973
|
+
self._error_callback(exception)
|
|
945
974
|
try:
|
|
946
975
|
self.error = str(exception.args[0])
|
|
947
976
|
except Exception:
|
|
@@ -2,19 +2,21 @@ 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
|
|
9
9
|
from opentrons.drivers.heater_shaker.driver import HeaterShakerDriver
|
|
10
10
|
from opentrons.drivers.heater_shaker.abstract import AbstractHeaterShakerDriver
|
|
11
11
|
from opentrons.drivers.heater_shaker.simulator import SimulatingDriver
|
|
12
|
+
from opentrons.drivers.asyncio.communication.errors import UnhandledGcode
|
|
12
13
|
from opentrons.drivers.types import Temperature, RPM, HeaterShakerLabwareLatchStatus
|
|
13
14
|
from opentrons.hardware_control.execution_manager import ExecutionManager
|
|
14
15
|
from opentrons.hardware_control.poller import Reader, Poller
|
|
15
16
|
from opentrons.hardware_control.modules import mod_abc, update
|
|
16
17
|
from opentrons.hardware_control.modules.types import (
|
|
17
18
|
ModuleDisconnectedCallback,
|
|
19
|
+
ModuleErrorCallback,
|
|
18
20
|
ModuleType,
|
|
19
21
|
TemperatureStatus,
|
|
20
22
|
SpeedStatus,
|
|
@@ -47,12 +49,13 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
47
49
|
port: str,
|
|
48
50
|
usb_port: USBPort,
|
|
49
51
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
50
|
-
execution_manager:
|
|
52
|
+
execution_manager: ExecutionManager,
|
|
53
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
54
|
+
error_callback: ModuleErrorCallback,
|
|
51
55
|
poll_interval_seconds: Optional[float] = None,
|
|
52
56
|
simulating: bool = False,
|
|
53
57
|
sim_model: Optional[str] = None,
|
|
54
58
|
sim_serial_number: Optional[str] = None,
|
|
55
|
-
disconnected_callback: ModuleDisconnectedCallback = None,
|
|
56
59
|
) -> "HeaterShaker":
|
|
57
60
|
"""
|
|
58
61
|
Build a HeaterShaker
|
|
@@ -67,6 +70,7 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
67
70
|
loop: Loop
|
|
68
71
|
sim_model: The model name used by simulator
|
|
69
72
|
disconnected_callback: Callback to inform the module controller that the device was disconnected
|
|
73
|
+
error_callback: Callback to inform the module controller of an asynchronous error
|
|
70
74
|
|
|
71
75
|
Returns:
|
|
72
76
|
HeaterShaker instance
|
|
@@ -91,6 +95,7 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
91
95
|
hw_control_loop=hw_control_loop,
|
|
92
96
|
execution_manager=execution_manager,
|
|
93
97
|
disconnected_callback=disconnected_callback,
|
|
98
|
+
error_callback=error_callback,
|
|
94
99
|
)
|
|
95
100
|
|
|
96
101
|
try:
|
|
@@ -109,8 +114,9 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
109
114
|
poller: Poller,
|
|
110
115
|
device_info: Mapping[str, str],
|
|
111
116
|
hw_control_loop: asyncio.AbstractEventLoop,
|
|
112
|
-
execution_manager:
|
|
113
|
-
disconnected_callback: ModuleDisconnectedCallback
|
|
117
|
+
execution_manager: ExecutionManager,
|
|
118
|
+
disconnected_callback: ModuleDisconnectedCallback,
|
|
119
|
+
error_callback: ModuleErrorCallback,
|
|
114
120
|
):
|
|
115
121
|
super().__init__(
|
|
116
122
|
port=port,
|
|
@@ -118,14 +124,22 @@ class HeaterShaker(mod_abc.AbstractModule):
|
|
|
118
124
|
hw_control_loop=hw_control_loop,
|
|
119
125
|
execution_manager=execution_manager,
|
|
120
126
|
disconnected_callback=disconnected_callback,
|
|
127
|
+
error_callback=error_callback,
|
|
121
128
|
)
|
|
122
129
|
self._device_info = device_info
|
|
123
130
|
self._driver = driver
|
|
124
131
|
self._reader = reader
|
|
125
132
|
self._poller = poller
|
|
133
|
+
self._unsubscribe_reader = self._reader.register_error_handler(
|
|
134
|
+
self._handle_error
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
def _handle_error(self, error: Exception) -> None:
|
|
138
|
+
self.error_callback(error)
|
|
126
139
|
|
|
127
140
|
async def cleanup(self) -> None:
|
|
128
141
|
"""Stop the poller task"""
|
|
142
|
+
self._unsubscribe_reader()
|
|
129
143
|
await self._poller.stop()
|
|
130
144
|
await self._driver.disconnect()
|
|
131
145
|
|
|
@@ -397,11 +411,22 @@ class HeaterShakerReader(Reader):
|
|
|
397
411
|
self.labware_latch = HeaterShakerLabwareLatchStatus.IDLE_UNKNOWN
|
|
398
412
|
self.error: Optional[str] = None
|
|
399
413
|
self._driver = driver
|
|
414
|
+
self._handle_error: Callable[[Exception], None] | None = None
|
|
415
|
+
|
|
416
|
+
def register_error_handler(
|
|
417
|
+
self, handle_error: Callable[[Exception], None]
|
|
418
|
+
) -> Callable[[], None]:
|
|
419
|
+
self._handle_error = handle_error
|
|
420
|
+
return self._unsubscribe_error_handler
|
|
421
|
+
|
|
422
|
+
def _unsubscribe_error_handler(self) -> None:
|
|
423
|
+
self._handle_error = None
|
|
400
424
|
|
|
401
425
|
async def read(self) -> None:
|
|
402
426
|
await self.read_temperature()
|
|
403
427
|
await self.read_rpm()
|
|
404
428
|
await self.read_labware_latch()
|
|
429
|
+
await self._read_errors()
|
|
405
430
|
self._set_error(None)
|
|
406
431
|
|
|
407
432
|
def on_error(self, exception: Exception) -> None:
|
|
@@ -420,7 +445,19 @@ class HeaterShakerReader(Reader):
|
|
|
420
445
|
if exception is None:
|
|
421
446
|
self.error = None
|
|
422
447
|
else:
|
|
448
|
+
if self._handle_error:
|
|
449
|
+
self._handle_error(exception)
|
|
423
450
|
try:
|
|
424
451
|
self.error = str(exception.args[0])
|
|
425
452
|
except Exception:
|
|
426
453
|
self.error = repr(exception)
|
|
454
|
+
|
|
455
|
+
async def _read_errors(self) -> None:
|
|
456
|
+
try:
|
|
457
|
+
await self._driver.get_error_state()
|
|
458
|
+
except UnhandledGcode:
|
|
459
|
+
# This device's firmware cannot accept this command, because it
|
|
460
|
+
# hasn't been updated or because it's a gen1. Ignore the result.
|
|
461
|
+
pass
|
|
462
|
+
# If the error is one we should let pass, raise it so the top level
|
|
463
|
+
# error handler can take it.
|