Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/edge' into api_fix_aspirate_tip
Browse files Browse the repository at this point in the history
  • Loading branch information
curtelsasser committed May 14, 2021
2 parents ede0869 + 33dd2b3 commit 09af36a
Show file tree
Hide file tree
Showing 57 changed files with 1,342 additions and 872 deletions.
Binary file modified api/docs/img/modules/multiples_of_a_module.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
43 changes: 8 additions & 35 deletions api/docs/v2/new_modules.rst
Original file line number Diff line number Diff line change
Expand Up @@ -602,13 +602,7 @@ Using Multiple Modules of the Same Type
***************************************

To use this feature, you must be running software version 4.3 or higher. Currently, you can only use multiple Magnetic Modules or multiple Temperature Modules.
You won't be able to load multiple Thermocycler Modules.

To use multiple modules, load two modules of the same type into your protocol. The robot will then map out
each module in order of appearance to the physical robot ports in order of appearance. That means if you load
two Temperature Modules, the Temperature Module attached to the first port starting from the left will be related to the first Temperature
Module in your protocol while the second Temperature Module loaded would be related to the Temperature Module connected to the
next port to the right.
You won’t be able to load multiple Thermocycler Modules.

The following diagram shows the mapping of two Temperature Modules on the robot.

Expand All @@ -626,34 +620,13 @@ In a protocol, the diagram would map to your modules as found below.
metadata = {'apiLevel': '|apiLevel|'}
def run(protocol: protocol_api.ProtocolContext):
# Load Temperature Module A in deck slot 1 on port 1 of the robot.
temperature_module_a = protocol.load_module('temperature module gen2', 1)
# Load Temperature Module B in deck slot 3 on port 1 of the hub.
temperature_module_b = protocol.load_module('temperature module gen2', 3)
Referencing the diagram, you should make sure that Temperature Module A in slot 1 is plugged into
port 1 of the robot and Temperature Module B in slot 3 is plugged into port 1 of the hub.

If for whatever reason you want to plug Temperature Module B into a port to the left of Temperature Module A,
you should load Temperature Module B before Temperature Module A.


.. code-block:: python
:substitutions:
from opentrons import protocol_api
metadata = {'apiLevel': '|apiLevel|'}
def run(protocol: protocol_api.ProtocolContext):
# Load Temperature Module B in deck slot 3 on port 1 of the robot.
temperature_module_b = protocol.load_module('temperature module gen2', 3)
# Load Temperature Module 1 in deck slot 1 on port 1 of the robot.
temperature_module_1 = protocol.load_module('temperature module gen2', 1)
# Load Temperature Module A in deck slot 1 on port 1 of the hub.
temperature_module_a = protocol.load_module('temperature module gen2', 1)
# Load Temperature Module 2 in deck slot 3 on port 2 of the robot.
temperature_module_2 = protocol.load_module('temperature module gen2', 3)
Referencing the diagram, you should make sure that Temperature Module 1 in slot 1 is plugged into port 1 of the robot and Temperature Module 1 in slot 3 is plugged into port 2 of the robot.
If for whatever reason you want to plug Temperature Module 2 into a port to the left of Temperature Module 1, you should switch the modules physically on your robot.

The Opentrons App will display USB Port information while using modules in your protocol. You can also use the
guidelines from above to get a predictable module order in Jupyter Notebook, but it will not display USB Port information
in Jupyter Notebook.
For detailed information, please refer to `Using Multiple Modules of the Same Type <https://support.opentrons.com/en/articles/5167312-using-modules-of-the-same-type-on-the-ot-2>`_ in our help center.
26 changes: 9 additions & 17 deletions api/src/opentrons/api/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@
SynchronousAdapter,
ExecutionCancelledError,
ThreadedAsyncLock)
from opentrons.hardware_control.types import (DoorState, HardwareEventType,
HardwareEvent)
from opentrons.hardware_control.types import (HardwareEventType, HardwareEvent,
PauseType)
from .models import Container, Instrument, Module
from .dev_types import State, StateInfo, Message, LastCommand, Error, CommandShortId

Expand Down Expand Up @@ -469,20 +469,21 @@ def pause(self,
reason: str = None,
user_message: str = None,
duration: float = None) -> None:
self._hardware.pause()
self._hardware.pause(PauseType.PAUSE)
self.set_state(
'paused', reason=reason,
user_message=user_message, duration=duration)

def resume(self) -> None:
if not self.blocked:
self._hardware.resume()
self._hardware.resume(PauseType.PAUSE)
self.set_state('running')

