opentrons 8.3.2a0__py2.py3-none-any.whl → 8.4.0__py2.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/calibration_storage/ot2/mark_bad_calibration.py +2 -0
- opentrons/calibration_storage/ot2/tip_length.py +6 -6
- opentrons/config/advanced_settings.py +9 -11
- opentrons/config/feature_flags.py +0 -4
- opentrons/config/reset.py +7 -2
- opentrons/drivers/asyncio/communication/__init__.py +2 -0
- opentrons/drivers/asyncio/communication/async_serial.py +4 -0
- opentrons/drivers/asyncio/communication/errors.py +41 -8
- opentrons/drivers/asyncio/communication/serial_connection.py +36 -10
- opentrons/drivers/flex_stacker/__init__.py +9 -3
- opentrons/drivers/flex_stacker/abstract.py +140 -15
- opentrons/drivers/flex_stacker/driver.py +593 -47
- opentrons/drivers/flex_stacker/errors.py +64 -0
- opentrons/drivers/flex_stacker/simulator.py +222 -24
- opentrons/drivers/flex_stacker/types.py +211 -15
- opentrons/drivers/flex_stacker/utils.py +19 -0
- opentrons/execute.py +4 -2
- opentrons/hardware_control/api.py +5 -0
- opentrons/hardware_control/backends/flex_protocol.py +4 -0
- opentrons/hardware_control/backends/ot3controller.py +12 -1
- opentrons/hardware_control/backends/ot3simulator.py +3 -0
- opentrons/hardware_control/backends/subsystem_manager.py +8 -4
- opentrons/hardware_control/instruments/ot2/instrument_calibration.py +10 -6
- opentrons/hardware_control/instruments/ot3/pipette_handler.py +59 -6
- opentrons/hardware_control/modules/__init__.py +12 -1
- opentrons/hardware_control/modules/absorbance_reader.py +11 -9
- opentrons/hardware_control/modules/flex_stacker.py +498 -0
- opentrons/hardware_control/modules/heater_shaker.py +12 -10
- opentrons/hardware_control/modules/magdeck.py +5 -1
- opentrons/hardware_control/modules/tempdeck.py +5 -1
- opentrons/hardware_control/modules/thermocycler.py +15 -14
- opentrons/hardware_control/modules/types.py +191 -1
- opentrons/hardware_control/modules/utils.py +3 -0
- opentrons/hardware_control/motion_utilities.py +20 -0
- opentrons/hardware_control/ot3api.py +145 -15
- opentrons/hardware_control/protocols/liquid_handler.py +47 -1
- opentrons/hardware_control/types.py +6 -0
- opentrons/legacy_commands/commands.py +102 -5
- opentrons/legacy_commands/helpers.py +74 -1
- opentrons/legacy_commands/types.py +33 -2
- opentrons/protocol_api/__init__.py +2 -0
- opentrons/protocol_api/_liquid.py +39 -8
- opentrons/protocol_api/_liquid_properties.py +20 -19
- opentrons/protocol_api/_transfer_liquid_validation.py +91 -0
- opentrons/protocol_api/core/common.py +3 -1
- opentrons/protocol_api/core/engine/deck_conflict.py +11 -1
- opentrons/protocol_api/core/engine/instrument.py +1356 -107
- opentrons/protocol_api/core/engine/labware.py +8 -4
- opentrons/protocol_api/core/engine/load_labware_params.py +68 -10
- opentrons/protocol_api/core/engine/module_core.py +118 -2
- opentrons/protocol_api/core/engine/pipette_movement_conflict.py +6 -14
- opentrons/protocol_api/core/engine/protocol.py +253 -11
- opentrons/protocol_api/core/engine/stringify.py +19 -8
- opentrons/protocol_api/core/engine/transfer_components_executor.py +858 -0
- opentrons/protocol_api/core/engine/well.py +73 -5
- opentrons/protocol_api/core/instrument.py +71 -21
- opentrons/protocol_api/core/labware.py +6 -2
- opentrons/protocol_api/core/legacy/labware_offset_provider.py +7 -3
- opentrons/protocol_api/core/legacy/legacy_instrument_core.py +76 -49
- opentrons/protocol_api/core/legacy/legacy_labware_core.py +8 -4
- opentrons/protocol_api/core/legacy/legacy_protocol_core.py +36 -0
- opentrons/protocol_api/core/legacy/legacy_well_core.py +27 -2
- opentrons/protocol_api/core/legacy/load_info.py +4 -12
- opentrons/protocol_api/core/legacy/module_geometry.py +6 -1
- opentrons/protocol_api/core/legacy/well_geometry.py +3 -3
- opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +73 -23
- opentrons/protocol_api/core/module.py +43 -0
- opentrons/protocol_api/core/protocol.py +33 -0
- opentrons/protocol_api/core/well.py +23 -2
- opentrons/protocol_api/instrument_context.py +454 -150
- opentrons/protocol_api/labware.py +98 -50
- opentrons/protocol_api/module_contexts.py +140 -0
- opentrons/protocol_api/protocol_context.py +163 -19
- opentrons/protocol_api/validation.py +51 -41
- opentrons/protocol_engine/__init__.py +21 -2
- opentrons/protocol_engine/actions/actions.py +5 -5
- opentrons/protocol_engine/clients/sync_client.py +6 -0
- opentrons/protocol_engine/commands/__init__.py +66 -36
- opentrons/protocol_engine/commands/absorbance_reader/__init__.py +0 -1
- opentrons/protocol_engine/commands/air_gap_in_place.py +3 -2
- opentrons/protocol_engine/commands/aspirate.py +6 -2
- opentrons/protocol_engine/commands/aspirate_in_place.py +3 -1
- opentrons/protocol_engine/commands/aspirate_while_tracking.py +210 -0
- opentrons/protocol_engine/commands/blow_out.py +2 -0
- opentrons/protocol_engine/commands/blow_out_in_place.py +4 -1
- opentrons/protocol_engine/commands/command_unions.py +102 -33
- opentrons/protocol_engine/commands/configure_for_volume.py +3 -0
- opentrons/protocol_engine/commands/dispense.py +3 -1
- opentrons/protocol_engine/commands/dispense_in_place.py +3 -0
- opentrons/protocol_engine/commands/dispense_while_tracking.py +204 -0
- opentrons/protocol_engine/commands/drop_tip.py +23 -1
- opentrons/protocol_engine/commands/flex_stacker/__init__.py +106 -0
- opentrons/protocol_engine/commands/flex_stacker/close_latch.py +72 -0
- opentrons/protocol_engine/commands/flex_stacker/common.py +15 -0
- opentrons/protocol_engine/commands/flex_stacker/empty.py +161 -0
- opentrons/protocol_engine/commands/flex_stacker/fill.py +164 -0
- opentrons/protocol_engine/commands/flex_stacker/open_latch.py +70 -0
- opentrons/protocol_engine/commands/flex_stacker/prepare_shuttle.py +112 -0
- opentrons/protocol_engine/commands/flex_stacker/retrieve.py +394 -0
- opentrons/protocol_engine/commands/flex_stacker/set_stored_labware.py +190 -0
- opentrons/protocol_engine/commands/flex_stacker/store.py +291 -0
- opentrons/protocol_engine/commands/generate_command_schema.py +31 -2
- opentrons/protocol_engine/commands/labware_handling_common.py +29 -0
- opentrons/protocol_engine/commands/liquid_probe.py +27 -13
- opentrons/protocol_engine/commands/load_labware.py +42 -39
- opentrons/protocol_engine/commands/load_lid.py +21 -13
- opentrons/protocol_engine/commands/load_lid_stack.py +130 -47
- opentrons/protocol_engine/commands/load_module.py +18 -17
- opentrons/protocol_engine/commands/load_pipette.py +3 -0
- opentrons/protocol_engine/commands/move_labware.py +139 -20
- opentrons/protocol_engine/commands/move_to_well.py +5 -11
- opentrons/protocol_engine/commands/pick_up_tip.py +5 -2
- opentrons/protocol_engine/commands/pipetting_common.py +159 -8
- opentrons/protocol_engine/commands/prepare_to_aspirate.py +15 -5
- opentrons/protocol_engine/commands/{evotip_dispense.py → pressure_dispense.py} +33 -34
- opentrons/protocol_engine/commands/reload_labware.py +6 -19
- opentrons/protocol_engine/commands/{evotip_seal_pipette.py → seal_pipette_to_tip.py} +97 -76
- opentrons/protocol_engine/commands/unsafe/unsafe_blow_out_in_place.py +3 -1
- opentrons/protocol_engine/commands/unsafe/unsafe_drop_tip_in_place.py +6 -1
- opentrons/protocol_engine/commands/{evotip_unseal_pipette.py → unseal_pipette_from_tip.py} +31 -40
- opentrons/protocol_engine/errors/__init__.py +10 -0
- opentrons/protocol_engine/errors/exceptions.py +62 -0
- opentrons/protocol_engine/execution/equipment.py +123 -106
- opentrons/protocol_engine/execution/labware_movement.py +8 -6
- opentrons/protocol_engine/execution/pipetting.py +235 -25
- opentrons/protocol_engine/execution/tip_handler.py +82 -32
- opentrons/protocol_engine/labware_offset_standardization.py +194 -0
- opentrons/protocol_engine/protocol_engine.py +22 -13
- opentrons/protocol_engine/resources/deck_configuration_provider.py +98 -2
- opentrons/protocol_engine/resources/deck_data_provider.py +1 -1
- opentrons/protocol_engine/resources/labware_data_provider.py +32 -12
- opentrons/protocol_engine/resources/labware_validation.py +7 -5
- opentrons/protocol_engine/slot_standardization.py +11 -23
- opentrons/protocol_engine/state/addressable_areas.py +84 -46
- opentrons/protocol_engine/state/frustum_helpers.py +36 -14
- opentrons/protocol_engine/state/geometry.py +892 -227
- opentrons/protocol_engine/state/labware.py +252 -55
- opentrons/protocol_engine/state/module_substates/__init__.py +4 -0
- opentrons/protocol_engine/state/module_substates/flex_stacker_substate.py +68 -0
- opentrons/protocol_engine/state/module_substates/heater_shaker_module_substate.py +22 -0
- opentrons/protocol_engine/state/module_substates/temperature_module_substate.py +13 -0
- opentrons/protocol_engine/state/module_substates/thermocycler_module_substate.py +20 -0
- opentrons/protocol_engine/state/modules.py +210 -67
- opentrons/protocol_engine/state/pipettes.py +54 -0
- opentrons/protocol_engine/state/state.py +1 -1
- opentrons/protocol_engine/state/tips.py +14 -0
- opentrons/protocol_engine/state/update_types.py +180 -25
- opentrons/protocol_engine/state/wells.py +55 -9
- opentrons/protocol_engine/types/__init__.py +300 -0
- opentrons/protocol_engine/types/automatic_tip_selection.py +39 -0
- opentrons/protocol_engine/types/command_annotations.py +53 -0
- opentrons/protocol_engine/types/deck_configuration.py +72 -0
- opentrons/protocol_engine/types/execution.py +96 -0
- opentrons/protocol_engine/types/hardware_passthrough.py +25 -0
- opentrons/protocol_engine/types/instrument.py +47 -0
- opentrons/protocol_engine/types/instrument_sensors.py +47 -0
- opentrons/protocol_engine/types/labware.py +111 -0
- opentrons/protocol_engine/types/labware_movement.py +22 -0
- opentrons/protocol_engine/types/labware_offset_location.py +111 -0
- opentrons/protocol_engine/types/labware_offset_vector.py +33 -0
- opentrons/protocol_engine/types/liquid.py +40 -0
- opentrons/protocol_engine/types/liquid_class.py +59 -0
- opentrons/protocol_engine/types/liquid_handling.py +13 -0
- opentrons/protocol_engine/types/liquid_level_detection.py +131 -0
- opentrons/protocol_engine/types/location.py +194 -0
- opentrons/protocol_engine/types/module.py +301 -0
- opentrons/protocol_engine/types/partial_tip_configuration.py +76 -0
- opentrons/protocol_engine/types/run_time_parameters.py +133 -0
- opentrons/protocol_engine/types/tip.py +18 -0
- opentrons/protocol_engine/types/util.py +21 -0
- opentrons/protocol_engine/types/well_position.py +124 -0
- opentrons/protocol_reader/extract_labware_definitions.py +7 -3
- opentrons/protocol_reader/file_format_validator.py +5 -3
- opentrons/protocol_runner/json_translator.py +4 -2
- opentrons/protocol_runner/legacy_command_mapper.py +6 -2
- opentrons/protocol_runner/run_orchestrator.py +4 -1
- opentrons/protocols/advanced_control/transfers/common.py +48 -1
- opentrons/protocols/advanced_control/transfers/transfer_liquid_utils.py +204 -0
- opentrons/protocols/api_support/definitions.py +1 -1
- opentrons/protocols/api_support/instrument.py +16 -3
- opentrons/protocols/labware.py +27 -23
- opentrons/protocols/models/__init__.py +0 -21
- opentrons/simulate.py +4 -2
- opentrons/types.py +20 -7
- opentrons/util/logging_config.py +94 -25
- opentrons/util/logging_queue_handler.py +61 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/METADATA +4 -4
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/RECORD +192 -151
- opentrons/calibration_storage/ot2/models/defaults.py +0 -0
- opentrons/calibration_storage/ot3/models/defaults.py +0 -0
- opentrons/protocol_api/core/legacy/legacy_robot_core.py +0 -0
- opentrons/protocol_engine/types.py +0 -1311
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/LICENSE +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/WHEEL +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/entry_points.txt +0 -0
- {opentrons-8.3.2a0.dist-info → opentrons-8.4.0.dist-info}/top_level.txt +0 -0
|
@@ -19,6 +19,7 @@ from opentrons.drivers.thermocycler.driver import (
|
|
|
19
19
|
LID_TARGET_MIN,
|
|
20
20
|
LID_TARGET_MAX,
|
|
21
21
|
)
|
|
22
|
+
from opentrons.hardware_control.modules import ModuleData, ModuleDataValidator
|
|
22
23
|
|
|
23
24
|
ThermocyclerModuleId = NewType("ThermocyclerModuleId", str)
|
|
24
25
|
|
|
@@ -141,3 +142,22 @@ class ThermocyclerModuleSubState:
|
|
|
141
142
|
f"Module {self.module_id} does not have a target block temperature set."
|
|
142
143
|
)
|
|
143
144
|
return target
|
|
145
|
+
|
|
146
|
+
@classmethod
|
|
147
|
+
def from_live_data(
|
|
148
|
+
cls, module_id: ThermocyclerModuleId, data: ModuleData | None
|
|
149
|
+
) -> "ThermocyclerModuleSubState":
|
|
150
|
+
"""Create a ThermocyclerModuleSubState from live data."""
|
|
151
|
+
if ModuleDataValidator.is_thermocycler_data(data):
|
|
152
|
+
return cls(
|
|
153
|
+
module_id=module_id,
|
|
154
|
+
is_lid_open=data["lid"] == "open",
|
|
155
|
+
target_block_temperature=data["targetTemp"],
|
|
156
|
+
target_lid_temperature=data["lidTarget"],
|
|
157
|
+
)
|
|
158
|
+
return cls(
|
|
159
|
+
module_id=module_id,
|
|
160
|
+
is_lid_open=False,
|
|
161
|
+
target_block_temperature=None,
|
|
162
|
+
target_lid_temperature=None,
|
|
163
|
+
)
|
|
@@ -36,8 +36,9 @@ from opentrons.protocol_engine.state.module_substates.absorbance_reader_substate
|
|
|
36
36
|
AbsorbanceReaderMeasureMode,
|
|
37
37
|
)
|
|
38
38
|
from opentrons.types import DeckSlotName, MountType, StagingSlotName
|
|
39
|
-
from .update_types import AbsorbanceReaderStateUpdate
|
|
40
|
-
from ..errors import ModuleNotConnectedError
|
|
39
|
+
from .update_types import AbsorbanceReaderStateUpdate, FlexStackerStateUpdate
|
|
40
|
+
from ..errors import ModuleNotConnectedError, AreaNotInDeckConfigurationError
|
|
41
|
+
from ..resources import deck_configuration_provider
|
|
41
42
|
|
|
42
43
|
from ..types import (
|
|
43
44
|
LoadedModule,
|
|
@@ -78,11 +79,13 @@ from .module_substates import (
|
|
|
78
79
|
TemperatureModuleSubState,
|
|
79
80
|
ThermocyclerModuleSubState,
|
|
80
81
|
AbsorbanceReaderSubState,
|
|
82
|
+
FlexStackerSubState,
|
|
81
83
|
MagneticModuleId,
|
|
82
84
|
HeaterShakerModuleId,
|
|
83
85
|
TemperatureModuleId,
|
|
84
86
|
ThermocyclerModuleId,
|
|
85
87
|
AbsorbanceReaderId,
|
|
88
|
+
FlexStackerId,
|
|
86
89
|
MagneticBlockSubState,
|
|
87
90
|
MagneticBlockId,
|
|
88
91
|
ModuleSubStateType,
|
|
@@ -147,11 +150,14 @@ class HardwareModule:
|
|
|
147
150
|
class ModuleState:
|
|
148
151
|
"""The internal data to keep track of loaded modules."""
|
|
149
152
|
|
|
150
|
-
|
|
151
|
-
"""The
|
|
153
|
+
load_location_by_module_id: Dict[str, Optional[str]]
|
|
154
|
+
"""The Cutout ID of the cutout (Flex) or slot (OT-2) that each module has been loaded.
|
|
152
155
|
|
|
153
156
|
This will be None when the module was added via
|
|
154
157
|
ProtocolEngine.use_attached_modules() instead of an explicit loadModule command.
|
|
158
|
+
AddressableAreaLocation is used to represent a literal Deck Slot for OT-2 locations.
|
|
159
|
+
The CutoutID string for a given Cutout that a Module Fixture is loaded into is used
|
|
160
|
+
for Flex. The type distinction is in place for implementation seperation between the two.
|
|
155
161
|
"""
|
|
156
162
|
|
|
157
163
|
additional_slots_occupied_by_module_id: Dict[str, List[DeckSlotName]]
|
|
@@ -211,7 +217,7 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
211
217
|
) -> None:
|
|
212
218
|
"""Initialize a ModuleStore and its state."""
|
|
213
219
|
self._state = ModuleState(
|
|
214
|
-
|
|
220
|
+
load_location_by_module_id={},
|
|
215
221
|
additional_slots_occupied_by_module_id={},
|
|
216
222
|
requested_model_by_id={},
|
|
217
223
|
hardware_by_module_id={},
|
|
@@ -302,6 +308,8 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
302
308
|
self._handle_absorbance_reader_commands(
|
|
303
309
|
state_update.absorbance_reader_state_update
|
|
304
310
|
)
|
|
311
|
+
if state_update.flex_stacker_state_update != update_types.NO_CHANGE:
|
|
312
|
+
self._handle_flex_stacker_commands(state_update.flex_stacker_state_update)
|
|
305
313
|
|
|
306
314
|
def _add_module_substate(
|
|
307
315
|
self,
|
|
@@ -312,11 +320,19 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
312
320
|
requested_model: Optional[ModuleModel],
|
|
313
321
|
module_live_data: Optional[LiveData],
|
|
314
322
|
) -> None:
|
|
323
|
+
# Loading slot name to Cutout ID (Flex)(OT-2) resolution
|
|
324
|
+
load_location: Optional[str]
|
|
325
|
+
if slot_name is not None:
|
|
326
|
+
load_location = deck_configuration_provider.get_cutout_id_by_deck_slot_name(
|
|
327
|
+
slot_name
|
|
328
|
+
)
|
|
329
|
+
else:
|
|
330
|
+
load_location = slot_name
|
|
331
|
+
|
|
315
332
|
actual_model = definition.model
|
|
316
333
|
live_data = module_live_data["data"] if module_live_data else None
|
|
317
|
-
|
|
318
334
|
self._state.requested_model_by_id[module_id] = requested_model
|
|
319
|
-
self._state.
|
|
335
|
+
self._state.load_location_by_module_id[module_id] = load_location
|
|
320
336
|
self._state.hardware_by_module_id[module_id] = HardwareModule(
|
|
321
337
|
serial_number=serial_number,
|
|
322
338
|
definition=definition,
|
|
@@ -328,31 +344,24 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
328
344
|
model=actual_model,
|
|
329
345
|
)
|
|
330
346
|
elif ModuleModel.is_heater_shaker_module_model(actual_model):
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
labware_latch_status = HeaterShakerLatchStatus.CLOSED
|
|
335
|
-
else:
|
|
336
|
-
labware_latch_status = HeaterShakerLatchStatus.OPEN
|
|
337
|
-
self._state.substate_by_module_id[module_id] = HeaterShakerModuleSubState(
|
|
347
|
+
self._state.substate_by_module_id[
|
|
348
|
+
module_id
|
|
349
|
+
] = HeaterShakerModuleSubState.from_live_data(
|
|
338
350
|
module_id=HeaterShakerModuleId(module_id),
|
|
339
|
-
|
|
340
|
-
is_plate_shaking=(
|
|
341
|
-
live_data is not None and live_data["targetSpeed"] is not None
|
|
342
|
-
),
|
|
343
|
-
plate_target_temperature=live_data["targetTemp"] if live_data else None, # type: ignore[arg-type]
|
|
351
|
+
data=live_data,
|
|
344
352
|
)
|
|
345
353
|
elif ModuleModel.is_temperature_module_model(actual_model):
|
|
346
|
-
self._state.substate_by_module_id[
|
|
354
|
+
self._state.substate_by_module_id[
|
|
355
|
+
module_id
|
|
356
|
+
] = TemperatureModuleSubState.from_live_data(
|
|
347
357
|
module_id=TemperatureModuleId(module_id),
|
|
348
|
-
|
|
358
|
+
data=live_data,
|
|
349
359
|
)
|
|
350
360
|
elif ModuleModel.is_thermocycler_module_model(actual_model):
|
|
351
|
-
self._state.substate_by_module_id[
|
|
352
|
-
module_id
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
target_lid_temperature=live_data["lidTarget"] if live_data else None, # type: ignore[arg-type]
|
|
361
|
+
self._state.substate_by_module_id[
|
|
362
|
+
module_id
|
|
363
|
+
] = ThermocyclerModuleSubState.from_live_data(
|
|
364
|
+
module_id=ThermocyclerModuleId(module_id), data=live_data
|
|
356
365
|
)
|
|
357
366
|
self._update_additional_slots_occupied_by_thermocycler(
|
|
358
367
|
module_id=module_id, slot_name=slot_name
|
|
@@ -372,6 +381,15 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
372
381
|
configured_wavelengths=None,
|
|
373
382
|
reference_wavelength=None,
|
|
374
383
|
)
|
|
384
|
+
elif ModuleModel.is_flex_stacker(actual_model):
|
|
385
|
+
self._state.substate_by_module_id[module_id] = FlexStackerSubState(
|
|
386
|
+
module_id=FlexStackerId(module_id),
|
|
387
|
+
pool_primary_definition=None,
|
|
388
|
+
pool_adapter_definition=None,
|
|
389
|
+
pool_lid_definition=None,
|
|
390
|
+
pool_count=0,
|
|
391
|
+
max_pool_count=0,
|
|
392
|
+
)
|
|
375
393
|
|
|
376
394
|
def _update_additional_slots_occupied_by_thermocycler(
|
|
377
395
|
self,
|
|
@@ -613,6 +631,20 @@ class ModuleStore(HasState[ModuleState], HandlesActions):
|
|
|
613
631
|
data=data,
|
|
614
632
|
)
|
|
615
633
|
|
|
634
|
+
def _handle_flex_stacker_commands(
|
|
635
|
+
self, state_update: FlexStackerStateUpdate
|
|
636
|
+
) -> None:
|
|
637
|
+
"""Handle Flex Stacker state updates."""
|
|
638
|
+
module_id = state_update.module_id
|
|
639
|
+
prev_substate = self._state.substate_by_module_id[module_id]
|
|
640
|
+
assert isinstance(
|
|
641
|
+
prev_substate, FlexStackerSubState
|
|
642
|
+
), f"{module_id} is not a Flex Stacker."
|
|
643
|
+
|
|
644
|
+
self._state.substate_by_module_id[
|
|
645
|
+
module_id
|
|
646
|
+
] = prev_substate.new_from_state_change(state_update)
|
|
647
|
+
|
|
616
648
|
|
|
617
649
|
class ModuleView:
|
|
618
650
|
"""Read-only view of computed module state."""
|
|
@@ -626,12 +658,17 @@ class ModuleView:
|
|
|
626
658
|
def get(self, module_id: str) -> LoadedModule:
|
|
627
659
|
"""Get module data by the module's unique identifier."""
|
|
628
660
|
try:
|
|
629
|
-
|
|
661
|
+
load_location = self._state.load_location_by_module_id[module_id]
|
|
630
662
|
attached_module = self._state.hardware_by_module_id[module_id]
|
|
631
663
|
|
|
632
664
|
except KeyError as e:
|
|
633
665
|
raise errors.ModuleNotLoadedError(module_id=module_id) from e
|
|
634
666
|
|
|
667
|
+
slot_name = None
|
|
668
|
+
if isinstance(load_location, str):
|
|
669
|
+
slot_name = deck_configuration_provider.get_deck_slot_for_cutout_id(
|
|
670
|
+
load_location
|
|
671
|
+
)
|
|
635
672
|
location = (
|
|
636
673
|
DeckSlotLocation(slotName=slot_name) if slot_name is not None else None
|
|
637
674
|
)
|
|
@@ -645,21 +682,39 @@ class ModuleView:
|
|
|
645
682
|
|
|
646
683
|
def get_all(self) -> List[LoadedModule]:
|
|
647
684
|
"""Get a list of all module entries in state."""
|
|
648
|
-
return [
|
|
685
|
+
return [
|
|
686
|
+
self.get(mod_id) for mod_id in self._state.load_location_by_module_id.keys()
|
|
687
|
+
]
|
|
649
688
|
|
|
650
689
|
def get_by_slot(
|
|
651
690
|
self,
|
|
652
691
|
slot_name: DeckSlotName,
|
|
653
692
|
) -> Optional[LoadedModule]:
|
|
654
693
|
"""Get the module located in a given slot, if any."""
|
|
655
|
-
|
|
694
|
+
locations_by_id = reversed(list(self._state.load_location_by_module_id.items()))
|
|
656
695
|
|
|
657
|
-
for module_id,
|
|
696
|
+
for module_id, load_location in locations_by_id:
|
|
697
|
+
module_slot: Optional[DeckSlotName]
|
|
698
|
+
if isinstance(load_location, str):
|
|
699
|
+
module_slot = deck_configuration_provider.get_deck_slot_for_cutout_id(
|
|
700
|
+
load_location
|
|
701
|
+
)
|
|
702
|
+
else:
|
|
703
|
+
module_slot = load_location
|
|
658
704
|
if module_slot == slot_name:
|
|
659
705
|
return self.get(module_id)
|
|
660
706
|
|
|
661
707
|
return None
|
|
662
708
|
|
|
709
|
+
def get_by_addressable_area(
|
|
710
|
+
self, addressable_area_name: str
|
|
711
|
+
) -> Optional[LoadedModule]:
|
|
712
|
+
"""Get the module associated with this addressable area, if any."""
|
|
713
|
+
for module_id in self._state.load_location_by_module_id.keys():
|
|
714
|
+
if addressable_area_name == self.get_provided_addressable_area(module_id):
|
|
715
|
+
return self.get(module_id)
|
|
716
|
+
return None
|
|
717
|
+
|
|
663
718
|
def _get_module_substate(
|
|
664
719
|
self, module_id: str, expected_type: Type[ModuleSubStateT], expected_name: str
|
|
665
720
|
) -> ModuleSubStateT:
|
|
@@ -764,6 +819,20 @@ class ModuleView:
|
|
|
764
819
|
expected_name="Absorbance Reader",
|
|
765
820
|
)
|
|
766
821
|
|
|
822
|
+
def get_flex_stacker_substate(self, module_id: str) -> FlexStackerSubState:
|
|
823
|
+
"""Return a `FlexStackerSubState` for the given Flex Stacker.
|
|
824
|
+
|
|
825
|
+
Raises:
|
|
826
|
+
ModuleNotLoadedError: If module_id has not been loaded.
|
|
827
|
+
WrongModuleTypeError: If module_id has been loaded,
|
|
828
|
+
but it's not a Flex Stacker.
|
|
829
|
+
"""
|
|
830
|
+
return self._get_module_substate(
|
|
831
|
+
module_id=module_id,
|
|
832
|
+
expected_type=FlexStackerSubState,
|
|
833
|
+
expected_name="Flex Stacker",
|
|
834
|
+
)
|
|
835
|
+
|
|
767
836
|
def get_location(self, module_id: str) -> DeckSlotLocation:
|
|
768
837
|
"""Get the slot location of the given module."""
|
|
769
838
|
location = self.get(module_id).location
|
|
@@ -773,6 +842,26 @@ class ModuleView:
|
|
|
773
842
|
)
|
|
774
843
|
return location
|
|
775
844
|
|
|
845
|
+
def get_provided_addressable_area(self, module_id: str) -> str:
|
|
846
|
+
"""Get the addressable area provided by this module.
|
|
847
|
+
|
|
848
|
+
If the current deck does not allow modules to provide locations (i.e., is an OT-2 deck)
|
|
849
|
+
then return the addressable area underneath the module.
|
|
850
|
+
"""
|
|
851
|
+
module = self.get(module_id)
|
|
852
|
+
|
|
853
|
+
if isinstance(module.location, DeckSlotLocation):
|
|
854
|
+
location = module.location.slotName
|
|
855
|
+
elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2:
|
|
856
|
+
location = DeckSlotName.SLOT_B1
|
|
857
|
+
else:
|
|
858
|
+
raise ValueError(
|
|
859
|
+
"Module location invalid for nominal module offset calculation."
|
|
860
|
+
)
|
|
861
|
+
if not self.get_deck_supports_module_fixtures():
|
|
862
|
+
return location.value
|
|
863
|
+
return self.ensure_and_convert_module_fixture_location(location, module.model)
|
|
864
|
+
|
|
776
865
|
def get_requested_model(self, module_id: str) -> Optional[ModuleModel]:
|
|
777
866
|
"""Return the model by which this module was requested.
|
|
778
867
|
|
|
@@ -841,10 +930,39 @@ class ModuleView:
|
|
|
841
930
|
Includes the slot-specific transform. Does not include the child's
|
|
842
931
|
Labware Position Check offset.
|
|
843
932
|
"""
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
933
|
+
base = self.get_nominal_offset_to_child_from_addressable_area(module_id)
|
|
934
|
+
if self.get_deck_supports_module_fixtures():
|
|
935
|
+
module_addressable_area = self.get_provided_addressable_area(module_id)
|
|
936
|
+
module_addressable_area_position = (
|
|
937
|
+
addressable_areas.get_addressable_area_offsets_from_cutout(
|
|
938
|
+
module_addressable_area
|
|
939
|
+
)
|
|
940
|
+
)
|
|
941
|
+
return base + LabwareOffsetVector(
|
|
942
|
+
x=module_addressable_area_position.x,
|
|
943
|
+
y=module_addressable_area_position.y,
|
|
944
|
+
z=module_addressable_area_position.z,
|
|
945
|
+
)
|
|
946
|
+
else:
|
|
947
|
+
return base
|
|
948
|
+
|
|
949
|
+
def get_nominal_offset_to_child_from_addressable_area(
|
|
950
|
+
self, module_id: str
|
|
951
|
+
) -> LabwareOffsetVector:
|
|
952
|
+
"""Get the position offset for a child of this module from the nearest AA.
|
|
953
|
+
|
|
954
|
+
On the Flex, this is always (0, 0, 0); on the OT-2, since modules load on top
|
|
955
|
+
of addressable areas rather than providing addressable areas, the offset is
|
|
956
|
+
the labwareOffset from the module definition, rotated by the module's
|
|
957
|
+
slotTransform if appropriate.
|
|
958
|
+
"""
|
|
959
|
+
if self.get_deck_supports_module_fixtures():
|
|
960
|
+
return LabwareOffsetVector(
|
|
961
|
+
x=0,
|
|
962
|
+
y=0,
|
|
963
|
+
z=0,
|
|
964
|
+
)
|
|
965
|
+
else:
|
|
848
966
|
definition = self.get_definition(module_id)
|
|
849
967
|
slot = self.get_location(module_id).slotName.id
|
|
850
968
|
|
|
@@ -879,29 +997,6 @@ class ModuleView:
|
|
|
879
997
|
y=xformed[1],
|
|
880
998
|
z=xformed[2],
|
|
881
999
|
)
|
|
882
|
-
else:
|
|
883
|
-
module = self.get(module_id)
|
|
884
|
-
if isinstance(module.location, DeckSlotLocation):
|
|
885
|
-
location = module.location.slotName
|
|
886
|
-
elif module.model == ModuleModel.THERMOCYCLER_MODULE_V2:
|
|
887
|
-
location = DeckSlotName.SLOT_B1
|
|
888
|
-
else:
|
|
889
|
-
raise ValueError(
|
|
890
|
-
"Module location invalid for nominal module offset calculation."
|
|
891
|
-
)
|
|
892
|
-
module_addressable_area = self.ensure_and_convert_module_fixture_location(
|
|
893
|
-
location, module.model
|
|
894
|
-
)
|
|
895
|
-
module_addressable_area_position = (
|
|
896
|
-
addressable_areas.get_addressable_area_offsets_from_cutout(
|
|
897
|
-
module_addressable_area
|
|
898
|
-
)
|
|
899
|
-
)
|
|
900
|
-
return LabwareOffsetVector(
|
|
901
|
-
x=module_addressable_area_position.x,
|
|
902
|
-
y=module_addressable_area_position.y,
|
|
903
|
-
z=module_addressable_area_position.z,
|
|
904
|
-
)
|
|
905
1000
|
|
|
906
1001
|
def get_module_calibration_offset(
|
|
907
1002
|
self, module_id: str
|
|
@@ -1109,12 +1204,21 @@ class ModuleView:
|
|
|
1109
1204
|
else:
|
|
1110
1205
|
neighbor_slot = DeckSlotName.from_primitive(neighbor_int)
|
|
1111
1206
|
|
|
1112
|
-
|
|
1207
|
+
# Convert the load location list from addressable areas and cutout IDs to a slot name list
|
|
1208
|
+
load_locations = self._state.load_location_by_module_id.values()
|
|
1209
|
+
module_slots = []
|
|
1210
|
+
for location in load_locations:
|
|
1211
|
+
if isinstance(location, str):
|
|
1212
|
+
module_slots.append(
|
|
1213
|
+
deck_configuration_provider.get_deck_slot_for_cutout_id(location)
|
|
1214
|
+
)
|
|
1215
|
+
|
|
1216
|
+
return neighbor_slot in module_slots
|
|
1113
1217
|
|
|
1114
1218
|
def select_hardware_module_to_load( # noqa: C901
|
|
1115
1219
|
self,
|
|
1116
1220
|
model: ModuleModel,
|
|
1117
|
-
location:
|
|
1221
|
+
location: str,
|
|
1118
1222
|
attached_modules: Sequence[HardwareModule],
|
|
1119
1223
|
expected_serial_number: Optional[str] = None,
|
|
1120
1224
|
) -> HardwareModule:
|
|
@@ -1143,10 +1247,13 @@ class ModuleView:
|
|
|
1143
1247
|
"""
|
|
1144
1248
|
existing_mod_in_slot = None
|
|
1145
1249
|
|
|
1146
|
-
for
|
|
1147
|
-
|
|
1250
|
+
for (
|
|
1251
|
+
mod_id,
|
|
1252
|
+
load_location,
|
|
1253
|
+
) in self._state.load_location_by_module_id.items():
|
|
1254
|
+
if isinstance(load_location, str) and location == load_location:
|
|
1148
1255
|
existing_mod_in_slot = self._state.hardware_by_module_id.get(mod_id)
|
|
1149
|
-
|
|
1256
|
+
|
|
1150
1257
|
if existing_mod_in_slot:
|
|
1151
1258
|
existing_def = existing_mod_in_slot.definition
|
|
1152
1259
|
|
|
@@ -1154,9 +1261,9 @@ class ModuleView:
|
|
|
1154
1261
|
return existing_mod_in_slot
|
|
1155
1262
|
|
|
1156
1263
|
else:
|
|
1264
|
+
_err = f" present in {location}"
|
|
1157
1265
|
raise errors.ModuleAlreadyPresentError(
|
|
1158
|
-
f"A {existing_def.model.value} is already"
|
|
1159
|
-
f" present in {location.slotName.value}"
|
|
1266
|
+
f"A {existing_def.model.value} is already" + _err
|
|
1160
1267
|
)
|
|
1161
1268
|
|
|
1162
1269
|
for m in attached_modules:
|
|
@@ -1168,7 +1275,10 @@ class ModuleView:
|
|
|
1168
1275
|
else:
|
|
1169
1276
|
return m
|
|
1170
1277
|
|
|
1171
|
-
raise errors.ModuleNotAttachedError(
|
|
1278
|
+
raise errors.ModuleNotAttachedError(
|
|
1279
|
+
f"No available {model.value} with {expected_serial_number or 'any'}"
|
|
1280
|
+
" serial found."
|
|
1281
|
+
)
|
|
1172
1282
|
|
|
1173
1283
|
def get_heater_shaker_movement_restrictors(
|
|
1174
1284
|
self,
|
|
@@ -1258,6 +1368,11 @@ class ModuleView:
|
|
|
1258
1368
|
"Only readings of 96 Well labware are supported for conversion to map of values by well."
|
|
1259
1369
|
)
|
|
1260
1370
|
|
|
1371
|
+
def get_deck_supports_module_fixtures(self) -> bool:
|
|
1372
|
+
"""Check if the loaded deck supports modules as fixtures."""
|
|
1373
|
+
deck_type = self._state.deck_type
|
|
1374
|
+
return deck_type not in [DeckType.OT2_STANDARD, DeckType.OT2_SHORT_TRASH]
|
|
1375
|
+
|
|
1261
1376
|
def ensure_and_convert_module_fixture_location(
|
|
1262
1377
|
self,
|
|
1263
1378
|
deck_slot: DeckSlotName,
|
|
@@ -1269,8 +1384,8 @@ class ModuleView:
|
|
|
1269
1384
|
"""
|
|
1270
1385
|
deck_type = self._state.deck_type
|
|
1271
1386
|
|
|
1272
|
-
if
|
|
1273
|
-
raise
|
|
1387
|
+
if not self.get_deck_supports_module_fixtures():
|
|
1388
|
+
raise AreaNotInDeckConfigurationError(
|
|
1274
1389
|
f"Invalid Deck Type: {deck_type.name} - Does not support modules as fixtures."
|
|
1275
1390
|
)
|
|
1276
1391
|
|
|
@@ -1296,6 +1411,11 @@ class ModuleView:
|
|
|
1296
1411
|
assert deck_slot.value[-1] == "3"
|
|
1297
1412
|
return f"absorbanceReaderV1{deck_slot.value}"
|
|
1298
1413
|
|
|
1414
|
+
elif model == ModuleModel.FLEX_STACKER_MODULE_V1:
|
|
1415
|
+
# loaded to column 3 but the addressable area is in column 4
|
|
1416
|
+
assert deck_slot.value[-1] == "3"
|
|
1417
|
+
return f"flexStackerModuleV1{deck_slot.value[0]}4"
|
|
1418
|
+
|
|
1299
1419
|
raise ValueError(
|
|
1300
1420
|
f"Unknown module {model.name} has no addressable areas to provide."
|
|
1301
1421
|
)
|
|
@@ -1311,3 +1431,26 @@ class ModuleView:
|
|
|
1311
1431
|
addressableAreaName="absorbanceReaderV1LidDock" + lid_doc_slot.value
|
|
1312
1432
|
)
|
|
1313
1433
|
return lid_dock_area
|
|
1434
|
+
|
|
1435
|
+
def get_stacker_max_fill_height(self, module_id: str) -> float:
|
|
1436
|
+
"""Get the maximum fill height for the Flex Stacker."""
|
|
1437
|
+
definition = self.get_definition(module_id)
|
|
1438
|
+
|
|
1439
|
+
if (
|
|
1440
|
+
definition.moduleType == ModuleType.FLEX_STACKER
|
|
1441
|
+
and hasattr(definition.dimensions, "maxStackerFillHeight")
|
|
1442
|
+
and definition.dimensions.maxStackerFillHeight is not None
|
|
1443
|
+
):
|
|
1444
|
+
return definition.dimensions.maxStackerFillHeight
|
|
1445
|
+
else:
|
|
1446
|
+
raise errors.WrongModuleTypeError(
|
|
1447
|
+
f"Cannot get max fill height of {definition.moduleType}"
|
|
1448
|
+
)
|
|
1449
|
+
|
|
1450
|
+
def stacker_max_pool_count_by_height(
|
|
1451
|
+
self, module_id: str, pool_height: float
|
|
1452
|
+
) -> int:
|
|
1453
|
+
"""Get the maximum stack count for the Flex Stacker by stack height."""
|
|
1454
|
+
max_fill_height = self.get_stacker_max_fill_height(module_id)
|
|
1455
|
+
assert max_fill_height > 0
|
|
1456
|
+
return math.floor(max_fill_height / pool_height)
|
|
@@ -125,6 +125,8 @@ class PipetteState:
|
|
|
125
125
|
flow_rates_by_id: Dict[str, FlowRates]
|
|
126
126
|
nozzle_configuration_by_id: Dict[str, NozzleMap]
|
|
127
127
|
liquid_presence_detection_by_id: Dict[str, bool]
|
|
128
|
+
ready_to_aspirate_by_id: Dict[str, bool]
|
|
129
|
+
has_clean_tips_by_id: Dict[str, bool]
|
|
128
130
|
|
|
129
131
|
|
|
130
132
|
class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
@@ -145,6 +147,8 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
145
147
|
flow_rates_by_id={},
|
|
146
148
|
nozzle_configuration_by_id={},
|
|
147
149
|
liquid_presence_detection_by_id={},
|
|
150
|
+
ready_to_aspirate_by_id={},
|
|
151
|
+
has_clean_tips_by_id={},
|
|
148
152
|
)
|
|
149
153
|
|
|
150
154
|
def handle_action(self, action: Action) -> None:
|
|
@@ -156,6 +160,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
156
160
|
self._update_pipette_nozzle_map(state_update)
|
|
157
161
|
self._update_tip_state(state_update)
|
|
158
162
|
self._update_volumes(state_update)
|
|
163
|
+
self._update_ready_for_aspirate(state_update)
|
|
159
164
|
|
|
160
165
|
if isinstance(action, SetPipetteMovementSpeedAction):
|
|
161
166
|
self._state.movement_speed_by_id[action.pipette_id] = action.speed
|
|
@@ -174,6 +179,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
174
179
|
)
|
|
175
180
|
self._state.movement_speed_by_id[pipette_id] = None
|
|
176
181
|
self._state.attached_tip_by_id[pipette_id] = None
|
|
182
|
+
self._state.ready_to_aspirate_by_id[pipette_id] = False
|
|
177
183
|
|
|
178
184
|
def _update_tip_state(self, state_update: update_types.StateUpdate) -> None:
|
|
179
185
|
if state_update.pipette_tip_state != update_types.NO_CHANGE:
|
|
@@ -209,6 +215,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
209
215
|
else:
|
|
210
216
|
pipette_id = state_update.pipette_tip_state.pipette_id
|
|
211
217
|
self._state.attached_tip_by_id[pipette_id] = None
|
|
218
|
+
self._state.has_clean_tips_by_id[pipette_id] = False
|
|
212
219
|
|
|
213
220
|
static_config = self._state.static_config_by_id.get(pipette_id)
|
|
214
221
|
if static_config:
|
|
@@ -314,9 +321,23 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
314
321
|
state_update.pipette_nozzle_map.pipette_id
|
|
315
322
|
] = state_update.pipette_nozzle_map.nozzle_map
|
|
316
323
|
|
|
324
|
+
def _update_ready_for_aspirate(
|
|
325
|
+
self, state_update: update_types.StateUpdate
|
|
326
|
+
) -> None:
|
|
327
|
+
if state_update.ready_to_aspirate != update_types.NO_CHANGE:
|
|
328
|
+
self._state.ready_to_aspirate_by_id[
|
|
329
|
+
state_update.ready_to_aspirate.pipette_id
|
|
330
|
+
] = state_update.ready_to_aspirate.ready_to_aspirate
|
|
331
|
+
|
|
317
332
|
def _update_volumes(self, state_update: update_types.StateUpdate) -> None:
|
|
318
333
|
if state_update.pipette_aspirated_fluid == update_types.NO_CHANGE:
|
|
319
334
|
return
|
|
335
|
+
# set the tip state to unclean, if an "empty" update has a clean_tip flag
|
|
336
|
+
# it will set it to true
|
|
337
|
+
self._state.has_clean_tips_by_id[
|
|
338
|
+
state_update.pipette_aspirated_fluid.pipette_id
|
|
339
|
+
] = False
|
|
340
|
+
|
|
320
341
|
if state_update.pipette_aspirated_fluid.type == "aspirated":
|
|
321
342
|
self._update_aspirated(state_update.pipette_aspirated_fluid)
|
|
322
343
|
elif state_update.pipette_aspirated_fluid.type == "ejected":
|
|
@@ -343,6 +364,7 @@ class PipetteStore(HasState[PipetteState], HandlesActions):
|
|
|
343
364
|
|
|
344
365
|
def _update_empty(self, update: update_types.PipetteEmptyFluidUpdate) -> None:
|
|
345
366
|
self._state.pipette_contents_by_id[update.pipette_id] = fluid_stack.FluidStack()
|
|
367
|
+
self._state.has_clean_tips_by_id[update.pipette_id] = update.clean_tip
|
|
346
368
|
|
|
347
369
|
def _update_unknown(self, update: update_types.PipetteUnknownFluidUpdate) -> None:
|
|
348
370
|
self._state.pipette_contents_by_id[update.pipette_id] = None
|
|
@@ -482,6 +504,29 @@ class PipetteView:
|
|
|
482
504
|
f"Pipette {pipette_id} not found; unable to get current volume."
|
|
483
505
|
) from e
|
|
484
506
|
|
|
507
|
+
def get_has_clean_tip(self, pipette_id: str) -> bool:
|
|
508
|
+
"""Get if the tip of a pipette by ID is clean.
|
|
509
|
+
|
|
510
|
+
This is only true directly after a pick up tip, once any kind of aspirate happens
|
|
511
|
+
it is no longer clean
|
|
512
|
+
|
|
513
|
+
Returns:
|
|
514
|
+
True if the tip is clean
|
|
515
|
+
False if it is unclean
|
|
516
|
+
|
|
517
|
+
Raises:
|
|
518
|
+
PipetteNotLoadedError: pipette ID does not exist.
|
|
519
|
+
TipNotAttachedError: if no tip is attached to the pipette.
|
|
520
|
+
"""
|
|
521
|
+
self.validate_tip_state(pipette_id, True)
|
|
522
|
+
|
|
523
|
+
try:
|
|
524
|
+
return self._state.has_clean_tips_by_id[pipette_id]
|
|
525
|
+
except KeyError as e:
|
|
526
|
+
raise errors.PipetteNotLoadedError(
|
|
527
|
+
f"Pipette {pipette_id} not found; unable to get current volume."
|
|
528
|
+
) from e
|
|
529
|
+
|
|
485
530
|
def get_liquid_dispensed_by_ejecting_volume(
|
|
486
531
|
self, pipette_id: str, volume: float
|
|
487
532
|
) -> Optional[float]:
|
|
@@ -822,3 +867,12 @@ class PipetteView:
|
|
|
822
867
|
) -> float:
|
|
823
868
|
"""Get the plunger position provided for the given pipette id."""
|
|
824
869
|
return self.get_config(pipette_id).plunger_positions[position_name]
|
|
870
|
+
|
|
871
|
+
def get_ready_to_aspirate(self, pipette_id: str) -> bool:
|
|
872
|
+
"""Get if the provided pipette is ready to aspirate for the given pipette id."""
|
|
873
|
+
try:
|
|
874
|
+
return self._state.ready_to_aspirate_by_id[pipette_id]
|
|
875
|
+
except KeyError as e:
|
|
876
|
+
raise errors.PipetteNotLoadedError(
|
|
877
|
+
f"Pipette {pipette_id} not found; unable to determine if pipette ready to aspirate."
|
|
878
|
+
) from e
|
|
@@ -374,7 +374,7 @@ class StateStore(StateView, ActionHandler):
|
|
|
374
374
|
self._addressable_areas = AddressableAreaView(state.addressable_areas)
|
|
375
375
|
self._labware = LabwareView(state.labware)
|
|
376
376
|
self._pipettes = PipetteView(state.pipettes)
|
|
377
|
-
self._modules = ModuleView(state.modules)
|
|
377
|
+
self._modules = ModuleView(state=state.modules)
|
|
378
378
|
self._liquid = LiquidView(state.liquids)
|
|
379
379
|
self._liquid_classes = LiquidClassView(state.liquid_classes)
|
|
380
380
|
self._tips = TipView(state.tips)
|
|
@@ -110,6 +110,20 @@ class TipStore(HasState[TipState], HandlesActions):
|
|
|
110
110
|
self._state.column_by_labware_id[labware_id] = [
|
|
111
111
|
column for column in definition.ordering
|
|
112
112
|
]
|
|
113
|
+
if state_update.batch_loaded_labware != update_types.NO_CHANGE:
|
|
114
|
+
for labware_id in state_update.batch_loaded_labware.new_locations_by_id:
|
|
115
|
+
definition = state_update.batch_loaded_labware.definitions_by_id[
|
|
116
|
+
labware_id
|
|
117
|
+
]
|
|
118
|
+
if definition.parameters.isTiprack:
|
|
119
|
+
self._state.tips_by_labware_id[labware_id] = {
|
|
120
|
+
well_name: TipRackWellState.CLEAN
|
|
121
|
+
for column in definition.ordering
|
|
122
|
+
for well_name in column
|
|
123
|
+
}
|
|
124
|
+
self._state.column_by_labware_id[labware_id] = [
|
|
125
|
+
column for column in definition.ordering
|
|
126
|
+
]
|
|
113
127
|
|
|
114
128
|
def _set_used_tips(self, pipette_id: str, well_name: str, labware_id: str) -> None:
|
|
115
129
|
columns = self._state.column_by_labware_id.get(labware_id, [])
|