Skip to content

Commit

Permalink
feat(api): Add G Code for pipette config in driver (#3388)
Browse files Browse the repository at this point in the history
  • Loading branch information
Laura-Danielle authored May 2, 2019
1 parent f4feee9 commit 77fffa6
Show file tree
Hide file tree
Showing 17 changed files with 23,265 additions and 22,932 deletions.
20 changes: 15 additions & 5 deletions api/src/opentrons/config/robot_configs.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,14 @@
'C': C_ACCELERATION
}

DEFAULT_STEPS_PER_MM = 'M92 X80.00 Y80.00 Z400 A400 B768 C768'
DEFAULT_STEPS_PER_MM: Dict[str, float] = {
'X': 80.00,
'Y': 80.00,
'Z': 400,
'A': 400,
'B': 768,
'C': 768
}
# This probe height is ~73 from deck to the top surface of the switch body
# per CAD; 74.3mm is the nominal for engagement from the switch drawing.
# Note that this has a piece-to-piece tolerance stackup of +-1.5mm
Expand Down Expand Up @@ -242,9 +249,10 @@ def _build_tip_probe(tip_probe_settings: dict) -> tip_probe_config:
)


def _build_acceleration(from_conf: Union[Dict, str, None]) -> Dict[str, float]:
def _build_conf_dict(
from_conf: Union[Dict, str, None], default) -> Dict[str, float]:
if not from_conf or isinstance(from_conf, str):
return DEFAULT_ACCELERATION
return default
else:
return from_conf