def _start_hardware_event_watcher(self) -> None:
if not callable(self._event_watcher):
# initialize and update window switch state
self._update_window_state(self._hardware.door_state)
self.door_state = str(self._hardware.door_state)
self.blocked = self._hardware._pause_manager.blocked_by_door
log.info('Starting hardware event watcher')
self._event_watcher = self._hardware.register_callback(
self._handle_hardware_event)
Expand All @@ -497,22 +498,13 @@ def _remove_hardware_event_watcher(self) -> None:

def _handle_hardware_event(self, hw_event: HardwareEvent) -> None:
if hw_event.event == HardwareEventType.DOOR_SWITCH_CHANGE:
self._update_window_state(hw_event.new_state)
if ff.enable_door_safety_switch() and \
hw_event.new_state == DoorState.OPEN and \
self.state == 'running':
self.door_state = str(hw_event.new_state)
self.blocked = hw_event.blocking
if self.blocked and self.state == 'running':
self.pause('Robot door is open')
else:
self._on_state_changed()

def _update_window_state(self, state: DoorState) -> None:
self.door_state = str(state)
if ff.enable_door_safety_switch() and \
state == DoorState.OPEN:
self.blocked = True
else:
self.blocked = False

@robot_is_busy
def _run(self) -> None:
def on_command(message: command_types.CommandMessage) -> None:
Expand Down
4 changes: 2 additions & 2 deletions api/src/opentrons/hardware_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"""

from .adapters import SynchronousAdapter
from .api import API
from .api import API, PauseManager
from .controller import Controller
from .simulator import Simulator
from .pipette import Pipette
Expand All @@ -24,7 +24,7 @@
from .threaded_async_lock import ThreadedAsyncLock, ThreadedAsyncForbidden

__all__ = [
'API', 'Controller', 'Simulator', 'Pipette',
'API', 'Controller', 'Simulator', 'Pipette', 'PauseManager',
'SynchronousAdapter', 'HardwareAPILike', 'CriticalPoint',
'NoTipAttachedError', 'TipAttachedError', 'DROP_TIP_RELEASE_DISTANCE',
'ThreadManager', 'ExecutionManager', 'ExecutionState',
Expand Down
37 changes: 25 additions & 12 deletions api/src/opentrons/hardware_control/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,12 @@
SHAKE_OFF_TIPS_PICKUP_DISTANCE,
DROP_TIP_RELEASE_DISTANCE)
from .execution_manager import ExecutionManager
from .pause_manager import PauseManager
from .types import (Axis, HardwareAPILike, CriticalPoint,
MustHomeError, NoTipAttachedError, DoorState,
DoorStateNotification, PipettePair, TipAttachedError,
HardwareAction, PairedPipetteConfigValueError,
MotionChecks)
MotionChecks, PauseType)
from . import modules, robot_calibration as rb_cal

if TYPE_CHECKING:
Expand Down Expand Up @@ -92,6 +93,7 @@ def __init__(self,
self._motion_lock = asyncio.Lock(loop=self._loop)
self._door_state = DoorState.CLOSED
self._robot_calibration = rb_cal.load()
self._pause_manager = PauseManager(self._door_state)

@property
def robot_calibration(self) -> rb_cal.RobotCalibration:
Expand All @@ -118,9 +120,11 @@ def _update_door_state(self, door_state: DoorState):
mod_log.info(
f'Updating the window switch status: {door_state}')
self.door_state = door_state
self._pause_manager.set_door(self.door_state)
for cb in self._callbacks:
hw_event = DoorStateNotification(
new_state=door_state)
new_state=door_state,
blocking=self._pause_manager.blocked_by_door)
try:
cb(hw_event)
except Exception:
Expand Down Expand Up @@ -332,13 +336,15 @@ async def delay(self, duration_s: float):
""" Delay execution by pausing and sleeping.
"""
await self._wait_for_is_running()
self.pause()
if not self.is_simulator:
async def sleep_for_seconds(seconds: float):
await asyncio.sleep(seconds)
delay_task = self._loop.create_task(sleep_for_seconds(duration_s))
await self._execution_manager.register_cancellable_task(delay_task)
self.resume()
self.pause(PauseType.DELAY)
try:
if not self.is_simulator:
async def sleep_for_seconds(seconds: float):
await asyncio.sleep(seconds)
delay_task = self._loop.create_task(sleep_for_seconds(duration_s))
await self._execution_manager.register_cancellable_task(delay_task)
finally:
self.resume(PauseType.DELAY)

def reset_instrument(self, mount: top_types.Mount = None):
"""
Expand Down Expand Up @@ -522,7 +528,7 @@ async def update_firmware(
explicit_modeset)

# Global actions API
def pause(self):
def pause(self, pause_type: PauseType):
"""
Pause motion of the robot after a current motion concludes.
Expand All @@ -534,6 +540,7 @@ def pause(self):
is paused will not proceed until the system is resumed with
:py:meth:`resume`.
"""
self._pause_manager.pause(pause_type)

