opentrons 8.7.0a9__py3-none-any.whl → 8.8.0a8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- opentrons/_version.py +2 -2
- opentrons/cli/analyze.py +4 -1
- opentrons/config/__init__.py +7 -0
- opentrons/drivers/asyncio/communication/serial_connection.py +126 -49
- opentrons/drivers/heater_shaker/abstract.py +5 -0
- opentrons/drivers/heater_shaker/driver.py +10 -0
- opentrons/drivers/heater_shaker/simulator.py +4 -0
- opentrons/drivers/thermocycler/abstract.py +6 -0
- opentrons/drivers/thermocycler/driver.py +61 -10
- opentrons/drivers/thermocycler/simulator.py +6 -0
- opentrons/drivers/vacuum_module/__init__.py +5 -0
- opentrons/drivers/vacuum_module/abstract.py +93 -0
- opentrons/drivers/vacuum_module/driver.py +208 -0
- opentrons/drivers/vacuum_module/errors.py +39 -0
- opentrons/drivers/vacuum_module/simulator.py +85 -0
- opentrons/drivers/vacuum_module/types.py +79 -0
- opentrons/execute.py +3 -0
- opentrons/hardware_control/api.py +24 -5
- opentrons/hardware_control/backends/controller.py +8 -2
- opentrons/hardware_control/backends/flex_protocol.py +1 -0
- opentrons/hardware_control/backends/ot3controller.py +35 -2
- opentrons/hardware_control/backends/ot3simulator.py +3 -1
- opentrons/hardware_control/backends/ot3utils.py +37 -0
- opentrons/hardware_control/backends/simulator.py +2 -1
- opentrons/hardware_control/backends/subsystem_manager.py +5 -2
- opentrons/hardware_control/emulation/abstract_emulator.py +6 -4
- opentrons/hardware_control/emulation/connection_handler.py +8 -5
- opentrons/hardware_control/emulation/heater_shaker.py +12 -3
- opentrons/hardware_control/emulation/settings.py +1 -1
- opentrons/hardware_control/emulation/thermocycler.py +67 -15
- opentrons/hardware_control/module_control.py +105 -10
- opentrons/hardware_control/modules/__init__.py +3 -0
- opentrons/hardware_control/modules/absorbance_reader.py +11 -4
- opentrons/hardware_control/modules/flex_stacker.py +38 -9
- opentrons/hardware_control/modules/heater_shaker.py +42 -5
- opentrons/hardware_control/modules/magdeck.py +8 -4
- opentrons/hardware_control/modules/mod_abc.py +14 -6
- opentrons/hardware_control/modules/tempdeck.py +25 -5
- opentrons/hardware_control/modules/thermocycler.py +68 -11
- opentrons/hardware_control/modules/types.py +20 -1
- opentrons/hardware_control/modules/utils.py +11 -4
- opentrons/hardware_control/motion_utilities.py +6 -6
- opentrons/hardware_control/nozzle_manager.py +3 -0
- opentrons/hardware_control/ot3api.py +92 -17
- opentrons/hardware_control/poller.py +22 -8
- opentrons/hardware_control/protocols/liquid_handler.py +12 -4
- opentrons/hardware_control/scripts/update_module_fw.py +5 -0
- opentrons/hardware_control/types.py +43 -2
- opentrons/legacy_commands/commands.py +58 -5
- opentrons/legacy_commands/module_commands.py +52 -0
- opentrons/legacy_commands/protocol_commands.py +53 -1
- opentrons/legacy_commands/types.py +155 -1
- opentrons/motion_planning/deck_conflict.py +17 -12
- opentrons/motion_planning/waypoints.py +15 -29
- opentrons/protocol_api/__init__.py +5 -1
- opentrons/protocol_api/_transfer_liquid_validation.py +17 -2
- opentrons/protocol_api/_types.py +8 -1
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/_default_labware_versions.py +33 -11
- opentrons/protocol_api/core/engine/deck_conflict.py +3 -1
- opentrons/protocol_api/core/engine/instrument.py +109 -26
- opentrons/protocol_api/core/engine/labware.py +8 -1
- opentrons/protocol_api/core/engine/module_core.py +95 -4
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +4 -18
- opentrons/protocol_api/core/engine/protocol.py +51 -2
- opentrons/protocol_api/core/engine/stringify.py +2 -0
- opentrons/protocol_api/core/engine/tasks.py +48 -0
- opentrons/protocol_api/core/engine/well.py +8 -0
- opentrons/protocol_api/core/instrument.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy/legacy_module_core.py +33 -2
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +23 -1
- opentrons/protocol_api/core/legacy/legacy_well_core.py +4 -0
- opentrons/protocol_api/core/legacy/tasks.py +19 -0
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +19 -2
- opentrons/protocol_api/core/legacy_simulator/legacy_protocol_core.py +14 -2
- opentrons/protocol_api/core/legacy_simulator/tasks.py +19 -0
- opentrons/protocol_api/core/module.py +58 -2
- opentrons/protocol_api/core/protocol.py +23 -2
- opentrons/protocol_api/core/tasks.py +31 -0
- opentrons/protocol_api/core/well.py +4 -0
- opentrons/protocol_api/instrument_context.py +388 -2
- opentrons/protocol_api/labware.py +10 -2
- opentrons/protocol_api/module_contexts.py +170 -6
- opentrons/protocol_api/protocol_context.py +87 -21
- opentrons/protocol_api/robot_context.py +41 -25
- opentrons/protocol_api/tasks.py +48 -0
- opentrons/protocol_api/validation.py +49 -3
- opentrons/protocol_engine/__init__.py +4 -0
- opentrons/protocol_engine/actions/__init__.py +6 -2
- opentrons/protocol_engine/actions/actions.py +31 -9
- opentrons/protocol_engine/clients/sync_client.py +42 -7
- opentrons/protocol_engine/commands/__init__.py +56 -0
- opentrons/protocol_engine/commands/absorbance_reader/close_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/open_lid.py +2 -15
- opentrons/protocol_engine/commands/absorbance_reader/read.py +22 -23
- opentrons/protocol_engine/commands/aspirate.py +1 -0
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +52 -19
- opentrons/protocol_engine/commands/capture_image.py +302 -0
- opentrons/protocol_engine/commands/command.py +2 -0
- opentrons/protocol_engine/commands/command_unions.py +62 -0
- opentrons/protocol_engine/commands/create_timer.py +83 -0
- opentrons/protocol_engine/commands/dispense.py +1 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +56 -19
- opentrons/protocol_engine/commands/drop_tip.py +32 -8
- opentrons/protocol_engine/commands/flex_stacker/common.py +35 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +7 -0
- opentrons/protocol_engine/commands/heater_shaker/__init__.py +14 -0
- opentrons/protocol_engine/commands/heater_shaker/common.py +20 -0
- opentrons/protocol_engine/commands/heater_shaker/set_and_wait_for_shake_speed.py +5 -4
- opentrons/protocol_engine/commands/heater_shaker/set_shake_speed.py +136 -0
- opentrons/protocol_engine/commands/heater_shaker/set_target_temperature.py +31 -5
- opentrons/protocol_engine/commands/move_labware.py +3 -4
- opentrons/protocol_engine/commands/move_to_addressable_area_for_drop_tip.py +1 -1
- opentrons/protocol_engine/commands/movement_common.py +31 -2
- opentrons/protocol_engine/commands/pick_up_tip.py +21 -11
- opentrons/protocol_engine/commands/pipetting_common.py +48 -3
- opentrons/protocol_engine/commands/set_tip_state.py +97 -0
- opentrons/protocol_engine/commands/temperature_module/set_target_temperature.py +38 -7
- opentrons/protocol_engine/commands/thermocycler/__init__.py +16 -0
- opentrons/protocol_engine/commands/thermocycler/run_extended_profile.py +6 -0
- opentrons/protocol_engine/commands/thermocycler/run_profile.py +8 -0
- opentrons/protocol_engine/commands/thermocycler/set_target_block_temperature.py +44 -7
- opentrons/protocol_engine/commands/thermocycler/set_target_lid_temperature.py +43 -14
- opentrons/protocol_engine/commands/thermocycler/start_run_extended_profile.py +191 -0
- opentrons/protocol_engine/commands/touch_tip.py +1 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_place_labware.py +6 -22
- opentrons/protocol_engine/commands/wait_for_tasks.py +98 -0
- opentrons/protocol_engine/create_protocol_engine.py +12 -0
- opentrons/protocol_engine/engine_support.py +3 -0
- opentrons/protocol_engine/errors/__init__.py +12 -0
- opentrons/protocol_engine/errors/exceptions.py +119 -0
- opentrons/protocol_engine/execution/__init__.py +4 -0
- opentrons/protocol_engine/execution/command_executor.py +62 -1
- opentrons/protocol_engine/execution/create_queue_worker.py +9 -2
- opentrons/protocol_engine/execution/labware_movement.py +13 -15
- opentrons/protocol_engine/execution/movement.py +2 -0
- opentrons/protocol_engine/execution/pipetting.py +26 -25
- opentrons/protocol_engine/execution/queue_worker.py +4 -0
- opentrons/protocol_engine/execution/run_control.py +8 -0
- opentrons/protocol_engine/execution/task_handler.py +157 -0
- opentrons/protocol_engine/protocol_engine.py +137 -36
- opentrons/protocol_engine/resources/__init__.py +4 -0
- opentrons/protocol_engine/resources/camera_provider.py +110 -0
- opentrons/protocol_engine/resources/concurrency_provider.py +27 -0
- opentrons/protocol_engine/resources/deck_configuration_provider.py +7 -0
- opentrons/protocol_engine/resources/file_provider.py +133 -58
- opentrons/protocol_engine/resources/labware_validation.py +10 -6
- opentrons/protocol_engine/slot_standardization.py +2 -0
- opentrons/protocol_engine/state/_well_math.py +60 -18
- opentrons/protocol_engine/state/addressable_areas.py +2 -0
- opentrons/protocol_engine/state/camera.py +54 -0
- opentrons/protocol_engine/state/commands.py +37 -14
- opentrons/protocol_engine/state/geometry.py +276 -379
- opentrons/protocol_engine/state/labware.py +62 -108
- opentrons/protocol_engine/state/labware_origin_math/errors.py +94 -0
- opentrons/protocol_engine/state/labware_origin_math/stackup_origin_to_labware_origin.py +1336 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +37 -0
- opentrons/protocol_engine/state/modules.py +30 -8
- opentrons/protocol_engine/state/motion.py +60 -18
- opentrons/protocol_engine/state/preconditions.py +59 -0
- opentrons/protocol_engine/state/state.py +44 -0
- opentrons/protocol_engine/state/state_summary.py +4 -0
- opentrons/protocol_engine/state/tasks.py +139 -0
- opentrons/protocol_engine/state/tips.py +177 -258
- opentrons/protocol_engine/state/update_types.py +26 -9
- opentrons/protocol_engine/types/__init__.py +23 -4
- opentrons/protocol_engine/types/command_preconditions.py +18 -0
- opentrons/protocol_engine/types/deck_configuration.py +5 -1
- opentrons/protocol_engine/types/instrument.py +8 -1
- opentrons/protocol_engine/types/labware.py +1 -13
- opentrons/protocol_engine/types/location.py +26 -2
- opentrons/protocol_engine/types/module.py +11 -1
- opentrons/protocol_engine/types/tasks.py +38 -0
- opentrons/protocol_engine/types/tip.py +9 -0
- opentrons/protocol_runner/create_simulating_orchestrator.py +29 -2
- opentrons/protocol_runner/protocol_runner.py +14 -1
- opentrons/protocol_runner/run_orchestrator.py +49 -2
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +2 -2
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/types.py +2 -1
- opentrons/simulate.py +51 -15
- opentrons/system/camera.py +334 -4
- opentrons/system/ffmpeg.py +110 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/METADATA +4 -4
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/RECORD +189 -161
- opentrons/protocol_engine/state/_labware_origin_math.py +0 -636
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/WHEEL +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/entry_points.txt +0 -0
- {opentrons-8.7.0a9.dist-info → opentrons-8.8.0a8.dist-info}/licenses/LICENSE +0 -0
|
@@ -414,7 +414,8 @@ class Simulator:
|
|
|
414
414
|
|
|
415
415
|
@ensure_yield
|
|
416
416
|
async def clean_up(self) -> None:
|
|
417
|
-
|
|
417
|
+
if hasattr(self, "_module_controls") and self._module_controls is not None:
|
|
418
|
+
await self._module_controls.clean_up()
|
|
418
419
|
|
|
419
420
|
@ensure_yield
|
|
420
421
|
async def configure_mount(
|
|
@@ -91,6 +91,8 @@ class SubsystemManager:
|
|
|
91
91
|
self._present_tools = tools.types.ToolSummary(
|
|
92
92
|
left=None, right=None, gripper=None
|
|
93
93
|
)
|
|
94
|
+
# This is intended to be an internal variable but is modified in unit tests to avoid long timeouts
|
|
95
|
+
self._check_device_update_timeout = 10.0
|
|
94
96
|
|
|
95
97
|
@property
|
|
96
98
|
def ok(self) -> bool:
|
|
@@ -183,11 +185,12 @@ class SubsystemManager:
|
|
|
183
185
|
return self._tool_task_state is True
|
|
184
186
|
|
|
185
187
|
async def _check_devices_after_update(
|
|
186
|
-
self, devices: Set[SubSystem], timeout_sec: float =
|
|
188
|
+
self, devices: Set[SubSystem], timeout_sec: Optional[float] = None
|
|
187
189
|
) -> None:
|
|
188
190
|
try:
|
|
189
191
|
await asyncio.wait_for(
|
|
190
|
-
self._do_check_devices_after_update(devices),
|
|
192
|
+
self._do_check_devices_after_update(devices),
|
|
193
|
+
timeout=timeout_sec or self._check_device_update_timeout,
|
|
191
194
|
)
|
|
192
195
|
except asyncio.TimeoutError:
|
|
193
196
|
raise RuntimeError("Device failed to come back after firmware update")
|
|
@@ -10,12 +10,14 @@ class AbstractEmulator(ABC):
|
|
|
10
10
|
"""Handle a command and return a response."""
|
|
11
11
|
...
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
def get_terminator() -> bytes:
|
|
13
|
+
def get_terminator(self) -> bytes:
|
|
15
14
|
"""Get the command terminator for messages coming from PI."""
|
|
16
15
|
return b"\r\n\r\n"
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
def get_ack() -> bytes:
|
|
17
|
+
def get_ack(self) -> bytes:
|
|
20
18
|
"""Get the command ack send to the PI."""
|
|
21
19
|
return b"ok\r\nok\r\n"
|
|
20
|
+
|
|
21
|
+
def get_autoack(self) -> bool:
|
|
22
|
+
"""Should this system automatically acknowledge messages?"""
|
|
23
|
+
return True
|
|
@@ -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,17 @@ 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
|
+
ModuleDisconnectedNotification,
|
|
34
|
+
)
|
|
24
35
|
from . import modules
|
|
25
36
|
|
|
26
37
|
if TYPE_CHECKING:
|
|
@@ -51,10 +62,21 @@ class AttachedModulesControl:
|
|
|
51
62
|
self,
|
|
52
63
|
api: Union["API", "OT3API"],
|
|
53
64
|
usb: interfaces.USBDriverInterface,
|
|
65
|
+
event_callback: Callable[[HardwareEvent], None],
|
|
54
66
|
) -> None:
|
|
55
67
|
self._available_modules: List[modules.AbstractModule] = []
|
|
56
68
|
self._api = api
|
|
57
69
|
self._usb = usb
|
|
70
|
+
self._event_callback = event_callback
|
|
71
|
+
if not IS_ROBOT and not api.is_simulator:
|
|
72
|
+
# Start task that registers emulated modules.
|
|
73
|
+
self._emulation_listen_task: asyncio.Task[
|
|
74
|
+
None
|
|
75
|
+
] | None = api.loop.create_task(
|
|
76
|
+
listen_module_connection(self.register_modules)
|
|
77
|
+
)
|
|
78
|
+
else:
|
|
79
|
+
self._emulation_listen_task = None
|
|
58
80
|
|
|
59
81
|
def subscribe_to_api_event(self, module: modules.AbstractModule) -> None:
|
|
60
82
|
self._api.add_status_bar_listener(module.event_listener)
|
|
@@ -64,22 +86,20 @@ class AttachedModulesControl:
|
|
|
64
86
|
cls,
|
|
65
87
|
api_instance: Union["API", "OT3API"],
|
|
66
88
|
board_revision: BoardRevision,
|
|
89
|
+
event_callback: Callable[[HardwareEvent], None],
|
|
67
90
|
) -> AttachedModulesControl:
|
|
68
91
|
usb_instance = (
|
|
69
92
|
usb.USBBus(board_revision)
|
|
70
93
|
if not api_instance.is_simulator and IS_ROBOT
|
|
71
94
|
else usb_simulator.USBBusSimulator()
|
|
72
95
|
)
|
|
73
|
-
mc_instance = cls(
|
|
96
|
+
mc_instance = cls(
|
|
97
|
+
api=api_instance, usb=usb_instance, event_callback=event_callback
|
|
98
|
+
)
|
|
74
99
|
|
|
75
100
|
if not api_instance.is_simulator:
|
|
76
101
|
# Do an initial scan of modules.
|
|
77
102
|
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
103
|
|
|
84
104
|
return mc_instance
|
|
85
105
|
|
|
@@ -87,6 +107,37 @@ class AttachedModulesControl:
|
|
|
87
107
|
def available_modules(self) -> List[modules.AbstractModule]:
|
|
88
108
|
return self._available_modules
|
|
89
109
|
|
|
110
|
+
async def clean_up(self) -> None:
|
|
111
|
+
"""Clean up all registered modules and emulator scanning tasks (if any)."""
|
|
112
|
+
for module in self._available_modules:
|
|
113
|
+
await module.cleanup()
|
|
114
|
+
if self._emulation_listen_task is not None:
|
|
115
|
+
self._emulation_listen_task.cancel("cleanup")
|
|
116
|
+
try:
|
|
117
|
+
await self._emulation_listen_task
|
|
118
|
+
except asyncio.CancelledError:
|
|
119
|
+
pass
|
|
120
|
+
except Exception:
|
|
121
|
+
log.exception("Exception cleaning up emulation listen task")
|
|
122
|
+
finally:
|
|
123
|
+
self._emulation_listen_task = None
|
|
124
|
+
|
|
125
|
+
async def register_simulated_module(
|
|
126
|
+
self,
|
|
127
|
+
simulated_usb_port: types.USBPort,
|
|
128
|
+
type: modules.ModuleType,
|
|
129
|
+
sim_model: str,
|
|
130
|
+
) -> modules.AbstractModule:
|
|
131
|
+
"""Register a simulated module."""
|
|
132
|
+
module = await self.build_module(
|
|
133
|
+
"", simulated_usb_port, type, sim_model, sim_serial_number=None
|
|
134
|
+
)
|
|
135
|
+
self._available_modules.append(module)
|
|
136
|
+
self._available_modules = sorted(
|
|
137
|
+
self._available_modules, key=modules.AbstractModule.sort_key
|
|
138
|
+
)
|
|
139
|
+
return module
|
|
140
|
+
|
|
90
141
|
async def build_module(
|
|
91
142
|
self,
|
|
92
143
|
port: str,
|
|
@@ -105,6 +156,7 @@ class AttachedModulesControl:
|
|
|
105
156
|
sim_model=sim_model,
|
|
106
157
|
sim_serial_number=sim_serial_number,
|
|
107
158
|
disconnected_callback=self._disconnected_callback,
|
|
159
|
+
error_callback=self._async_error_callback,
|
|
108
160
|
)
|
|
109
161
|
last_event = StatusBarUpdateEvent(
|
|
110
162
|
self._api.get_status_bar_state(), self._api.get_status_bar_enabled()
|
|
@@ -113,13 +165,51 @@ class AttachedModulesControl:
|
|
|
113
165
|
self.subscribe_to_api_event(mod)
|
|
114
166
|
return mod
|
|
115
167
|
|
|
116
|
-
def _disconnected_callback(
|
|
168
|
+
def _disconnected_callback(
|
|
169
|
+
self, model: str, port: str, serial: Optional[str]
|
|
170
|
+
) -> None:
|
|
117
171
|
"""Used by the module to indicate that it was disconnected and should be deleted."""
|
|
118
172
|
mod = ModuleAtPort(port=port, serial=serial, name="")
|
|
119
173
|
asyncio.run_coroutine_threadsafe(
|
|
120
174
|
self.unregister_modules([mod]),
|
|
121
175
|
self._api.loop,
|
|
122
176
|
)
|
|
177
|
+
try:
|
|
178
|
+
self._api.loop.call_soon(
|
|
179
|
+
self._event_callback,
|
|
180
|
+
ModuleDisconnectedNotification(
|
|
181
|
+
module_serial=serial,
|
|
182
|
+
module_model=modules.module_model_from_string(model),
|
|
183
|
+
port=port,
|
|
184
|
+
),
|
|
185
|
+
)
|
|
186
|
+
except Exception:
|
|
187
|
+
log.exception(
|
|
188
|
+
f"Module disconnect callback for module {model} {serial} at {port} failed"
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
def _async_error_callback(
|
|
192
|
+
self,
|
|
193
|
+
exc: Exception,
|
|
194
|
+
model: str,
|
|
195
|
+
port: str,
|
|
196
|
+
serial: str | None,
|
|
197
|
+
) -> None:
|
|
198
|
+
"""Used by the module to indicate it saw an error from its data poller."""
|
|
199
|
+
try:
|
|
200
|
+
self._api.loop.call_soon(
|
|
201
|
+
self._event_callback,
|
|
202
|
+
AsynchronousModuleErrorNotification(
|
|
203
|
+
exception=EnumeratedError.ensure(exc),
|
|
204
|
+
module_serial=serial,
|
|
205
|
+
module_model=modules.module_model_from_string(model),
|
|
206
|
+
port=port,
|
|
207
|
+
),
|
|
208
|
+
)
|
|
209
|
+
except Exception:
|
|
210
|
+
log.exception(
|
|
211
|
+
f"Async error callback for module {model} {serial} at {port} for exc {exc} failed"
|
|
212
|
+
)
|
|
123
213
|
|
|
124
214
|
async def unregister_modules(
|
|
125
215
|
self,
|
|
@@ -144,10 +234,15 @@ class AttachedModulesControl:
|
|
|
144
234
|
for removed_mod in removed_modules:
|
|
145
235
|
try:
|
|
146
236
|
self._available_modules.remove(removed_mod)
|
|
237
|
+
# Important: this wants to be after the remove because this may trigger
|
|
238
|
+
# recursion back to here; we therefore want the module to already be
|
|
239
|
+
# removed so that the recursion terminates next loop
|
|
240
|
+
removed_mod.disconnected_callback()
|
|
147
241
|
except ValueError:
|
|
148
|
-
log.
|
|
242
|
+
log.warning(
|
|
149
243
|
f"Removed Module {removed_mod} not found in attached modules"
|
|
150
244
|
)
|
|
245
|
+
|
|
151
246
|
for removed_mod in removed_modules:
|
|
152
247
|
log.info(
|
|
153
248
|
f"Module {removed_mod.name()} detached from port {removed_mod.port}"
|
|
@@ -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:
|