forked from NicholsSchool/2022-EV3-Lego-Robotic-Arm
-
Notifications
You must be signed in to change notification settings - Fork 1
/
remote_control.py
518 lines (432 loc) · 17.4 KB
/
remote_control.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
#!/usr/bin/env python3
# ev3-robot-arm 6dof, originally by Nino Guba.
# v2 improved by Marno van der Molen;
# - bugfixes
# - don't require grabber attachment to run
# - more debug output for troubleshooting
# - improved gamepad responsiveness
# - proportional control for some motors
# - code cleanup / simplify
# v2.1 refinements by Marno van der Molen:
# - simlify code
# - allow changing speed of movement by holding d-pad up/down
# - optionally support a color sensor to align waist by pressing d-pad left/right
# v2.2 minor improvements by Marno van der Molen
# - maintain grabber grip during spin
# - increase joystick deadzone a bit to prevent unintended movement while pressing L3/R3
# - start work on calibration support using touch sensors
# - prevent calculate_speed() returning values over 100 which causes exceptions
__author__ = 'Nino Guba'
import logging
import os
import sys
import threading
import time
import evdev
import rpyc
from signal import signal, SIGINT
from ev3dev2 import DeviceNotFound
from ev3dev2.led import Leds
from ev3dev2.sensor import INPUT_1, INPUT_2, INPUT_3, INPUT_4
from ev3dev2.sensor.lego import ColorSensor, TouchSensor
from ev3dev2.motor import OUTPUT_A, OUTPUT_B, OUTPUT_C, OUTPUT_D, LargeMotor, MoveTank
from ev3dev2.power import PowerSupply
# from ev3dev2.sound import Sound
from evdev import InputDevice
from math_helper import scale_stick
# Config
REMOTE_HOST = '10.42.0.3'
JOYSTICK_DEADZONE = 20
# Define speeds
FULL_SPEED = 100
FAST_SPEED = 75
NORMAL_SPEED = 50
SLOW_SPEED = 25
VERY_SLOW_SPEED = 10
# Setup logging
os.system('setfont Lat7-Terminus12x6')
logging.basicConfig(level=logging.INFO, stream=sys.stdout,
format='%(message)s')
logger = logging.getLogger(__name__)
def reset_motors():
""" reset motor positions to default """
logger.info("Resetting motors...")
waist_motor.reset()
shoulder_motors.reset()
elbow_motor.reset()
roll_motor.reset()
pitch_motor.reset()
spin_motor.reset()
if grabber_motor:
grabber_motor.reset()
# Initial setup
# RPyC
# Setup on slave EV3: https://ev3dev-lang.readthedocs.io/projects/python-ev3dev/en/stable/rpyc.html
# Create a RPyC connection to the remote ev3dev device.
# Use the hostname or IP address of the ev3dev device.
# If this fails, verify your IP connectivty via ``ping X.X.X.X``
logger.info("Connecting RPyC to {}...".format(REMOTE_HOST))
# change this IP address for your slave EV3 brick
conn = rpyc.classic.connect(REMOTE_HOST)
# remote_ev3 = conn.modules['ev3dev.ev3']
remote_power_mod = conn.modules['ev3dev2.power']
remote_motor = conn.modules['ev3dev2.motor']
remote_led = conn.modules['ev3dev2.led']
logger.info("RPyC started succesfully")
# Gamepad
# If bluetooth is not available, check https://github.com/ev3dev/ev3dev/issues/1314
logger.info("Connecting wireless controller...")
gamepad = InputDevice(evdev.list_devices()[0])
if gamepad.name != 'Wireless Controller':
logger.error('Failed to connect to wireless controller')
sys.exit(1)
# LEDs
leds = Leds()
remote_leds = remote_led.Leds()
# Power
power = PowerSupply(name_pattern='*ev3*')
remote_power = remote_power_mod.PowerSupply(name_pattern='*ev3*')
# Sound
# sound = Sound()
# Primary EV3
# Sensors
try:
color_sensor = ColorSensor(INPUT_1)
color_sensor.mode = ColorSensor.MODE_COL_COLOR
logger.info("Color sensor detected!")
except DeviceNotFound:
logger.info("Color sensor not detected (primary EV3, input 1) - running without it...")
color_sensor = False
try:
shoulder_touch = TouchSensor(INPUT_3)
logger.info("Shoulder touch sensor detected!")
except DeviceNotFound:
logger.info("Shoulder touch sensor not detected (primary EV3, input 3) - running without it...")
shoulder_touch = False
try:
elbow_touch = TouchSensor(INPUT_4)
logger.info("Elbow touch sensor detected!")
except DeviceNotFound:
logger.info("Elbow touch sensor not detected (primary EV3, input 4) - running without it...")
elbow_touch = False
# Motors
waist_motor = LargeMotor(OUTPUT_A)
shoulder_motors = MoveTank(OUTPUT_B, OUTPUT_C)
elbow_motor = LargeMotor(OUTPUT_D)
# Secondary EV3
# Motors
roll_motor = remote_motor.MediumMotor(remote_motor.OUTPUT_A)
pitch_motor = remote_motor.MediumMotor(remote_motor.OUTPUT_B)
pitch_motor.stop_action = remote_motor.MediumMotor.STOP_ACTION_COAST
spin_motor = remote_motor.MediumMotor(remote_motor.OUTPUT_C)
try:
grabber_motor = remote_motor.MediumMotor(remote_motor.OUTPUT_D)
grabber_motor.stop_action = remote_motor.MediumMotor.STOP_ACTION_COAST
logger.info("Grabber motor detected!")
except DeviceNotFound:
logger.info("Grabber motor not detected (secondary EV3, port D) - running without it...")
grabber_motor = False
# Not sure why but resetting all motors before doing anything else seems to improve reliability
reset_motors()
# Variables for stick input
shoulder_speed = 0
elbow_speed = 0
# Variables for button input
waist_left = False
waist_right = False
roll_left = False
roll_right = False
pitch_up = False
pitch_down = False
spin_left = False
spin_right = False
grabber_open = False
grabber_close = False
# We are running!
running = True
def log_power_info():
logger.info('Local battery power: {}V / {}A'.format(round(power.measured_volts,2 ), round(power.measured_amps, 2)))
logger.info('Remote battery power: {}V / {}A'.format(round(remote_power.measured_volts, 2), round(remote_power.measured_amps, 2)))
speed_modifier = 0
def calculate_speed(speed, max=100):
if speed_modifier == 0:
return min(speed, max)
elif speed_modifier == -1: # dpad up
return min(speed * 1.5, max)
elif speed_modifier == 1: # dpad down
return min(speed / 1.5, max)
waist_target_color = 0
aligning_waist = False
def align_waist_to_color(waist_target_color):
if waist_target_color == -1:
target_color = ColorSensor.COLOR_RED
elif waist_target_color == 1:
target_color = ColorSensor.COLOR_BLUE
else:
# if someone asks us to move to an unknown/unmapped
# color, just make this a noop.
return
# Set a flag for the MotorThread to prevent stopping the waist motor while
# we're trying to align it
global aligning_waist
aligning_waist = True
# If we're not on the correct color, start moving but make sure there's a
# timeout to prevent trying forever.
if color_sensor.color != target_color:
logger.info('Moving to color {}...'.format(target_color))
waist_motor.on(NORMAL_SPEED)
max_iterations = 100
iterations = 0
while color_sensor.color != target_color:
# wait a bit between checks. Ideally there would be a wait_for_color()
# method or something, but as far as I know that's not possible with the
# current libraries, so we do it like this.
time.sleep(0.1)
# prevent running forver
iterations += 1
if iterations >= max_iterations:
logger.info('Failed to align waist to requested color {}'.format(target_color))
break
# we're either aligned or reached a timeout. Stop moving.
waist_motor.stop()
# update flag for MotorThead so waist control works again.
aligning_waist = False
def clean_shutdown(signal_received=None, frame=None):
""" make sure all motors are stopped when stopping this script """
logger.info('Shutting down...')
global running
running = False
logger.info('waist..')
waist_motor.stop()
logger.info('shoulder..')
shoulder_motors.stop()
logger.info('elbow..')
elbow_motor.stop()
logger.info('pitch..')
# For some reason the pitch motor sometimes gets stuck here, and a reset helps?
# pitch_motor.reset()
pitch_motor.stop()
logger.info('roll..')
roll_motor.stop()
logger.info('spin..')
spin_motor.stop()
if grabber_motor:
logger.info('grabber..')
grabber_motor.stop()
# See https://github.com/gvalkov/python-evdev/issues/19 if this raises exceptions, but it seems
# stable now.
gamepad.close()
logger.info('Shutdown completed.')
sys.exit(0)
class WaistAlignThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
logger.info("WaistAlignThread running!")
while running:
if waist_target_color != 0 and not aligning_waist:
align_waist_to_color(waist_target_color)
time.sleep(2) # prevent performance impact, drawback is you need to hold the button for a bit before it registers
logger.info("WaistAlignThread stopping!")
class MotorThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
logger.info("MotorThread running!")
# os.system('setfont Lat7-Terminus12x6')
leds.set_color("LEFT", "BLACK")
leds.set_color("RIGHT", "BLACK")
remote_leds.set_color("LEFT", "BLACK")
remote_leds.set_color("RIGHT", "BLACK")
# sound.play_song((('C4', 'e'), ('D4', 'e'), ('E5', 'q')))
leds.set_color("LEFT", "GREEN")
leds.set_color("RIGHT", "GREEN")
remote_leds.set_color("LEFT", "GREEN")
remote_leds.set_color("RIGHT", "GREEN")
logger.info("Starting main loop...")
while running:
# Proportional control
if shoulder_speed != 0:
shoulder_motors.on(shoulder_speed, shoulder_speed)
elif shoulder_motors.is_running:
shoulder_motors.stop()
# Proportional control
if elbow_speed != 0:
elbow_motor.on(elbow_speed)
elif elbow_motor.is_running:
elbow_motor.stop()
# on/off control
if not aligning_waist:
if waist_left:
waist_motor.on(calculate_speed(-SLOW_SPEED))
elif waist_right:
waist_motor.on(calculate_speed(SLOW_SPEED))
elif waist_motor.is_running:
waist_motor.stop()
# on/off control
if roll_left:
roll_motor.on(calculate_speed(-SLOW_SPEED))
elif roll_right:
roll_motor.on(calculate_speed(SLOW_SPEED))
elif roll_motor.is_running:
roll_motor.stop()
# on/off control
#
# Pitch affects grabber as well, but to a lesser degree. We could improve this
# in the future to adjust grabber based on pitch movement as well.
if pitch_up:
pitch_motor.on(calculate_speed(VERY_SLOW_SPEED))
elif pitch_down:
pitch_motor.on(calculate_speed(-VERY_SLOW_SPEED))
elif pitch_motor.is_running:
pitch_motor.stop()
# on/off control
#
# If we keep spinning, the grabber motor can get stuck because it remains stationary
# but is forced to move around the worm gear. We need to adjust it while spinning.
#
# spin motor: 7:1 (=23.6RPM)
# grabber motor: 1:1 (=165RPM) untill the worm gear which we need to keep steady
#
# So, I think the grabber_motor needs to move 7 times slower than the spin_motor
# to maintain it's position.
#
# NOTE: I'm using knob wheels to control the grabber, which is not smoothly rotating
# at these low speeds. Therefor the grabber has to move a bit quicker for me, but I
# think when using regular gears the 7 ratio should be sufficient.
# NOTE: Yes, with regular gears the calculated ratio is correct!
GRABBER_SPIN_RATIO = 7
if spin_left:
spin_motor_speed = calculate_speed(-SLOW_SPEED)
spin_motor.on(spin_motor_speed)
if grabber_motor:
# determine grabber_motor speed based on spin_motor speed & invert
grabber_spin_sync_speed = (spin_motor_speed / GRABBER_SPIN_RATIO) * -1
grabber_motor.on(grabber_spin_sync_speed, False)
# logger.info('Spin motor {}, grabber {}'.format(spin_motor_speed, grabber_spin_sync_speed))
elif spin_right:
spin_motor_speed = calculate_speed(SLOW_SPEED)
spin_motor.on(spin_motor_speed)
if grabber_motor:
# determine grabber_motor speed based on spin_motor speed & invert
grabber_spin_sync_speed = (spin_motor_speed / GRABBER_SPIN_RATIO) * -1
grabber_motor.on(grabber_spin_sync_speed, False)
# logger.info('Spin motor {}, grabber {}'.format(spin_motor_speed, grabber_spin_sync_speed))
elif spin_motor.is_running:
spin_motor.stop()
if grabber_motor:
grabber_motor.stop()
# on/off control - can only control this directly if we're not currently spinning
elif grabber_motor:
if grabber_open:
grabber_motor.on(calculate_speed(NORMAL_SPEED), False)
elif grabber_close:
grabber_motor.on(calculate_speed(-NORMAL_SPEED), False)
elif grabber_motor.is_running:
grabber_motor.stop()
logger.info("MotorThread stopping!")
# Ensure clean shutdown on CTRL+C
signal(SIGINT, clean_shutdown)
log_power_info()
# Main motor control thread
motor_thread = MotorThread()
motor_thread.setDaemon(True)
motor_thread.start()
# We only need the WaistAlignThread if we detected a color sensor
if color_sensor:
waist_align_thread = WaistAlignThread()
waist_align_thread.setDaemon(True)
waist_align_thread.start()
# Handle gamepad input
for event in gamepad.read_loop(): # this loops infinitely
if event.type == 3: # stick input
if event.code == 0: # Left stick X-axis
shoulder_speed = scale_stick(event.value, deadzone=JOYSTICK_DEADZONE, invert=True)
elif event.code == 3: # Right stick X-axis
elbow_speed = scale_stick(event.value, deadzone=JOYSTICK_DEADZONE)
elif event.code == 17: # dpad up/down
speed_modifier = event.value
elif event.code == 16: # dpad left/right
waist_target_color = event.value
elif event.type == 1: # button input
if event.code == 310: # L1
if event.value == 1:
waist_right = False
waist_left = True
elif event.value == 0:
waist_left = False
elif event.code == 311: # R1
if event.value == 1:
waist_left = False
waist_right = True
elif event.value == 0:
waist_right = False
elif event.code == 308: # Square
if event.value == 1:
roll_right = False
roll_left = True
elif event.value == 0:
roll_left = False
elif event.code == 305: # Circle
if event.value == 1:
roll_left = False
roll_right = True
elif event.value == 0:
roll_right = False
elif event.code == 307: # Triangle
if event.value == 1:
pitch_down = False
pitch_up = True
elif event.value == 0:
pitch_up = False
elif event.code == 304: # X
if event.value == 1:
pitch_up = False
pitch_down = True
elif event.value == 0:
pitch_down = False
elif event.code == 312: # L2
if event.value == 1:
spin_right = False
spin_left = True
elif event.value == 0:
spin_left = False
elif event.code == 313: # R2
if event.value == 1:
spin_left = False
spin_right = True
elif event.value == 0:
spin_right = False
elif event.code == 317: # L3
if event.value == 1:
grabber_close = False
grabber_open = True
elif event.value == 0:
grabber_open = False
elif event.code == 318: # R3
if event.value == 1:
grabber_open = False
grabber_close = True
elif event.value == 0:
grabber_close = False
elif event.code == 314 and event.value == 1: # Share
# debug info
log_power_info()
elif event.code == 315 and event.value == 1: # Options
# debug info
logger.info('Elbow motor state: {}'.format(elbow_motor.state))
logger.info('Elbow motor duty cycle: {}'.format(elbow_motor.duty_cycle))
logger.info('Elbow motor speed: {}'.format(elbow_motor.speed))
elif event.code == 316 and event.value == 1: # PS
# stop control loop
running = False
# Move motors to default position
# motors_to_center()
# sound.play_song((('E5', 'e'), ('C4', 'e')))
leds.set_color("LEFT", "BLACK")
leds.set_color("RIGHT", "BLACK")
remote_leds.set_color("LEFT", "BLACK")
remote_leds.set_color("RIGHT", "BLACK")
time.sleep(1) # Wait for the motor thread to finish
break
clean_shutdown()