async def _chained_calls():
await self._execution_manager.pause()
Expand All @@ -545,12 +552,17 @@ def pause_with_message(self, message):
self._log.warning('Pause with message: {}'.format(message))
for cb in self._callbacks:
cb(message)
self.pause()
self.pause(PauseType.PAUSE)

def resume(self):
def resume(self, pause_type: PauseType):
"""
Resume motion after a call to :py:meth:`pause`.
"""
self._pause_manager.resume(pause_type)

if self._pause_manager.should_pause:
return

# Resume must be called immediately to awaken thread running hardware
# methods (ThreadManager)
self._backend.resume()
Expand Down Expand Up @@ -601,6 +613,7 @@ async def reset(self):
This will re-scan instruments and models, clearing any cached
information about their presence or state.
"""
self._pause_manager.reset()
await self._execution_manager.reset()
self._attached_instruments = {
k: None for k in self._attached_instruments.keys()}
Expand Down
47 changes: 47 additions & 0 deletions api/src/opentrons/hardware_control/pause_manager.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from typing import List

from opentrons.config import feature_flags as ff

from .types import DoorState, PauseType, PauseResumeError


class PauseManager:
""" This class determines whether or not the hardware controller should
pause or resume by evaluating the pause and resume types. The use of two
pause types are used to separate the delay resume (triggered when the delay
timer runs out) and the pause resume (trigged by user via the app).
"""

def __init__(self, door_state: DoorState) -> None:
self.queue: List[PauseType] = []
self._blocked_by_door = self._evaluate_door_state(door_state)

@property
def should_pause(self) -> bool:
return bool(self.queue)

@property
def blocked_by_door(self) -> bool:
return self._blocked_by_door

def _evaluate_door_state(self, door_state: DoorState) -> bool:
if ff.enable_door_safety_switch():
return door_state is DoorState.OPEN
return False

def set_door(self, door_state: DoorState):
self._blocked_by_door = self._evaluate_door_state(door_state)

def resume(self, pause_type: PauseType):
# door should be closed before a resume from the app can be received
if self._blocked_by_door and pause_type is PauseType.PAUSE:
raise PauseResumeError
if pause_type in self.queue:
self.queue.remove(pause_type)

def pause(self, pause_type: PauseType):
if pause_type not in self.queue:
self.queue.append(pause_type)

def reset(self):
self.queue = []
10 changes: 10 additions & 0 deletions api/src/opentrons/hardware_control/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class DoorStateNotification:
event: 'DoorStateNotificationType' = \
HardwareEventType.DOOR_SWITCH_CHANGE
new_state: DoorState = DoorState.CLOSED
blocking: bool = False


# new event types get new dataclasses
Expand Down Expand Up @@ -235,6 +236,15 @@ def __str__(self):
return self.name


class PauseType(enum.Enum):
PAUSE = 0
DELAY = 1


class PauseResumeError(RuntimeError):
pass


class ExecutionCancelledError(RuntimeError):
pass

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from opentrons import types, API
from opentrons.protocols.api_support.types import APIVersion
from opentrons.config import feature_flags as fflags
from opentrons.hardware_control.types import DoorState
from opentrons.hardware_control.types import DoorState, PauseType
from opentrons.hardware_control import SynchronousAdapter
from opentrons.protocols.api_support.definitions import MAX_SUPPORTED_VERSION
from opentrons.protocols.geometry.deck import Deck
Expand Down Expand Up @@ -259,11 +259,11 @@ def get_loaded_instruments(self) -> InstrumentDict:

def pause(self, msg: Optional[str]) -> None:
"""Pause the protocol."""
self._hw_manager.hardware.pause()
self._hw_manager.hardware.pause(PauseType.PAUSE)

def resume(self) -> None:
"""Result the protocol."""
self._hw_manager.hardware.resume()
self._hw_manager.hardware.resume(PauseType.PAUSE)

def comment(self, msg: str) -> None:
"""Add comment to run log."""
Expand Down
9 changes: 9 additions & 0 deletions api/tests/opentrons/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,15 @@ async def old_aspiration(monkeypatch):
'useOldAspirationFunctions', False)


@pytest.fixture
async def enable_door_safety_switch():
await config.advanced_settings.set_adv_setting(
'enableDoorSafetySwitch', True)
yield
await config.advanced_settings.set_adv_setting(
'enableDoorSafetySwitch', False)


@pytest.fixture
async def use_new_calibration(monkeypatch):
await config.advanced_settings.set_adv_setting(
Expand Down
Loading

0 comments on commit 09af36a

Please sign in to comment.