Skip to content

Commit 0e09a39

Browse files
authored
Merge pull request #123 from OpenTrons/motor-driver-updates
Motor driver updates
2 parents 97a50f8 + 685f38f commit 0e09a39

File tree

6 files changed

+184
-64
lines changed

6 files changed

+184
-64
lines changed

opentrons/config/smoothie/smoothie-defaults.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ timeout = 0.1
44

55
[state]
66
head_speed = 3000
7+
plunger_speed = {"a": 300, "b": 300}
78

89
[config]
910
version = v1.2.0

opentrons/drivers/motor.py

Lines changed: 105 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@ class CNCDriver(object):
5454

5555
DISENGAGE_FEEDBACK = 'M63'
5656

57+
RESET = 'reset'
58+
5759
ABSOLUTE_POSITIONING = 'G90'
5860
RELATIVE_POSITIONING = 'G91'
5961

@@ -90,9 +92,9 @@ class CNCDriver(object):
9092
ot_version = None
9193

9294
def __init__(self):
93-
95+
self.halted = Event()
9496
self.stopped = Event()
95-
self.can_move = Event()
97+
self.do_not_pause = Event()
9698
self.resume()
9799
self.current_commands = []
98100

@@ -125,6 +127,12 @@ def _copy_defaults_to_settings(self):
125127
if key not in self.saved_settings[n]:
126128
self.saved_settings[n][key] = val
127129

130+
def _set_step_per_mm_from_config(self):
131+
for axis in 'xyz':
132+
value = self.saved_settings['config'].get(
133+
self.CONFIG_STEPS_PER_MM[axis])
134+
self.set_steps_per_mm(axis, value)
135+
128136
def _apply_settings(self):
129137
self.serial_timeout = float(
130138
self.saved_settings['serial'].get('timeout', 0.1))
@@ -133,6 +141,9 @@ def _apply_settings(self):
133141

134142
self.head_speed = int(
135143
self.saved_settings['state'].get('head_speed', 3000))
144+
self.plunger_speed = json.loads(
145+
self.saved_settings['state'].get(
146+
'plunger_speed', '{"a":300,"b",300}'))
136147

137148
self.COMPATIBLE_FIRMARE = json.loads(
138149
self.saved_settings['versions'].get('firmware', '[]'))
@@ -194,47 +205,56 @@ def get_serial_ports_list(self):
194205
return result
195206

196207
def disconnect(self):
197-
if self.is_connected():
208+
if self.is_connected() and self.connection:
198209
self.connection.close()
199210
self.connection = None
200211

201212
def connect(self, device):
202213
self.connection = device
203-
self.reset_port()
214+
self.toggle_port()
204215
log.debug("Connected to {}".format(device))
216+
217+
self.turn_off_feedback()
205218
self.versions_compatible()
206-
# set the previously saved steps_per_mm values for X and Y
207219
if self.ignore_smoothie_sd:
208-
for axis in 'xyz':
209-
self.set_steps_per_mm(
210-
axis, self.saved_settings['config'].get(
211-
self.CONFIG_STEPS_PER_MM[axis]))
220+
self._set_step_per_mm_from_config()
221+
212222
return self.calm_down()
213223

214224
def is_connected(self):
215225
return self.connection and self.connection.isOpen()
216226

217-
def reset_port(self):
227+
def toggle_port(self):
218228
self.connection.close()
219229
self.connection.open()
220230
self.flush_port()
221231

222-
self.turn_off_feedback()
223-
224232
def pause(self):
225-
self.can_move.clear()
233+
self.halted.clear()
234+
self.stopped.clear()
235+
self.do_not_pause.clear()
226236

227237
def resume(self):
228-
self.can_move.set()
238+
self.halted.clear()
229239
self.stopped.clear()
240+
self.do_not_pause.set()
230241

231242
def stop(self):
243+
self.halted.clear()
232244
self.stopped.set()
233-
self.can_move.set()
245+
self.do_not_pause.set()
246+
247+
def halt(self):
248+
self.halted.set()
249+
self.stopped.set()
250+
self.do_not_pause.set()
234251

235252
def check_paused_stopped(self):
236-
self.can_move.wait()
253+
self.do_not_pause.wait()
237254
if self.stopped.is_set():
255+
if self.halted.is_set():
256+
self.send_command(self.HALT)
257+
self.calm_down()
238258
self.resume()
239259
raise RuntimeWarning(self.STOPPED)
240260

@@ -256,16 +276,27 @@ def send_command(self, command, **kwargs):
256276
return response
257277

