-
Notifications
You must be signed in to change notification settings - Fork 4
/
Copy pathlib_motion.py
565 lines (458 loc) · 23.7 KB
/
lib_motion.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
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
import math
import statistics
import time
import pigpio
import lib_para_360_servo
class control:
"""
Controls the robot movement.
This class controls the robots movement by controlling the two Parallax Feedback 360°
High-Speed Servos `360_data_sheet`_ . The robots local coordinate system is defined in
the following way. X-axis positive is straight forward, y-axis positive is perpendicular
to the x-axis in the direction to the left wheel. The center (0/0) is where the middle
of the robots chassi is cutting across a imaginary line through both wheels/servos.
Angle phi is the displacement of the local coordinate system to the real world
coordinate system. See :ref:`Used_local_coordinate_system` for a picture of it.
.. warning::
Be carefull with setting the min and max pulsewidth! Test carefully ``min_pw`` and ``max_pw``
before setting them. Wrong values can damage the servo, see set_servo_pulsewidth_ !!!
:param pigpio.pi pi:
Instance of a pigpio.pi() object.
:param int width_robot:
Width of the robot in mm, so distance between middle right wheel and middle
left wheel.
**Default:** 102 mm, measured.
:param diameter_wheels:
Diameter of both wheels.
**Default:** 66 mm, measured and taken from the products website `wheel_robot`_ .
:param int unitsFC:
Units in a full circle, so each wheel is divided into X sections/ticks.
This value should not be changed.
**Default:** 360
:param float dcMin_l:
Min duty cycle of the left wheel.
**Default:** 27.3, measured with method :meth:`lib_para_360_servo.calibrate_pwm` ,
see :ref:`Examples` .
:param float dcMax_l:
Max duty cycle of the left wheel.
**Default:** 969.15, measured with method :meth:`lib_para_360_servo.calibrate_pwm` ,
see :ref:`Examples` .
:param float dcMin_r:
Min duty cycle of the right wheel.
**Default:** 27.3, measured with method :meth:`lib_para_360_servo.calibrate_pwm` ,
see :ref:`Examples` .
:param float dcMax_r:
Max duty cycle of the left wheel.
**Default:** 978.25, measured with method :meth:`lib_para_360_servo.calibrate_pwm` ,
see :ref:`Examples` .
:param int l_wheel_gpio:
GPIO identified by their Broadcom number, see elinux.org_ .
To this GPIO the feedback wire of the left servo has to be connected.
:param int r_wheel_gpio:
GPIO identified by their Broadcom number, see elinux.org_ .
To this GPIO the feedback wire of the right servo has to be connected.
:param int servo_l_gpio:
GPIO identified by their Broadcom number, see elinux.org_ .
To this GPIO the control wire of the left servo has to be connected.
:param int min_pw_l:
Min pulsewidth, see **Warning**, carefully test the value before!
**Default:** 1280, taken from the data sheet `360_data_sheet`_ .
:param int max_pw_l:
Max pulsewidth, see **Warning**, carefully test the value before!
**Default:** 1720, taken from the data sheet `360_data_sheet`_ .
:param int min_speed_l:
Min speed which the servo is able to move. **Default:** -1, so that
the speed range is also scaled between -1 and 1 as the output of
the inner control loop.
:param int max_speed_l:
Max speed which the servo is able to move. **Default:** 1, so that
the speed range is also scaled between -1 and 1 as the output of
the inner control loop.
:param int servo_r_gpio:
GPIO identified by their Broadcom number, see elinux.org_ .
To this GPIO the control wire of the right servo has to be connected.
:param int min_pw_r:
Min pulsewidth, see **Warning**, carefully test the value before!
**Default:** 1280, taken from the data sheet `360_data_sheet`_ .
:param int max_pw_r:
Max pulsewidth, see **Warning**, carefully test the value before!
**Default:** 1720, taken from the data sheet `360_data_sheet`_ .
:param int min_speed_r:
Min speed which the servo is able to move. **Default:** -1, so that
the speed range is also scaled between -1 and 1 as the output of
the inner control loop.
:param int max_speed_r:
Max speed which the servo is able to move. **Default:** 1, so that
the speed range is also scaled between -1 and 1 as the output of
the inner control loop.
:param float sampling_time:
Sampling time of the four PID controllers in seconds.
**Default:** 0.01.
1. PWM of motor feedback is 910Hz (0,001098901 s), so position changes cannot
be recognized faster than 1.1 ms. Therefore, it is not needed to run the outer control
loop more often and update the speed values which have a 50 Hz (20ms) PWM.
2. Tests of the runtime of the code including the controller part have shown that
writing the pulsewidth (pi.set_servo_pulsewidth()) in :meth:`lib_para_360_servo.write_pwm.set_pw` is
the bottleneck which drastically slows down the code by the factor ~400
(0,002 seconds vs 0,000005 seconds; runtime with vs without writing pulsewidth).
3. For recognizing the RPMs of the wheels 10ms is needed to have enough changes in the
position. This was found out by testing. See method :meth:`move` for more informations.
:param int,float Kp_p:
Kp value of the outer PID controllers, see method :meth:`move`
for more informations.
**Default:** 0.1.
:param int,float Ki_p:
Ki value of the outer PID controllers, see method :meth:`move`
for more informations.
**Default:** 0.1.
:param int,float Kd_p:
Kd value of the outer PID controllers, see method :meth:`move`
for more informations.
**Default:** 0.
:param int,float Kp_s:
Kp value of the inner PID controllers, see method :meth:`move`
for more informations.
**Default:** 0.5.
:param int,float Ki_s:
Ki value of the inner PID controllers, see method :meth:`move`
for more informations.
**Default:** 0.
:param int,float Kd_s:
Kd value of the inner PID controllers, see method :meth:`move`
for more informations.
**Default:** 0.
.. _`360_data_sheet`: https://www.parallax.com/sites/default/files/downloads/900-00360-Feedback-360-HS-Servo-v1.1.pdf
.. _`wheel_robot`: https://www.parallax.com/product/28114
.. _elinux.org: https://elinux.org/RPi_Low-level_peripherals#Model_A.2B.2C_B.2B_and_B2
"""
def __init__(
self, pi, width_robot = 102, diameter_wheels = 66, unitsFC = 360,
dcMin_l = 27.3, dcMax_l = 969.15,
dcMin_r = 27.3, dcMax_r = 978.25,
l_wheel_gpio = 16, r_wheel_gpio = 20,
servo_l_gpio = 17, min_pw_l = 1280, max_pw_l = 1720, min_speed_l = -1, max_speed_l = 1,
servo_r_gpio = 27, min_pw_r = 1280, max_pw_r = 1720, min_speed_r = -1, max_speed_r = 1,
sampling_time = 0.01,
Kp_p = 0.1, #not too big values, otherwise output of position control would slow down too abrupt
Ki_p = 0.1,
Kd_p = 0,
Kp_s = 0.5,
Ki_s = 0,
Kd_s = 0):
self.pi = pi
self.width_robot = width_robot
self.diameter_wheels = diameter_wheels
self.unitsFC = unitsFC
self.dcMin_l = dcMin_l
self.dcMax_l = dcMax_l
self.dcMin_r = dcMin_r
self.dcMax_r = dcMax_r
self.sampling_time = sampling_time
self.Kp_p = Kp_p
self.Ki_p = Ki_p
self.Kd_p = Kd_p
self.Kp_s = Kp_s
self.Ki_s = Ki_s
self.Kd_s = Kd_s
self.l_wheel = lib_para_360_servo.read_pwm(pi = self.pi, gpio = l_wheel_gpio)
self.r_wheel = lib_para_360_servo.read_pwm(pi = self.pi, gpio = r_wheel_gpio)
self.servo_l = lib_para_360_servo.write_pwm(pi = self.pi, gpio = servo_l_gpio, min_pw = min_pw_l, max_pw = max_pw_l, min_speed = min_speed_l, max_speed = max_speed_l)
self.servo_r = lib_para_360_servo.write_pwm(pi = self.pi, gpio = servo_r_gpio, min_pw = min_pw_r, max_pw = max_pw_r, min_speed = min_speed_r, max_speed = max_speed_r)
#needed time for initializing the four instances
time.sleep(1)
#angular position in units full circle
def get_angle_l(self):
#driving forward will increase the angle
angle_l = (self.unitsFC - 1) - ((self.l_wheel.read() - self.dcMin_l) * self.unitsFC) / (self.dcMax_l - self.dcMin_l + 1)
angle_l = max(min((self.unitsFC - 1), angle_l), 0)
return angle_l
#angular position in units full circle
def get_angle_r(self):
#driving forward will increase the angle
angle_r = (self.r_wheel.read() - self.dcMin_r) * self.unitsFC / (self.dcMax_r - self.dcMin_r + 1)
angle_r = max(min((self.unitsFC - 1), angle_r), 0)
return angle_r
def set_speed_l(self, speed):
#the value of the speed of the left wheel will always be multiplied
#by minus 1 before setting it.
#this ensures that e.g. positive passed speed values/arguments
#will let the robot drive forward, which means for the left servo
#that it has to rotate counter-clockwise/negative in its local
#orientation system as it is defined in the lib_para_360_servo module.
self.servo_l.set_speed(-speed)
return None
def set_speed_r(self, speed):
self.servo_r.set_speed(speed)
return None
def get_total_angle(self, angle, unitsFC, prev_angle, turns):
#### counting number of rotations
#If 4th to 1st quadrant
if((angle < (0.25*unitsFC)) and (prev_angle > (0.75*unitsFC))):
turns += 1
#If in 1st to 4th quadrant
elif((prev_angle < (0.25*unitsFC)) and (angle > (0.75*unitsFC))):
turns -= 1
####total angle measurement from zero
if(turns >= 0):
total_angle = (turns*unitsFC) + angle
elif(turns < 0):
total_angle = ((turns + 1)*unitsFC) - (unitsFC - angle)
return turns, total_angle
def get_target_angle(self, number_ticks, angle):
#positiv number_ticks will be added, negativ number_ticks substracted
target_angle = angle + number_ticks
return target_angle
def tick_length(self):
tick_length_mm = math.pi * self.diameter_wheels / self.unitsFC
return tick_length_mm
def arc_circle(self, degree):
arc_circle_mm = degree * math.pi * self.width_robot / 360.0
return arc_circle_mm
def turn(self, degree):
"""
Turns the robot about x degree.
This method turns the robot x degree to the left or to the right.
Positive degree values turn the robot to the left,
negative degree values to the right, see picture in :ref:`Used_local_coordinate_system` ,
where the local coordinate system of the robot is shown. This method calls
:meth:`lib_motion.control.move` which controls the movement of the robot.
:param int,float degree:
Degree the robot has to turn.
"""
number_ticks = self.arc_circle(degree)/self.tick_length()
self.move(number_ticks = number_ticks, turn = True)
return None
def straight(self, distance_in_mm):
"""
Moves the robot about x mm forward or backward.
This method moves the robot x mm forward or backward. Positive distance
values move the robot forward (regarding the local x-axis), negative distance
values backward (regarding the local x-axis), see picture in :ref:`Used_local_coordinate_system` ,
where the local coordinate system of the robot is shown. This method calls
:meth:`lib_motion.control.move` which controls the movement of the robot.
:param int,float distance_in_mm:
Distance the robot has to move.
"""
number_ticks = distance_in_mm/self.tick_length()
self.move(number_ticks = number_ticks, straight = True)
return None
def move(
self,
number_ticks = 0,
straight = False,
turn = False):
"""
Core of motion control.
This method controls the movement of the robot. It is called from :meth:`lib_motion.control.turn`
or :meth:`lib_motion.control.straight` and is not ment to be called directly. Four
digital PID controllers are used to make two cascade control loops, one cascade control loop
for each wheel. Each cascade control loop has the same parameters (P/I/D parameters), so that
both wheels are controlled in the same way. Chosen default: Outer control loop is a PI
controller, inner control loop is a P controller. The outer loop is a position controller,
the inner loop a speed controller. After both wheels have reached their set-point (position),
the method waits one second before the movement is marked as finished. This ensures that
overshoots/oscillations are possible and that both wheels can independently reach their
set-point (position). The I part of each PID controller is limited to -1 and 1, so that
the sum of the errors is not integrated till infinity which means to very high or low values
which might cause problems. The output value of each inner PID controller is scaled between -1
and 1 and the output value of each outer PID controller is limited to -1 and 1.
This ensures that no scaling factors are introduced in the P/I/D parameters and also that
the output of each PID controller matches the speed range of the servos, defined in
:meth:`lib_para_360_servo.write_pwm.set_speed` . A sliding median window is used to filter out
the noise in the rotation speed measurement (ticks/s) which is done indirectly by measuring the
position of the servo. Also a deadband filter after the error calculation of the outer control
loop is implemented. This adjustments help to make the controllers more stable, e.g. filter out
outliers while calculating the rotation speed and therefore avoid high value changes/jumps or
avoid oscillations after reaching the set-point (position). The sample time of the digital PID
controllers can also be freely chosen and does not influence the P/I/D parameters, the rotation
speed measurement or the time before the movement is marked as finished.
:param int,float number_ticks:
Number of ticks the wheels have to move.
:param bool straight:
True or False, if robot should move straight.
**Default:** False.
:param bool turn:
True or False, if robot should turn.
**Default:** False.
"""
turns_l = 0
turns_r = 0
angle_l = self.get_angle_l()
angle_r = self.get_angle_r()
target_angle_r = self.get_target_angle(number_ticks = number_ticks, angle = angle_r)
if straight == True:
#speed and number_ticks of servo_l must rotate in
#SAME direction to servo_r while driving straight
target_angle_l = self.get_target_angle(number_ticks = number_ticks, angle = angle_l)
elif turn == True:
#speed and number_ticks of servo_l must rotate in
#OPPOSITE direction to servo_r while turning
target_angle_l = self.get_target_angle(number_ticks = -number_ticks, angle = angle_l)
#initial values sum_error_*
sum_error_r_p = 0
sum_error_r_s = 0
sum_error_l_p = 0
sum_error_l_s = 0
#initial values error_*_old
error_l_p_old = 0
error_l_s_old = 0
error_r_p_old = 0
error_r_s_old = 0
#empty lists for ticks_*
list_ticks_l = []
list_ticks_r = []
position_reached = False
reached_sp_counter = 0
#position must be reached for one second to allow
#overshoots/oscillations before stopping control loop
wait_after_reach_sp = 1/self.sampling_time
#start time of the control loop
start_time = time.time()
#control loop:
while not position_reached:
#DEBUGGING OPTION:
#printing runtime of loop , see end of while true loop
#start_time_each_loop = time.time()
angle_l = self.get_angle_l()
angle_r = self.get_angle_r()
#try needed, because:
#- first iteration of the while loop prev_angle_* is missing and the
#method self.get_total_angle() will throw an exception.
#- second iteration of the while loop prev_total_angle_* is missing,
#which will throw another exception
try:
turns_l, total_angle_l = self.get_total_angle(angle_l, self.unitsFC, prev_angle_l, turns_l)
turns_r, total_angle_r = self.get_total_angle(angle_r, self.unitsFC, prev_angle_r, turns_r)
#### cascade control right wheel
## Position Control
#Er = SP - PV
error_r_p = target_angle_r - total_angle_r
#Deadband-Filter to remove ocillating forwards and backwards after reaching set-point
if error_r_p <= 2 and error_r_p >= -2:
error_r_p = 0
#I-Part
sum_error_r_p += error_r_p
#limit I-Part to -1 and 1
#try needed, because Ki_p can be zero
try:
sum_error_r_p = max(min(1/self.Ki_p, sum_error_r_p), -1/self.Ki_p)
except Exception:
pass
#PID-Controller
output_r_p = self.Kp_p * error_r_p + self.Ki_p * self.sampling_time * sum_error_r_p + self.Kd_p / self.sampling_time * (error_r_p - error_r_p_old)
#limit output of position control to speed range
output_r_p = max(min(1, output_r_p), -1)
error_r_p_old = error_r_p
## Speed Control
#convert range output_r_p from -1 to 1 to ticks/s
#full speed of a wheel forward and backward = +-650 ticks/s
output_r_p_con = 650 * output_r_p
#ticks per second (ticks/s), calculated from a moving median window with 5 values
ticks_r = (total_angle_r - prev_total_angle_r) / self.sampling_time
list_ticks_r.append(ticks_r)
list_ticks_r = list_ticks_r[-5:]
ticks_r = statistics.median(list_ticks_r)
#Er = SP - PV
error_r_s = output_r_p_con - ticks_r
#I-Part
sum_error_r_s += error_r_s
#limit I-Part to -1 and 1
#try needed, because Ki_s can be zero
try:
sum_error_r_s = max(min(650/self.Ki_s, sum_error_r_s), -650/self.Ki_s)
except Exception:
pass
#PID-Controller
output_r_s = self.Kp_s * error_r_s + self.Ki_s * self.sampling_time * sum_error_r_s + self.Kd_s / self.sampling_time * (error_r_s - error_r_s_old)
error_r_s_old = error_r_s
#convert range output_r_s fom ticks/s to -1 to 1
output_r_s_con = output_r_s / 650
self.set_speed_r(output_r_s_con)
#### cascade control left wheel
## Position Control
#Er = SP - PV
error_l_p = target_angle_l - total_angle_l
#Deadband-Filter to remove ocillating forwards and backwards after reaching set-point
if error_l_p <= 2 and error_l_p >= -2:
error_l_p = 0
#I-Part
sum_error_l_p += error_l_p
#limit I-Part to -1 and 1
#try needed, because Ki_p can be zero
try:
sum_error_l_p = max(min(1/self.Ki_p, sum_error_l_p), -1/self.Ki_p)
except Exception:
pass
#PID-Controller
output_l_p = self.Kp_p * error_l_p + self.Ki_p * self.sampling_time * sum_error_l_p + self.Kd_p / self.sampling_time * (error_l_p - error_l_p_old)
#limit output of position control to speed range
output_l_p = max(min(1, output_l_p), -1)
error_l_p_old = error_l_p
## Speed Control
#convert range output_l_p from -1 to 1 to ticks/s
#full speed of a wheel forward and backward = +-650 ticks/s
output_l_p_con = 650 * output_l_p
#ticks per second (ticks/s), calculated from a moving median window with 5 values
ticks_l = (total_angle_l - prev_total_angle_l) / self.sampling_time
list_ticks_l.append(ticks_l)
list_ticks_l = list_ticks_l[-5:]
ticks_l = statistics.median(list_ticks_l)
#Er = SP - PV
error_l_s = output_l_p_con - ticks_l
#I-Part
sum_error_l_s += error_l_s
#limit I-Part to -1 and 1
#try needed, because Ki_s can be zero
try:
sum_error_l_s = max(min(650/self.Ki_s, sum_error_l_s), -650/self.Ki_s)
except Exception:
pass
#PID-Controller
output_l_s = self.Kp_s * error_l_s + self.Ki_s * self.sampling_time * sum_error_l_s + self.Kd_s / self.sampling_time * (error_l_s - error_l_s_old)
error_l_s_old = error_l_s
#convert range output_l_s fom ticks/s to -1 to 1
output_l_s_con = output_l_s / 650
self.set_speed_l(output_l_s_con)
except Exception:
pass
prev_angle_l = angle_l
prev_angle_r = angle_r
#try needed, because first iteration of the while loop prev_angle_* is
#missing and the method self.get_total_angle() will throw an exception,
#and therefore no total_angle_* gets calculated
try:
prev_total_angle_l = total_angle_l
prev_total_angle_r = total_angle_r
except Exception:
pass
try:
if error_l_p == 0 and error_r_p == 0:
reached_sp_counter += 1
if reached_sp_counter >= wait_after_reach_sp:
self.set_speed_r(0.0)
self.set_speed_l(0.0)
position_reached = True
else:
pass
except Exception:
pass
#Pause control loop for chosen sample time
#https://stackoverflow.com/questions/474528/what-is-the-best-way-to-repeatedly-execute-a-function-every-x-seconds-in-python/25251804#25251804
time.sleep(self.sampling_time - ((time.time() - start_time) % self.sampling_time))
#DEBUGGING OPTION:
#printing runtime of loop, see beginning of while true loop
#print('{:.20f}'.format((time.time() - start_time_each_loop)))
return None
def cancel(self):
"""
Cancel the started callback function.
This method cancels the started callback function if initializing an object.
As written in the pigpio callback_ documentation, the callback function may be cancelled
by calling the cancel function after the created instance is not needed anymore.
.. _callback: http://abyz.me.uk/rpi/pigpio/python.html#callback
"""
self.l_wheel.cancel()
self.r_wheel.cancel()
if __name__ == '__main__':
#just continue
pass