Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Calibration API Implementation #366

Merged
merged 6 commits into from
Oct 3, 2017
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 62 additions & 9 deletions api/opentrons/api/calibration.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,74 @@
from copy import copy

from opentrons.robot.robot import Robot
from opentrons.util import calibration_functions
from opentrons.broker import publish

from .models import Container


VALID_STATES = {'probing', 'moving', 'ready'}


class CalibrationManager:
TOPIC = 'calibration'

def __init__(self, loop=None):
self._loop = loop
self._robot = Robot()

self.state = None

def _set_state(self, state):
if state not in VALID_STATES:
raise ValueError(
'State {0} not in {1}'.format(state, VALID_STATES))
self.state = state
self._on_state_changed()

def tip_probe(self, instrument):
raise NotImplemented()
self._set_state('probing')
calibration_functions.probe_instrument(instrument, self._robot)
self._set_state('ready')

def move_to_front(self, instrument):
# instrument.move_to(PIPETTE_CHANGE_POSITION)
raise NotImplemented()
self._set_state('moving')
calibration_functions.move_instrument_for_probing_prep(
instrument._instrument, self._robot
)
self._set_state('ready')

def move_to(self, instrument, obj):
# instrument.move_to(obj[0])
raise NotImplemented()
if not isinstance(obj, Container):
raise ValueError(
'Invalid object type {0}. Expected models.Container'
.format(type(obj)))

self._set_state('moving')
instrument._instrument.move_to(obj._container[0])
self._set_state('ready')

def jog(self, instrument, coordinates):
# instrument.jog(coordinates)
raise NotImplemented()
self._set_state('moving')
instrument._instrument.jog(coordinates)
self._set_state('ready')

def update_container_offset(self, container, instrument):
self._robot.calibrate_container_with_instrument(
container=container._container,
instrument=instrument._instrument,
save=True
)

# TODO (artyom, 20171003): along with session, consider extracting this
# into abstract base class or find any other way to keep notifications
# consistent across all managers
def _snapshot(self):
return {
'topic': CalibrationManager.TOPIC,
'name': 'state',
'payload': copy(self)
}

def update_container_offset(self):
raise NotImplemented()
def _on_state_changed(self):
publish(CalibrationManager.TOPIC, self._snapshot())
13 changes: 4 additions & 9 deletions api/opentrons/api/models.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,22 @@
class Container:
def __init__(self, container, instruments=None):
instruments = instruments or []
self._container = container

self.id = id(container)
self.name = container.get_name()
self.type = container.get_type()
self.slot = container.parent.get_name()
self.instruments = [
Instrument(instrument)
for instrument in instruments]
self.wells = [Well(well) for well in container]


class Well:
def __init__(self, well):
self.id = id(well)
self.properties = well.properties.copy()
self.coordinates = well.coordinates(reference=well.parent)


class Instrument:
def __init__(self, instrument, containers=None):
containers = containers or []
self._instruments = instrument
self._instrument = instrument

self.id = id(instrument)
self.name = instrument.name
self.channels = instrument.channels
Expand Down
18 changes: 13 additions & 5 deletions api/opentrons/api/routers.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
from opentrons.broker import subscribe, Notifications

from .session import SessionManager, Session
from .calibration import CalibrationManager
from opentrons.broker import subscribe, Notifications


class MainRouter:
def __init__(self, loop=None):
self._notifications = Notifications(loop=loop)

self._unsubscribe = []
self._unsubscribe += [subscribe(
Session.TOPIC,
self._notifications.on_notify)]
self._unsubscribe += [subscribe(
CalibrationManager.TOPIC,
self._notifications.on_notify)]

self.session_manager = SessionManager(loop=loop)
self.calibration_manager = CalibrationManager(loop=loop)
self._unsubscribe = subscribe(
Session.TOPIC,
self._notifications.on_notify)

@property
def notifications(self):
Expand All @@ -20,4 +27,5 @@ def __enter__(self):
return self

def __exit__(self, exc_type, exc_value, traceback):
self._unsubscribe()
for unsubscribe in self._unsubscribe:
unsubscribe()
72 changes: 41 additions & 31 deletions api/opentrons/api/session.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import ast
from copy import copy
from datetime import datetime
from functools import reduce

from .models import Container, Instrument

from opentrons.broker import publish, subscribe
from opentrons.containers import get_container
from opentrons.commands import tree, types
from opentrons import robot
from opentrons.robot.robot import Robot
from opentrons.containers import get_container

from .models import Container, Instrument


VALID_STATES = {'loaded', 'running', 'finished', 'stopped', 'paused'}
Expand Down Expand Up @@ -43,6 +44,9 @@ def __init__(self, name, text):
self._instruments = []
self._interactions = []