258278
def write_to_serial(self, data, max_tries=10, try_interval=0.2):
279+
"""
280+
Sends data string to serial ports
281+
282+
Returns data immediately read from port after write
283+
284+
Raises RuntimeError write fails or connection times out
285+
"""
259286
log.debug("Write: {}".format(str(data).encode()))
260287
if self.is_connected():
261-
self.connection.write(str(data).encode())
288+
try:
289+
self.connection.write(str(data).encode())
290+
except Exception as e:
291+
self.disconnect()
292+
raise RuntimeError('Lost connection with serial port') from e
262293
return self.wait_for_response()
263294
elif self.connection is None:
264295
msg = "No connection found."
265296
log.warn(msg)
266297
raise RuntimeError(msg)
267298
elif max_tries > 0:
268-
self.reset_port()
299+
self.toggle_port()
269300
return self.write_to_serial(
270301
data, max_tries=max_tries - 1, try_interval=try_interval
271302
)
@@ -276,9 +307,15 @@ def write_to_serial(self, data, max_tries=10, try_interval=0.2):
276307
raise RuntimeError(msg)
277308

278309
def wait_for_response(self, timeout=20.0):
310+
"""
311+
Repeatedly reads from serial port until data is received,
312+
or timeout is exceeded
313+
314+
Raises RuntimeWarning() if no response was recieved before timeout
315+
"""
279316
count = 0
280317
max_retries = int(timeout / self.serial_timeout)
281-
while count < max_retries:
318+
while self.is_connected() and count < max_retries:
282319
count = count + 1
283320
out = self.readline_from_serial()
284321
if out:
@@ -292,23 +329,39 @@ def wait_for_response(self, timeout=20.0):
292329
log.debug(
293330
"Waiting {} lines for response.".format(count)
294331
)
295-
raise RuntimeWarning('no response after {} seconds'.format(timeout))
332+
raise RuntimeWarning(
333+
'No response from serial port after {} seconds'.format(timeout))
296334

297335
def flush_port(self):
298336
while self.readline_from_serial():
299337
time.sleep(self.serial_timeout)
300338

301339
def readline_from_serial(self):
340+
"""
341+
Attempt to read a line of data from serial port
342+
343+
Raises RuntimeWarning if read fails on serial port
344+
"""
302345
msg = b''
303-
if self.is_connected():
304-
# serial.readline() returns an empty byte string if it times out
305-
msg = self.connection.readline().strip()
306-
if msg:
307-
log.debug("Read: {}".format(msg))
346+
try:
347+
msg = self.connection.readline()
348+
msg = msg.strip()
349+
except Exception as e:
350+
self.disconnect()
351+
raise RuntimeWarning('Lost connection with serial port') from e
352+
if msg:
353+
log.debug("Read: {}".format(msg))
354+
self.detect_limit_hit(msg) # raises RuntimeWarning if switch hit
355+
356+
return msg
357+
358+
def detect_limit_hit(self, msg):
359+
"""
360+
Detect if it hit a home switch
308361
309-
# detect if it hit a home switch
362+
Raises RuntimeWarning if Smoothie reports a limit hit
363+
"""
310364
if b'!!' in msg or b'limit' in msg:
311-
# TODO (andy): allow this to bubble up so UI is notified
312365
log.debug('home switch hit')
313366
self.flush_port()
314367
self.calm_down()
@@ -319,8 +372,6 @@ def readline_from_serial(self):
319372
axis = ax
320373
raise RuntimeWarning('{} limit switch hit'.format(axis.upper()))
321374

322-
return msg
323-
324375
def set_coordinate_system(self, mode):
325376
if mode == 'absolute':
326377
self.send_command(self.ABSOLUTE_POSITIONING)
@@ -329,22 +380,10 @@ def set_coordinate_system(self, mode):
329380
else:
330381
raise ValueError('Invalid coordinate mode: ' + mode)
331382

332-
def move_plunger(self, mode='absolute', **kwargs):
333-
383+
def move(self, mode='absolute', **kwargs):
334384
self.set_coordinate_system(mode)
335385

