-
Notifications
You must be signed in to change notification settings - Fork 7
Expand file tree
/
Copy pathhexapodMotion.py
More file actions
413 lines (324 loc) · 14.4 KB
/
hexapodMotion.py
File metadata and controls
413 lines (324 loc) · 14.4 KB
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
#!/usr/bin/env python
from __future__ import division
from Adafruit_PWM_Servo_Driver import PWM
import scipy.optimize
from common import *
import time
import math
import thread
# some trig functions converted to degrees (not used when performance is needed)
def sin(angle):
return math.sin(math.radians(angle))
def asin(x):
return math.degrees(math.asin(x))
def cos(angle):
return math.cos(math.radians(angle))
def acos(x):
return math.degrees(math.acos(x))
def tan(angle):
return math.tan(math.radians(angle))
# mechanical parameters in cm (between rotational points)
coxaFemurLen = 1.7
femurLen = 8.0
tibiaLen = 12.5
# this sets the frequency at which pulses are sent to the servos. 1000/pwmFrequency ms for each cycle
pwmFrequency = 60
# global absolute limits for servo travel. pulseLen/4096 is the ratio of time the pulse is set high in each cycle
minPulseLen = 170
maxPulseLen = 580
# coxa positive movement ccw from top view (0 point with leg perpendicular to body)
# femur positive movement ccw from rearview (0 point parallel to floor)
# tibia positive movement ccw from rearview (0 point when right angle to femur)
# leg 1 is the rear right leg. leg number increasing ccw looking from top
# servo calibration values. order: pulseLen1, pulseLen2, degrees at pulseLen1, degrees at pulseLen2. linear interpolation/extrapolation is used from these calibration values
servoParameters = {
"coxa1": [376, 255, 0, 45],
"coxa2": [353, 255, 0, 40],
"coxa3": [365, 255, 0, 43],
"coxa4": [360, 455, 0, 29],
"coxa5": [401, 475, 0, 25],
"coxa6": [365, 475, 0, 35],
"femur1": [369, 575, 0, 75],
"femur2": [351, 568, 0, 77],
"femur3": [386, 192, 0, 73],
"femur4": [392, 580, 0, 67],
"femur5": [380, 180, 0, 75],
"femur6": [382, 170, 0, 69],
"tibia1": [340, 582, 0, 75],
"tibia2": [340, 180, 0, 75],
"tibia3": [359, 180, 0, 75],
"tibia4": [380, 582, 0, 75],
"tibia5": [347, 582, 0, 75],
"tibia6": [367, 180, 0, 75]
}
# channel map on the pwm chips. legs 1-3 are on pwmR, 4-6 on pwmL
servoChans = {
"coxa1": 0,
"coxa2": 3,
"coxa3": 11,
"coxa4": 4,
"coxa5": 12,
"coxa6": 15,
"femur1": 1,
"femur2": 4,
"femur3": 12,
"femur4": 3,
"femur5": 11,
"femur6": 14,
"tibia1": 2,
"tibia2": 5,
"tibia3": 13,
"tibia4": 2,
"tibia5": 10,
"tibia6": 13,
}
# calculates the servo pulse length for a desired angle. assumes servo angle is linear wrt the pulseLen
def getPulseLenFromAngle(legSection, angle):
totalSteps = servoParameters[legSection][1] - servoParameters[legSection][0]
totalAngle = servoParameters[legSection][3] - servoParameters[legSection][2]
slope = totalSteps / totalAngle
intercept = servoParameters[legSection][0] - slope * servoParameters[legSection][2]
pulseLen = slope * angle + intercept
return int(round(pulseLen, 0))
class hexapodMotion:
# when this is enabled, servos will not be moved
testMode = False
currentServoAngles = {
}
# allows or disallows changing of leg angles. this is to prevent femur going down when coxa is repositioning itself after end of powerstroke and to prevent neddless recalculation
legCommandLock = {
'1': False,
'2': False,
'3': False,
'4': False,
'5': False,
'6': False
}
# initial parameters (i.e. while standing and default walking position)
femurStandStartAngle = 20
tibiaStandStartAngle = -10
coxaStandStartAngle = 0
# walk parameters
walkSpeed = 0 # float value from -1 to 1. sign gives direction of walking
walkResolution = 60 # number of steps for one full walk cycle. some of these are technically "skipped" when repositioning for powerstroke
stepAngle = 360 / walkResolution # degrees in each small step in the cycle
coxaWalkSweepAngle = 22 # half of the total sweep angle for each leg
# servo takes 0.22s to go 60 degrees @ 6V. self.walkSpeed depends on this value
servoMaxSpeed = 60/0.35 # deg/sec. the actual servo speed depends on the angle difference it is commanded to move. for now will just pad it
stepTimeInterval = 0.001 # the minimum interval in seconds between successive walk, rotation etc. commands. a multiplier for this is calculated based on walkSpeed
# current leg walk angles. on initialization of the class these will get generated by generateLegWalkOffsets
legWalkAngles = {}
# cache for the femur and tibia angle calculations during walking
tibiaFemurWalkAnglesCache = {}
def __init__(self):
addressPwmL = 0x41
addressPwmR = 0x40
if self.testMode == False:
# initialise the PWM devices
self.pwmL = PWM(addressPwmL, debug=False) # left side (view from rear)
self.pwmL.setPWMFreq(pwmFrequency) # frequency in Hz
self.pwmR = PWM(addressPwmR, debug=False) # right side
self.pwmR.setPWMFreq(pwmFrequency) # frequency in Hz
log("PCA9685 modules initialized. pwmL addr: " + hex(addressPwmL) + " pwmR addr: " + hex(addressPwmR))
# calculated initial robot parameters
self.robotHeight = self.calcRobotHeight(self.femurStandStartAngle, self.tibiaStandStartAngle)
self.stanceWidth = self.calcStanceWidth(self.femurStandStartAngle, self.tibiaStandStartAngle)
self.tibiaIntersectWidth = self.calcTibiaIntersectWidth(self.femurStandStartAngle, self.tibiaStandStartAngle)
# fill the walkvalues with initial defaults
self.generateLegWalkOffsets(self.legWalkAngles)
# create servo angle cache
self.createTibiaFemurWalkAnglesCache()
# the initial offsets in the walk angle between legs
def generateLegWalkOffsets(self, legWalkAngles):
legWalkAngles['1'] = 150
legWalkAngles['2'] = 210
legWalkAngles['3'] = 90
legWalkAngles['4'] = 180
legWalkAngles['5'] = 120
legWalkAngles['6'] = 240
def moveServoToAngle(self, legSection, angle):
servoChan = servoChans[legSection]
legNum = int(legSection[-1:])
pulseLen = getPulseLenFromAngle(legSection, angle)
if 1 <= legNum <= 3:
pwm = self.pwmR
else:
pwm = self.pwmL
if minPulseLen <= pulseLen <= maxPulseLen:
#print "servo " + legSection + " told to go to pos: " + str(pulseLen)
if self.testMode == False: pwm.setPWM(servoChan, 0, pulseLen)
self.currentServoAngles[legSection] = angle
else:
log("servo " + legSection + " told to go to an out of range pos: " + str(pulseLen))
# walk supporting functions
# calculate direction based on walkSpeed value
def direction(self):
if self.walkSpeed < 0: return -1
if self.walkSpeed == 0: return 0
if self.walkSpeed > 0: return 1
# modify the walking speed based on float input -1 to 1, i.e. from the right (r3) analog stick
def stepIntervalMultiplier(self):
maxLegCommandLockTime = 2 * self.coxaWalkSweepAngle / self.servoMaxSpeed
minTimeInterval = maxLegCommandLockTime / self.walkResolution # the minimum delay between commanded steps, dependent on servo speed
minScale = int(minTimeInterval / self.stepTimeInterval) + 5 # min scale value. don't want to send commands too fast
maxScale = 80 # max scale value. set so robot has a reasonable minimum speed
scale = int((1 - math.fabs(self.walkSpeed)) * maxScale)
if scale > maxScale: scale == maxScale
if scale <= 0: scale = minScale
return (scale)
# vertical distance from ground(tibia tip) to femur/coxa pivot point (robot height)
def calcRobotHeight(self, femurAngle, tibiaAngle):
return tibiaLen * cos(femurAngle + tibiaAngle) - femurLen * sin(femurAngle)
# horizontal distance (on floor) from tibia tip to femur pivot point
def calcStanceWidth(self, femurAngle, tibiaAngle):
return femurLen * cos(femurAngle) + tibiaLen * sin(femurAngle + tibiaAngle)
# horizontal distance from femur pivot point to tibia intersect
def calcTibiaIntersectWidth(self, femurAngle, tibiaAngle):
return femurLen * cos(femurAngle) + femurLen * sin(femurAngle) * tan(femurAngle + tibiaAngle)
# lookup cache to prevent needless recalculation of tibia and femur angles (very costly!)
def putTibiaFemurWalkAnglesInCache(self, coxaAngle, angles):
if coxaAngle not in self.tibiaFemurWalkAnglesCache: self.tibiaFemurWalkAnglesCache[coxaAngle] = {}
self.tibiaFemurWalkAnglesCache[coxaAngle] = angles
# perform a tibiaFemurWalkAnglesCache cache lookup
def getTibiaFemurWalkAnglesInCache(self, coxaAngle):
if coxaAngle not in self.tibiaFemurWalkAnglesCache: return ["error"]
else: return self.tibiaFemurWalkAnglesCache[coxaAngle]
# will calculate all angles at once instead of realtime so that it doesn't slow down the speed and repeat calculations needlessly
def createTibiaFemurWalkAnglesCache(self):
t = time.time()
log("Generating tibia and femur walk angles cache...")
# leg loop
for i in range(1, 7):
# needs to be run for walkAngle between +90 and +270
walkAngle = 90
coxaAngle = 0
while walkAngle <= 270:
leg = str(i)
coxaAngle = self.coxaWalkSweepAngle * sin(walkAngle)
self.tibiaFemurWalkAngles(leg, coxaAngle)
walkAngle += self.stepAngle
#print self.tibiaFemurWalkAnglesCache
log("Generated tibia and femur walk angles cache in " + str(time.time() - t) + " seconds")
# when coxa angle changes during walking, these are the femur and tibia angles to maintain constant robot height and position of tibia tip on floor without slipping
def tibiaFemurWalkAngles(self, leg, coxaAngle):
# because only legs 2 and 5 have tibias in line with the femur pivot, these offsets are needed to "fudge" the math for the other legs which are offset...will adjust later
amountOffset = 20
coxaAngleOffsets = {
'1': -amountOffset,
'2': 0,
'3': amountOffset,
'4': amountOffset,
'5': 0,
'6': -amountOffset
}
coxaAngle += coxaAngleOffsets[leg]
# check if walkAngle already in cache
cacheResult = self.getTibiaFemurWalkAnglesInCache(coxaAngle)
if cacheResult[0] != "error":
return cacheResult
if self.walkSpeed != 0: log("tibiaFemurWalkAngles being calculated for leg: " + leg + " and coxaangle: " + str(coxaAngle))
# constraints: robotHeight needs to remain constant and tibia tip at a fixed point on floor for fluid forward movement
d = self.stanceWidth / cos(coxaAngle) # the length needed so the tibia tip is along the line coinciding with the other tibia tips (parallel to movement direction) so the legs don't slip on the ground
# the exact IK equation. x = femurAngle
def f(x):
eqns = (self.robotHeight + femurLen * math.sin(x)) ** 2 + (d - femurLen * math.cos(x)) ** 2 - tibiaLen ** 2
return eqns
# approximate the femurAngle
femurAngle = scipy.optimize.newton(f, math.radians(self.femurStandStartAngle)) # make the initial guess the standing start angle
# calculate exactly the tibiaAngle from the approximate femurAngle
tibiaAngle = math.acos( (self.robotHeight + femurLen * math.sin(femurAngle)) / tibiaLen ) - femurAngle
# convert to degrees
femurAngle = math.degrees(femurAngle)
tibiaAngle = math.degrees(tibiaAngle)
# put in cache so don't have to recalculate
self.putTibiaFemurWalkAnglesInCache(coxaAngle, [femurAngle, tibiaAngle])
#print str(d) + " " + str(femurAngle) + " " + str(tibiaAngle)
return [femurAngle, tibiaAngle]
# calculate the commanded servo angles
def walkServoAngles(self, leg):
# modify the walkAngles
self.legWalkAngles[leg] = self.legWalkAngles[leg] + (self.direction() * self.stepAngle)
# var to hold the coxa, femur and tibia angles
angles = {}
# make sure walkAngle is always between 0-360
self.legWalkAngles[leg] = self.legWalkAngles[leg] % 360
# supporting vars
if self.legWalkAngles[leg] >= 0: m = 1
else: m = -1
# if not in the powerstroke, reposition the leg so it is
if not ((m * 90) <= self.legWalkAngles[leg] <= (m * 270)):
self.legCommandLock[leg] = True # flag the leg as locked
# on execution of legTimedUnlock the leg will unlock and reposition to floor
sleepTime = 2 * self.coxaWalkSweepAngle / self.servoMaxSpeed
thread.start_new_thread(self.legTimedUnlock, (leg, sleepTime,))
self.legWalkAngles[leg] = self.legWalkAngles[leg] + self.direction() * 180
# make sure walkAngle is always between 0-360
self.legWalkAngles[leg] = self.legWalkAngles[leg] % 360
# calculate the angles
angles["coxa"] = self.coxaWalkSweepAngle * sin(self.legWalkAngles[leg])
ftAngles = self.tibiaFemurWalkAngles(leg, angles["coxa"])
angles["femur"] = ftAngles[0]
angles["tibia"] = ftAngles[1]
if self.legCommandLock[leg] == True:
angles["femur"] = self.femurStandStartAngle + 20
# debugging
if leg == "5" and self.direction() != 0 and self.testMode == True:
if self.direction() == 1: print "walking forward"
elif self.direction() == -1: print "walking backward"
print str(time.time()) + ": leg" + leg + " walkangles: " + str(self.legWalkAngles[leg]) + " " + str(angles)
return angles
# lock leg angles from changing
def legTimedUnlock(self, leg, sleepTime):
# sleep to allow servo to get to beginning of powerstroke position
#print "sleeping for" + str(sleepTime)
time.sleep(sleepTime)
# unlock leg and execute another step so leg will move down to floor
self.legCommandLock[leg] = False
self.walkServoAngles(leg)
# stand function
def stand(self):
for i in range(1, 7):
self.moveServoToAngle("coxa" + str(i), self.coxaStandStartAngle)
self.moveServoToAngle("femur" + str(i), self.femurStandStartAngle + 20)
time.sleep(0.4)
for i in range(1, 7):
self.moveServoToAngle("tibia" + str(i), self.tibiaStandStartAngle)
time.sleep(0.4)
for i in range(1, 7):
self.moveServoToAngle("femur" + str(i), self.femurStandStartAngle)
time.sleep(0.4)
# walk function
def walk(self):
if self.walkSpeed == 0:
time.sleep(0.1)
return
for i in range(1, 7): # leg loop
leg = str(i)
# call only if leg is not locked
if self.legCommandLock[leg] == False:
# get the angles
angles = self.walkServoAngles(leg)
# move the servos
self.moveServoToAngle("coxa" + leg, angles["coxa"])
self.moveServoToAngle("femur" + leg, angles["femur"])
self.moveServoToAngle("tibia" + leg, angles["tibia"])
if self.testMode == True: print ""
time.sleep(self.stepTimeInterval * self.stepIntervalMultiplier())
# temporary functions
def __moveServoToPos(self, legSection, pulseLen):
servoChan = servoChans[legSection]
legNum = int(legSection[-1:])
if 1 <= legNum <= 3:
pwm = self.pwmR
else:
pwm = self.pwmL
if minPulseLen <= pulseLen <= maxPulseLen:
pwm.setPWM(servoChan, 0, pulseLen)
def __testServoOffsets(self):
self.__moveServoToPos("tibia6", 367)
#self.pwmR.setPWM(12, 0, 580)
#self.pwmL.setPWM(3, 0, 170)
for i in range(1, 7):
self.moveServoToAngle("coxa" + str(i), 0)
self.moveServoToAngle("femur" + str(i), 50)
#self.moveServoToAngle("tibia" + str(i), 45)