self.instruments = None
self.containers = None

self.refresh()

def get_instruments(self):
Expand Down Expand Up @@ -76,6 +80,8 @@ def clear_logs(self):
self.errors.clear()

def _simulate(self):
self._reset()

stack = []
res = []
commands = []
Expand Down Expand Up @@ -105,12 +111,15 @@ def on_command(message):
unsubscribe = subscribe(types.COMMAND, on_command)

try:
self.run()
exec(self._protocol, {})
except Exception as e:
self.error_append(e)
raise e
finally:
unsubscribe()

# Accumulate containers, instruments, interactions from commands
containers, instruments, interactions = _accumulate(
instruments, containers, interactions = _accumulate(
[_get_labware(command) for command in commands])

self._containers.extend(_dedupe(containers))
Expand All @@ -120,7 +129,7 @@ def on_command(message):
return res

def refresh(self):
self.clear_logs()
self._reset()

try:
parsed = ast.parse(self.protocol_text)
Expand All @@ -129,6 +138,10 @@ def refresh(self):
finally:
if self.errors:
raise Exception(*self.errors)

self.containers = self.get_containers()
self.instruments = self.get_instruments()

self.set_state('loaded')
return self

Expand All @@ -147,34 +160,25 @@ def resume(self):
self.set_state('running')
return self

def run(self, devicename=None):
# HACK: hard reset singleton by replacing all of it's attributes
# with the one from a newly constructed robot
robot.__dict__ = {**Robot().__dict__}
self.clear_logs()
_unsubscribe = None

def run(self, devicename):
def on_command(message):
if message['$'] == 'before':
self.log_append()

if devicename is not None:
_unsubscribe = subscribe(types.COMMAND, on_command)
self.set_state('running')
robot.connect(devicename)
self._reset()

_unsubscribe = subscribe(types.COMMAND, on_command)
self.set_state('running')
robot.connect(devicename)

try:
exec(self._protocol, {})
except Exception as e:
self.error_append(e)
raise e
finally:
if _unsubscribe:
_unsubscribe()
# TODO (artyom, 20170927): we should fully separate
# run and simulate code
if devicename is not None:
self.set_state('finished')
_unsubscribe()
self.set_state('finished')
robot.disconnect()

return self
Expand Down Expand Up @@ -204,18 +208,24 @@ def error_append(self, error):
)
self._on_state_changed()

def _reset(self):
# HACK: hard reset singleton by replacing all of it's attributes
# with the one from a newly constructed robot
robot.__dict__ = {**Robot().__dict__}
self.clear_logs()

# TODO (artyom, 20171003): along with calibration, consider extracting this
# into abstract base class or find any other way to keep notifications
# consistent across all managers
def _snapshot(self):
return {
'topic': Session.TOPIC,
'name': 'state',
'payload': {
'name': self.name,
'state': self.state,
'protocol_text': self.protocol_text,
'commands': self.commands.copy(),
'command_log': self.command_log.copy(),
'errors': self.errors.copy()
}
# we are making a copy to avoid the scenario
# when object state is updated elsewhere before
# it is serialized and transferred
'payload': copy(self)

}

def _on_state_changed(self):
Expand Down
1 change: 0 additions & 1 deletion api/opentrons/broker/broker.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def __aiter__(self):

def subscribe(topic, handler):
handlers = subscriptions[topic] = subscriptions.get(topic, [])

if handler in handlers:
return

Expand Down
50 changes: 50 additions & 0 deletions api/tests/opentrons/api/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import asyncio
import pytest
from functools import partial
from opentrons.api import models
from collections import namedtuple


async def wait_until(matcher, notifications, timeout=1, loop=None):
result = []
for coro in iter(notifications.__anext__, None):
done, pending = await asyncio.wait([coro], timeout=timeout)

if pending:
raise TimeoutError('While waiting for {0}'.format(matcher))

result += [done.pop().result()]

if matcher(result[-1]):
return result


@pytest.fixture
def model():
from opentrons import robot, instruments, containers
from opentrons.robot.robot import Robot

robot.__dict__ = {**Robot().__dict__}

pipette = instruments.Pipette(axis='a')
plate = containers.load('96-flat', 'A1')

instrument = models.Instrument(pipette)
container = models.Container(plate)

return namedtuple('model', 'instrument container')(
instrument=instrument,
container=container
)


@pytest.fixture
def main_router(loop):
from opentrons.api.routers import MainRouter

with MainRouter(loop=loop) as router:
router.wait_until = partial(
wait_until,
notifications=router.notifications,
loop=loop)
yield router
Loading