Skip to content

Commit

Permalink
feat(api): add v4 JSON executor
Browse files Browse the repository at this point in the history
  • Loading branch information
shlokamin committed Mar 12, 2020
1 parent 1c4e1b5 commit d3c68ec
Show file tree
Hide file tree
Showing 5 changed files with 193 additions and 5 deletions.
15 changes: 14 additions & 1 deletion api/src/opentrons/protocol_api/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}')
154 changes: 154 additions & 0 deletions api/src/opentrons/protocol_api/execute_v4.py
Original file line number Diff line number Diff line change
@@ -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))
10 changes: 10 additions & 0 deletions api/src/opentrons/protocol_api/module_contexts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
17 changes: 14 additions & 3 deletions api/src/opentrons/protocol_api/module_geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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


Expand Down
2 changes: 1 addition & 1 deletion api/src/opentrons/protocol_api/protocol_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down

0 comments on commit d3c68ec

Please sign in to comment.