From d3c68ecb38d62b79b1cae18b1fbe4313ae4c1b9f Mon Sep 17 00:00:00 2001 From: Shlok Amin Date: Thu, 12 Mar 2020 14:43:12 -0400 Subject: [PATCH] feat(api): add v4 JSON executor #4868 --- api/src/opentrons/protocol_api/execute.py | 15 +- api/src/opentrons/protocol_api/execute_v4.py | 154 ++++++++++++++++++ .../opentrons/protocol_api/module_contexts.py | 10 ++ .../opentrons/protocol_api/module_geometry.py | 17 +- .../protocol_api/protocol_context.py | 2 +- 5 files changed, 193 insertions(+), 5 deletions(-) create mode 100644 api/src/opentrons/protocol_api/execute_v4.py diff --git a/api/src/opentrons/protocol_api/execute.py b/api/src/opentrons/protocol_api/execute.py index 30242df1167..1c9f8c4065f 100644 --- a/api/src/opentrons/protocol_api/execute.py +++ b/api/src/opentrons/protocol_api/execute.py @@ -8,7 +8,7 @@ from opentrons.drivers.smoothie_drivers.driver_3_0 import SmoothieAlarm from .contexts import ProtocolContext -from . import execute_v3 +from . import execute_v3, execute_v4 from opentrons.protocols.types import PythonProtocol, Protocol, APIVersion from opentrons.hardware_control import ExecutionCancelledError @@ -142,6 +142,19 @@ def run_protocol(protocol: Protocol, lw = execute_v3.load_labware_from_json_defs( context, protocol.contents) execute_v3.dispatch_json(context, protocol.contents, ins, lw) + elif protocol.schema_version == 4: + # reuse the v3 fns for loading labware and pipettes + # b/c the v4 protocol has no changes for these keys + ins = execute_v3.load_pipettes_from_json( + context, protocol.contents) + + modules = execute_v4.load_modules_from_json( + context, protocol.contents) + + lw = execute_v4.load_labware_from_json_defs( + context, protocol.contents, modules) + execute_v4.dispatch_json( + context, protocol.contents, ins, lw, modules) else: raise RuntimeError( f'Unsupported JSON protocol schema: {protocol.schema_version}') diff --git a/api/src/opentrons/protocol_api/execute_v4.py b/api/src/opentrons/protocol_api/execute_v4.py new file mode 100644 index 00000000000..de4e68c1b82 --- /dev/null +++ b/api/src/opentrons/protocol_api/execute_v4.py @@ -0,0 +1,154 @@ +import logging +import asyncio +from typing import Any, Dict + +from .contexts import ProtocolContext, InstrumentContext, ModuleContext +from . import labware +from .execute_v3 import _delay, _blowout, _pick_up_tip, _drop_tip, _aspirate, \ + _dispense, _touch_tip, _move_to_slot + +MODULE_LOG = logging.getLogger(__name__) + + +def load_labware_from_json_defs( + ctx: ProtocolContext, + protocol: Dict[Any, Any], + modules: Dict[str, ModuleContext]) -> Dict[str, labware.Labware]: + protocol_labware = protocol['labware'] + definitions = protocol['labwareDefinitions'] + loaded_labware = {} + + for labware_id, props in protocol_labware.items(): + slot = props['slot'] + definition = definitions[props['definitionId']] + label = props.get('displayName', None) + if slot in modules: + loaded_labware[labware_id] = \ + modules[slot].load_labware_from_definition(definition, label) + else: + loaded_labware[labware_id] = \ + ctx.load_labware_from_definition(definition, slot, label) + + return loaded_labware + + +def load_modules_from_json( + ctx: ProtocolContext, + protocol: Dict[Any, Any]) -> Dict[str, ModuleContext]: + module_data = protocol['modules'] + modules_by_id = {} + for module_id, props in module_data.items(): + model = props['model'] + slot = props['slot'] + instr = ctx.load_module(model, slot) + modules_by_id[module_id] = instr + + return modules_by_id + + +def _engage_magnet(modules, params) -> None: + module_id = params['module'] + module = modules[module_id] + engage_height = params['engageHeight'] + module.engage(height_from_base=engage_height) + + +def _disengage_magnet(modules, params) -> None: + module_id = params['module'] + module = modules[module_id] + module.disengage() + + +def _temperature_module_set_temp(modules, params) -> None: + module_id = params['module'] + module = modules[module_id] + temperature = params['temperature'] + module.start_set_temperature(temperature) + + +def _temperature_module_deactivate(modules, params) -> None: + module_id = params['module'] + module = modules[module_id] + module.deactivate() + + +def _temperature_module_await_temp(modules, params) -> None: + module_id = params['module'] + module = modules[module_id] + + awaiting_temperature = params['temperature'] + status = module.status + # in some cases the module status will flicker from one state to another + # while holding, because the temperature delta will be exceeded. + # when this flicker happens, this function would sleep for a moment longer + + if status == 'heating': + while (module.temperature < awaiting_temperature): + asyncio.sleep(0.2) + + elif status == 'cooling': + while (module.temperature > awaiting_temperature): + asyncio.sleep(0.2) + + +dispatcher_map: Dict[Any, Any] = { + "delay": _delay, + "blowout": _blowout, + "pickUpTip": _pick_up_tip, + "dropTip": _drop_tip, + "aspirate": _aspirate, + "dispense": _dispense, + "touchTip": _touch_tip, + "moveToSlot": _move_to_slot, + "magneticModule/engageMagnet": _engage_magnet, + "magneticModule/disengageMagnet": _disengage_magnet, + "temperatureModule/setTargetTemperature": _temperature_module_set_temp, + "temperatureModule/deactivate": _temperature_module_deactivate, + "temperatureModule/awaitTemperature": _temperature_module_await_temp +} + + +def dispatch_json(context: ProtocolContext, + protocol_data: Dict[Any, Any], + instruments: Dict[str, InstrumentContext], + loaded_labware: Dict[str, labware.Labware], + modules: Dict[str, ModuleContext]) -> None: + commands = protocol_data['commands'] + + pipette_command_list = [ + "blowout", + "pickUpTip", + "dropTip", + "aspirate", + "dispense", + "touchTip", + ] + + module_command_list = [ + "magneticModule/engageMagnet", + "magneticModule/disengageMagnet", + "temperatureModule/setTargetTemperature", + "temperatureModule/deactivate", + "temperatureModule/awaitTemperature" + ] + + for command_item in commands: + command_type = command_item['command'] + params = command_item['params'] + + # different `_command` helpers take different args + if command_type in pipette_command_list: + dispatcher_map[command_type]( + instruments, loaded_labware, params) + elif command_type == 'delay': + dispatcher_map[command_type](context, params) + elif command_type == 'moveToSlot': + dispatcher_map[command_type]( + context, instruments, params) + elif command_type in module_command_list: + dispatcher_map[command_type]( + modules, params + ) + else: + raise RuntimeError( + "Unsupported command type {}".format(command_type)) diff --git a/api/src/opentrons/protocol_api/module_contexts.py b/api/src/opentrons/protocol_api/module_contexts.py index d29cbb2015c..790798f246c 100644 --- a/api/src/opentrons/protocol_api/module_contexts.py +++ b/api/src/opentrons/protocol_api/module_contexts.py @@ -232,6 +232,16 @@ def target(self): """ Current target temperature in C""" return self._module.target + @property # type: ignore + @requires_version(2, 0) + def status(self): + """ The status of the module. + + Returns 'holding at target', 'cooling', 'heating', or 'idle' + + """ + return self._module.status + class MagneticModuleContext(ModuleContext): """ An object representing a connected Temperature Module. diff --git a/api/src/opentrons/protocol_api/module_geometry.py b/api/src/opentrons/protocol_api/module_geometry.py index 896591cf2e3..0f07a17a270 100644 --- a/api/src/opentrons/protocol_api/module_geometry.py +++ b/api/src/opentrons/protocol_api/module_geometry.py @@ -515,6 +515,15 @@ def load_module( def resolve_module_model(module_name: str) -> ModuleModel: """ Turn any of the supported load names into module model names """ + + model_map: Mapping[str, ModuleModel] = { + 'magneticModuleV1': MagneticModuleModel.MAGNETIC_V1, + 'magneticModuleV2': MagneticModuleModel.MAGNETIC_V2, + 'temperatureModuleV1': TemperatureModuleModel.TEMPERATURE_V1, + 'temperatureModuleV2': TemperatureModuleModel.TEMPERATURE_V2, + 'thermocyclerModuleV1': ThermocyclerModuleModel.THERMOCYCLER_V1, + } + alias_map: Mapping[str, ModuleModel] = { 'magdeck': MagneticModuleModel.MAGNETIC_V1, 'magnetic module': MagneticModuleModel.MAGNETIC_V1, @@ -525,12 +534,14 @@ def resolve_module_model(module_name: str) -> ModuleModel: 'thermocycler': ThermocyclerModuleModel.THERMOCYCLER_V1, 'thermocycler module': ThermocyclerModuleModel.THERMOCYCLER_V1, } + lower_name = module_name.lower() - resolved_name = alias_map.get(lower_name, None) + resolved_name = model_map.get(module_name, None) \ + or alias_map.get(lower_name, None) if not resolved_name: raise ValueError(f'{module_name} is not a valid module load name.\n' - 'Valid names (ignoring case): ' - '"' + '", "'.join(alias_map.keys()) + '"') + 'Valid names (ignoring case): ''"' + '", "' + .join(*model_map.keys(), *alias_map.keys()) + '"') return resolved_name diff --git a/api/src/opentrons/protocol_api/protocol_context.py b/api/src/opentrons/protocol_api/protocol_context.py index 2765cd76aed..93215d4d561 100644 --- a/api/src/opentrons/protocol_api/protocol_context.py +++ b/api/src/opentrons/protocol_api/protocol_context.py @@ -409,7 +409,7 @@ def load_module( A map of deck positions to loaded modules can be accessed later using :py:attr:`loaded_modules`. - :param str module_name: The name of the module. + :param str module_name: The name or model of the module. :param location: The location of the module. This is usually the name or number of the slot on the deck where you will be placing the module. Some modules, like