336-
args = {axis.upper(): kwargs.get(axis)
337-
for axis in 'ab'
338-
if axis in kwargs}
339-
340-
return self.consume_move_commands(args)
341-
342-
def move_head(self, mode='absolute', **kwargs):
343-
344-
self.set_coordinate_system(mode)
345-
self.set_head_speed()
346386
current = self.get_head_position()['target']
347-
348387
log.debug('Current Head Position: {}'.format(current))
349388
target_point = {
350389
axis: kwargs.get(
@@ -355,20 +394,30 @@ def move_head(self, mode='absolute', **kwargs):
355394
}
356395
log.debug('Destination: {}'.format(target_point))
357396

358-
target_vector = Vector(target_point)
397+
flipped_vector = self.flip_coordinates(
398+
Vector(target_point), mode)
399+
for axis in 'xyz':
400+
kwargs[axis] = flipped_vector[axis]
359401

360-
flipped_vector = self.flip_coordinates(target_vector, mode)
361-
args = (
362-
{axis.upper(): flipped_vector[axis]
363-
for axis in 'xyz' if axis in kwargs}
364-
)
402+
args = {axis.upper(): kwargs.get(axis)
403+
for axis in 'xyzab'
404+
if axis in kwargs}
405+
args.update({"F": self.head_speed})
406+
args.update({"a": self.plunger_speed['a']})
407+
args.update({"b": self.plunger_speed['b']})
365408

366409
return self.consume_move_commands(args)
367410

411+
def move_plunger(self, mode='absolute', **kwargs):
412+
return self.move(mode, **kwargs)
413+
414+
def move_head(self, mode='absolute', **kwargs):
415+
return self.move(mode, **kwargs)
416+
368417
def consume_move_commands(self, args):
369418
self.check_paused_stopped()
370419

371-
log.debug("Moving head: {}".format(args))
420+
log.debug("Moving : {}".format(args))
372421
res = self.send_command(self.MOVE, **args)
373422
if res != b'ok':
374423
return (False, self.SMOOTHIE_ERROR)
@@ -399,6 +448,7 @@ def wait_for_arrival(self, tolerance=0.1):
399448
arrived = False
400449
coords = self.get_position()
401450
while not arrived:
451+
self.check_paused_stopped()
402452
coords = self.get_position()
403453
diff = {}
404454
for axis in coords.get('target', {}):
@@ -461,6 +511,11 @@ def calm_down(self):
461511
res = self.send_command(self.CALM_DOWN)
462512
return res == b'ok'
463513

514+
def reset(self):
515+
res = self.send_command(self.RESET)
516+
if b'Rebooting' in res:
517+
self.disconnect()
518+
464519
def set_position(self, **kwargs):
465520
uppercase_args = {}
466521
for key in kwargs:
@@ -533,9 +588,7 @@ def set_head_speed(self, rate=None):
533588
def set_plunger_speed(self, rate, axis):
534589
if axis.lower() not in 'ab':
535590
raise ValueError('Axis {} not supported'.format(axis))
536-
kwargs = {axis.lower(): rate}
537-
res = self.send_command(self.SET_SPEED, **kwargs)
538-
return res == b'ok'
591+
self.plunger_speed[axis] = rate
539592

540593
def versions_compatible(self):
541594
self.get_ot_version()

opentrons/drivers/virtual_smoothie.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,10 @@ def process_get_position(self, arguments):
150150
def process_calm_down(self, arguments):
151151
return 'ok'
152152

153+
def process_halt(self, arguments):
154+
e = 'ok Emergency Stop Requested - reset or M999 required to continue'
155+
return e
156+
153157
def process_absolute_positioning(self, arguments):
154158
self.absolute = True
155159
return 'ok'
@@ -161,6 +165,9 @@ def process_relative_positioning(self, arguments):
161165
def process_version(self, arguments):
162166
return '{"version":' + self.version + '}'
163167

168+
def process_reset(self, arguments):
169+
return 'Smoothie out. Peace. Rebooting in 5 seconds...'
170+
164171
def process_config_get(self, arguments):
165172
folder = arguments[0]
166173
setting = arguments[1]
@@ -222,6 +229,7 @@ def process_command(self, command):
222229
'M119': self.process_get_endstops,
223230
'M92': self.process_steps_per_mm,
224231
'M999': self.process_calm_down,
232+
'M112': self.process_halt,
225233
'M63': self.process_disengage_feedback,
226234
'G90': self.process_absolute_positioning,
227235
'G91': self.process_relative_positioning,
@@ -240,6 +248,7 @@ def process_command(self, command):
240248
'M17': self.process_power_on,
241249
'M18': self.process_power_off,
242250
'version': self.process_version,
251+
'reset': self.process_reset,
243252
'config-get': self.process_config_get,
244253
'config-set': self.process_config_set
245254
}
@@ -259,12 +268,16 @@ def process_command(self, command):
259268
'Command {} is not supported'.format(command))
260269

261270
def write(self, data):
271+
if not self.isOpen():
272+
raise Exception('Virtual Smoothie no currently connected')
262273
if not isinstance(data, str):
263274
data = data.decode('utf-8')
264275
# make it async later
265276
self.process_command(data)
266277

267278
def readline(self):
279+
if not self.isOpen():
280+
raise Exception('Virtual Smoothie no currently connected')
268281
if len(self.responses) > 0:
269282
return self.responses.pop().encode('utf-8')
270283
else:

0 commit comments

Comments
 (0)