Expand All @@ -254,8 +262,10 @@ def _build_config(deck_cal: List[List[float]],
cfg = robot_config(
name=robot_settings.get('name', 'Ada Lovelace'),
version=int(robot_settings.get('version', ROBOT_CONFIG_VERSION)),
steps_per_mm=robot_settings.get('steps_per_mm', DEFAULT_STEPS_PER_MM),
acceleration=_build_acceleration(robot_settings.get('acceleration')),
steps_per_mm=_build_conf_dict(
robot_settings.get('steps_per_mm'), DEFAULT_STEPS_PER_MM),
acceleration=_build_conf_dict(
robot_settings.get('acceleration'), DEFAULT_ACCELERATION),
gantry_calibration=deck_cal or DEFAULT_DECK_CALIBRATION,
instrument_offset=build_fallback_instrument_offset(robot_settings),
tip_length=robot_settings.get('tip_length', DEFAULT_TIP_LENGTH_DICT),
Expand Down
39 changes: 39 additions & 0 deletions api/src/opentrons/drivers/smoothie_drivers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,42 @@ class VirtualSmoothie(object):

def __init__(self):
pass


class SimulatingDriver:
def __init__(self):
self._steps_per_mm = {}

def home(self):
pass

def update_pipette_config(self, axis, data):
'''
Updates the following configs for a given pipette mount based on
the detected pipette type:
- homing positions M365.0
- Max Travel M365.1
- endstop debounce M365.2 (NOT for zprobe debounce)
- retract from endstop distance M365.3
'''
pass

@property
def current(self):
return self._current_settings

@property
def speed(self):
pass

@property
def steps_per_mm(self):
return self._steps_per_mm

@steps_per_mm.setter
def steps_per_mm(self, axis, mm):
# Keep track of any updates to the steps per mm per axis
self._steps_per_mm[axis] = mm

def update_steps_per_mm(self, data):
pass
84 changes: 80 additions & 4 deletions api/src/opentrons/drivers/smoothie_drivers/driver_3_0.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@
'C': 19
}

EEPROM_DEFAULT = {
'X': 0.0,
'Y': 0.0,
'Z': 0.0,
'A': 0.0,
'B': 0.0,
'C': 0.0
}

PLUNGER_BACKLASH_MM = 0.3
LOW_CURRENT_Z_SPEED = 30
CURRENT_CHANGE_DELAY = 0.005
Expand Down Expand Up @@ -79,6 +88,7 @@
'PUSH_SPEED': 'M120',
'POP_SPEED': 'M121',
'SET_SPEED': 'G0F',
'STEPS_PER_MM': 'M92',
'READ_INSTRUMENT_ID': 'M369',
'WRITE_INSTRUMENT_ID': 'M370',
'READ_INSTRUMENT_MODEL': 'M371',
Expand Down Expand Up @@ -254,6 +264,7 @@ class SmoothieDriver_3_0_0:
def __init__(self, config):
self.run_flag = Event()
self.run_flag.set()
self.dist_from_eeprom = EEPROM_DEFAULT.copy()

self._position = HOMED_POSITION.copy()
self.log = []
Expand Down Expand Up @@ -302,7 +313,7 @@ def __init__(self, config):
self._saved_max_speed_settings = self._max_speed_settings.copy()
self._combined_speed = float(DEFAULT_AXES_SPEED)
self._saved_axes_speed = float(self._combined_speed)

self._steps_per_mm = {}
self._acceleration = config.acceleration.copy()
self._saved_acceleration = config.acceleration.copy()

Expand Down Expand Up @@ -432,6 +443,44 @@ def write_pipette_model(self, mount, data_string):
self._write_to_pipette(
GCODES['WRITE_INSTRUMENT_MODEL'], mount, data_string)

def update_pipette_config(self, axis, data):
'''
Updates the following configs for a given pipette mount based on
the detected pipette type:
- homing positions M365.0
- Max Travel M365.1
- endstop debounce M365.2 (NOT for zprobe debounce)
- retract from endstop distance M365.3
'''
if self.simulating:
return {axis: data}

gcodes = {
'retract': 'M365.3',
'debounce': 'M365.2',
'max_travel': 'M365.1',
'home': 'M365.0'}

res_msg = {axis: {}}

for key, value in data.items():
if key == 'debounce':
# debounce variable for all axes, so do not specify an axis
cmd = f' O{value}'
else:
cmd = f' {axis}{value}'
res = self._send_command(gcodes[key] + cmd)
if res is None:
raise ValueError(
f'{key} was not updated to {value} on {axis} axis')
# ensure smoothie received code and changed value through
# return message. Format of return message:
# <Axis> (or E for endstop) updated <Value>
arr_result = res.strip().split(' ')
res_msg[axis][str(arr_result[0])] = float(arr_result[2])

return res_msg

# FIXME (JG 9/28/17): Should have a more thought out
# way of simulating vs really running
def connect(self, port=None):
Expand Down Expand Up @@ -566,6 +615,10 @@ def current(self):
def speed(self):
pass

@property
def steps_per_mm(self):
return self._steps_per_mm

def set_speed(self, value):
''' set total axes movement speed in mm/second'''
self._combined_speed = float(value)
Expand Down Expand Up @@ -995,7 +1048,7 @@ def _setup(self):
log.debug("wait for ack done")
self._reset_from_error()
log.debug("_reset")
self._send_command(self._config.steps_per_mm)
self.update_steps_per_mm(self._config.steps_per_mm)
log.debug("sent steps")
self._send_command(GCODES['ABSOLUTE_COORDS'])
log.debug("sent abs")
Expand All @@ -1007,6 +1060,24 @@ def _setup(self):
self.pop_acceleration()
log.debug("setup done")

def update_steps_per_mm(self, data):
# Using M92, update steps per mm for a given axis
if self.simulating:
self.steps_per_mm.update(data)
return

if isinstance(data, str):
# Unfortunately update server calls driver._setup() before the
# update can correctly load the robot_config change on disk.
# Need to account for old command format to avoid this issue.
self._send_command(data)
else:
cmd = ''
for axis, value in data.items():
cmd = f'{cmd} {axis}{value}'
self.steps_per_mm[axis] = value
self._send_command(GCODES['STEPS_PER_MM'] + cmd)

def _read_from_pipette(self, gcode, mount) -> Optional[str]:
'''
Read from an attached pipette's internal memory. The gcode used
Expand Down Expand Up @@ -1227,6 +1298,7 @@ def home(self, axis=AXES, disabled=DISABLE_AXES):
ax: self.homed_position.get(ax)
for ax in ''.join(home_sequence)
}
log.info(f'Home before update pos {homed}')
self.update_position(default=homed)
for axis in ''.join(home_sequence):
self.engaged_axes[axis] = True
Expand All @@ -1239,6 +1311,7 @@ def home(self, axis=AXES, disabled=DISABLE_AXES):
if ax in axis
}
self._homed_position.update(new)
log.info(f'Homed position after {new}')

return self.position

Expand Down Expand Up @@ -1450,7 +1523,7 @@ def _smoothie_hard_halt(self):
gpio.set_high(gpio.OUTPUT_PINS['HALT'])
sleep(0.25)

async def update_firmware(self,
async def update_firmware(self, # noqa(C901)
filename: str,
loop: asyncio.AbstractEventLoop = None,
explicit_modeset: bool = True) -> str:
Expand Down Expand Up @@ -1499,7 +1572,10 @@ async def update_firmware(self,
rd: bytes = await proc.stdout.read() # type: ignore
res = rd.decode().strip()
await proc.communicate()

try:
self._connection.close()
except Exception:
log.exception('Failed to close smoothie connection.')
# re-open the port
self._connection.open()
# reset smoothieware
Expand Down
23 changes: 23 additions & 0 deletions api/src/opentrons/hardware_control/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from opentrons.util import linal
from .simulator import Simulator
from opentrons.config import robot_configs
from opentrons.config.robot_configs import DEFAULT_STEPS_PER_MM
from .pipette import Pipette
try:
from .controller import Controller
Expand Down Expand Up @@ -267,6 +268,28 @@ async def cache_instruments(self,
self._config.instrument_offset[mount.name.lower()],
instrument_data['id'])
self._attached_instruments[mount] = p
mount_axis = Axis.by_mount(mount)
plunger_axis = Axis.of_plunger(mount)
if 'v2' in model:
# Check if new model of pipettes, load smoothie configs
# for this particular model
self._backend._smoothie_driver.update_steps_per_mm(
{plunger_axis.name: 2133.33})
# TODO(LC25-4-2019): Modify configs to update to as
# testing informs better values
self._backend._smoothie_driver.update_pipette_config(
mount_axis.name, {'home': 172.15})
self._backend._smoothie_driver.update_pipette_config(
plunger_axis.name, {'max_travel': 60})
else:
self._backend._smoothie_driver.update_steps_per_mm(
{plunger_axis.name: DEFAULT_STEPS_PER_MM[
plunger_axis.name]})

self._backend._smoothie_driver.update_pipette_config(
mount_axis.name, {'home': 220})
self._backend._smoothie_driver.update_pipette_config(
plunger_axis.name, {'max_travel': 30})
else:
self._attached_instruments[mount] = None
mod_log.info("Instruments found: {}".format(
Expand Down
4 changes: 4 additions & 0 deletions api/src/opentrons/hardware_control/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from contextlib import contextmanager
from opentrons import types
from opentrons.config.pipette_config import config_models
from opentrons.drivers.smoothie_drivers import SimulatingDriver
from . import modules


Expand Down Expand Up @@ -81,6 +82,9 @@ def __init__(
self._position = copy.copy(_HOME_POSITION)
# Engaged axes start all true in smoothie for some reason so we
# imitate that here
# TODO(LC2642019) Create a simulating driver for smoothie instead of
# using a flag
self._smoothie_driver = SimulatingDriver()
self._engaged_axes = {ax: True for ax in _HOME_POSITION}
self._lights = {'button': False, 'rails': False}
self._run_flag = Event()
Expand Down
2 changes: 2 additions & 0 deletions api/src/opentrons/legacy_api/instruments/pipette.py
Original file line number Diff line number Diff line change
Expand Up @@ -1818,11 +1818,13 @@ def _remove_tip(self, length):
def _max_deck_height(self):
mount_max_height = self.instrument_mover.axis_maximum(
self.robot.poses, 'z')
log.info(f'Mount Max Height {mount_max_height}')
_, _, pipette_max_height = pose_tracker.change_base(
self.robot.poses,
src=self,
dst=self.mount,
point=(0, 0, mount_max_height))
log.info(f'Coordinate transform {pipette_max_height}')
return pipette_max_height

@property
Expand Down
32 changes: 18 additions & 14 deletions api/src/opentrons/legacy_api/robot/mover.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
from opentrons.trackers.pose_tracker import Point, change_base, update, ROOT
import logging


log = logging.getLogger(__name__)


class Mover:
Expand Down Expand Up @@ -68,6 +72,8 @@ def defaults(_x, _y, _z):
return update(pose_tree, self, Point(*defaults(dst_x, dst_y, dst_z)))

def home(self, pose_tree):
log.info(f'Pose tree in home transform {pose_tree}')
log.info(f'Axis mapping {self._axis_mapping.values()}')
self._driver.home(axis=''.join(self._axis_mapping.values()))
return self.update_pose_from_driver(pose_tree)

Expand Down Expand Up @@ -137,19 +143,17 @@ def axis_maximum(self, pose_tree, axis):
assert axis in 'xyz', "axis value should be x, y or z"
assert axis in self._axis_mapping, "mapping is not set for " + axis

# change_base is computationally expensive
# so only run if max pos is not known
if self._axis_maximum[axis] is None:
d_axis_max = self._driver.homed_position[self._axis_mapping[axis]]
d_point = {'x': 0, 'y': 0, 'z': 0}
d_point[axis] = d_axis_max
x, y, z = change_base(
pose_tree,
src=self._dst,
dst=self._src,
point=Point(**d_point))
point = {'x': x, 'y': y, 'z': z}
self._axis_maximum[axis] = point[axis]
d_axis_max = self._driver.homed_position[self._axis_mapping[axis]]
log.info(f"Axis max from driver: {d_axis_max}")
d_point = {'x': 0, 'y': 0, 'z': 0}
d_point[axis] = d_axis_max
x, y, z = change_base(
pose_tree,
src=self._dst,
dst=self._src,
point=Point(**d_point))
point = {'x': x, 'y': y, 'z': z}
self._axis_maximum[axis] = point[axis]
return self._axis_maximum[axis]

def update_pose_from_driver(self, pose_tree):
Expand All @@ -160,5 +164,5 @@ def update_pose_from_driver(self, pose_tree):
y=self._driver.position.get(self._axis_mapping.get('y', ''), 0.0),
z=self._driver.position.get(self._axis_mapping.get('z', ''), 0.0)
)

log.info(f'Point in update pose from driver {point}')
return update(pose_tree, self, point)
Loading

0 comments on commit 77fffa6

Please sign in to comment.