00001
00002
00003 """
00004 servos.py: classes for servo interaction
00005 Copyright (c) 2011 Vanadium Labs LLC. All right reserved.
00006
00007 Redistribution and use in source and binary forms, with or without
00008 modification, are permitted provided that the following conditions are met:
00009 * Redistributions of source code must retain the above copyright
00010 notice, this list of conditions and the following disclaimer.
00011 * Redistributions in binary form must reproduce the above copyright
00012 notice, this list of conditions and the following disclaimer in the
00013 documentation and/or other materials provided with the distribution.
00014 * Neither the name of Vanadium Labs LLC nor the names of its
00015 contributors may be used to endorse or promote products derived
00016 from this software without specific prior written permission.
00017
00018 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
00019 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00020 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
00021 DISCLAIMED. IN NO EVENT SHALL VANADIUM LABS BE LIABLE FOR ANY DIRECT, INDIRECT,
00022 INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
00023 LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
00024 OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
00025 LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
00026 OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
00027 ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
00028 """
00029
00030 import rospy
00031
00032 from math import pi, radians
00033 import xml.dom.minidom
00034
00035 from std_msgs.msg import Float64
00036 from arbotix_msgs.srv import *
00037 from diagnostic_msgs.msg import *
00038
00039 from arbotix_python.ax12 import *
00040
00041
00042 class Servo:
00043 """ Class to handle services and updates for a single Servo,
00044 on an ArbotiX robocontroller's AX/RX-bus. """
00045
00046 def __init__(self, device, name):
00047 self.device = device
00048 self.name = name
00049 n = "~dynamixels/"+name+"/"
00050
00051 self.id = int(rospy.get_param(n+"id"))
00052 self.neutral = rospy.get_param(n+"neutral",512)
00053 self.ticks = rospy.get_param(n+"ticks",1024)
00054 self.rad_per_tick = radians(rospy.get_param(n+"range",300.0))/self.ticks
00055
00056 self.max_angle = radians(rospy.get_param(n+"max_angle",150))
00057 self.min_angle = radians(rospy.get_param(n+"min_angle",-150))
00058 self.max_speed = radians(rospy.get_param(n+"max_speed",684.0))
00059
00060 self.invert = rospy.get_param(n+"invert",False)
00061 self.readable = rospy.get_param(n+"readable",True)
00062
00063 self.controller = None
00064 self.status = "OK"
00065 self.level = DiagnosticStatus.OK
00066
00067 self.dirty = False
00068 self.angle = 0.0
00069 self.desired = 0.0
00070 self.last_cmd = 0.0
00071 self.velocity = 0.0
00072 self.relaxed = True
00073 self.last = rospy.Time.now()
00074
00075 self.reads = 0.0
00076 self.errors = 0
00077 self.total_reads = 0.0
00078 self.total_errors = [0.0]
00079
00080 self.voltage = 0.0
00081 self.temperature = 0.0
00082
00083
00084 rospy.Subscriber(name+'/command', Float64, self.commandCb)
00085 rospy.Service(name+'/relax', Relax, self.relaxCb)
00086
00087 def angleToTicks(self, angle):
00088 """ Convert an angle to ticks, applying limits. """
00089 ticks = self.neutral + (angle/self.rad_per_tick)
00090 if self.invert:
00091 ticks = self.neutral - (angle/self.rad_per_tick)
00092 if ticks >= self.ticks:
00093 return self.ticks-1.0
00094 if ticks < 0:
00095 return 0
00096 return ticks
00097
00098 def ticksToAngle(self, ticks):
00099 """ Convert an ticks to angle, applying limits. """
00100 angle = (ticks - self.neutral) * self.rad_per_tick
00101 if self.invert:
00102 angle = -1.0 * angle
00103
00104
00105
00106
00107 return angle
00108
00109 def relaxCb(self, req):
00110 """ Turn off servo torque, so that it is pose-able. """
00111 if not self.device.fake:
00112 self.device.disableTorque(self.id)
00113 self.dirty = False
00114 self.relaxed = True
00115 return RelaxResponse()
00116
00117 def commandCb(self, req):
00118 """ Float64 style command input. """
00119 if self.desired != req.data or self.relaxed:
00120 self.dirty = True
00121 self.relaxed = False
00122 self.desired = req.data
00123
00124 def update(self, reading=None):
00125 """ Update angle in radians by reading from servo, or by
00126 using position passed in from a sync read (in ticks). """
00127 if reading == None:
00128 if self.readable and not self.device.fake:
00129 reading = self.device.getPosition(self.id)
00130 if reading > -1 and reading < self.ticks:
00131 self.reads += 1
00132 self.total_reads += 1
00133 last_angle = self.angle
00134 self.angle = self.ticksToAngle(reading)
00135
00136 t = rospy.Time.now()
00137 self.velocity = (self.angle - last_angle)/((t - self.last).to_nsec()/1000000000.0)
00138 self.last = t
00139 else:
00140 rospy.logdebug("Invalid read of servo: id " + str(self.id) + ", value " + str(reading))
00141 self.errors += 1
00142 self.total_reads += 1
00143 return
00144 if self.relaxed:
00145 self.last_cmd = self.angle
00146
00147 def updateTemp(self, reading=None):
00148 """ Update temperature by reading from servo, or by passing value from a sync read. """
00149 if reading == None:
00150 if self.readable and not self.device.fake:
00151 self.temperature = self.device.getTemperature(self.id)
00152 else:
00153 self.temperature = reading
00154 if self.temperature > 60:
00155 self.status = "OVERHEATED, SHUTDOWN"
00156 self.level = DiagnosticStatus.ERROR
00157 elif self.temperature > 50 and self.status != "OVERHEATED, SHUTDOWN":
00158 self.status = "OVERHEATING"
00159 self.level = DiagnosticStatus.WARN
00160 elif self.status != "OVERHEATED, SHUTDOWN":
00161 self.status = "OK"
00162 self.level = DiagnosticStatus.OK
00163
00164 def updateVoltage(self, reading=None):
00165 """ Update voltage by reading from servo, or by passing value from a sync read. """
00166 if reading == None:
00167 if self.readable and not self.device.fake:
00168 self.voltage = self.device.getVoltage(self.id)
00169 else:
00170 self.voltage = reading
00171
00172 def interpolate(self, frame):
00173 """ Get the new position to move to, in ticks. """
00174 if self.controller and self.controller.active():
00175
00176 return None
00177 if self.dirty:
00178
00179 cmd = self.desired - self.last_cmd
00180 if cmd > self.max_speed/frame:
00181 cmd = self.max_speed/frame
00182 elif cmd < -self.max_speed/frame:
00183 cmd = -self.max_speed/frame
00184
00185 ticks = self.angleToTicks(self.last_cmd + cmd)
00186 self.last_cmd = self.ticksToAngle(ticks)
00187 self.speed = cmd*frame
00188
00189 if self.last_cmd == self.desired:
00190 self.dirty = False
00191 if self.device.fake:
00192 self.angle = self.last_cmd
00193 return None
00194 return int(ticks)
00195 else:
00196 return None
00197
00198 def setControl(self, position):
00199 """ Set the position that controller is moving to.
00200 Returns output value in ticks. """
00201 ticks = self.angleToTicks(position)
00202 self.desired = self.ticksToAngle(ticks)
00203 self.last_cmd = self.ticksToAngle(ticks)
00204 return int(ticks)
00205
00206 def getDiagnostics(self):
00207 """ Get a diagnostics status. """
00208 msg = DiagnosticStatus()
00209 msg.name = self.name
00210 msg.level = self.level
00211 msg.message = self.status
00212 msg.values.append(KeyValue("Position", str(self.angle)))
00213 msg.values.append(KeyValue("Temperature", str(self.temperature)))
00214 msg.values.append(KeyValue("Voltage", str(self.voltage)))
00215 if self.reads + self.errors > 100:
00216 self.total_errors.append((self.errors*100.0)/(self.reads+self.errors))
00217 if len(self.total_errors) > 10:
00218 self.total_errors = self.total_errors[-10:]
00219 self.reads = 0
00220 self.errors = 0
00221 msg.values.append(KeyValue("Reads", str(self.total_reads)))
00222 msg.values.append(KeyValue("Error Rate", str(sum(self.total_errors)/len(self.total_errors))+"%" ))
00223 if self.relaxed:
00224 msg.values.append(KeyValue("Torque", "OFF"))
00225 else:
00226 msg.values.append(KeyValue("Torque", "ON"))
00227 return msg
00228
00229
00230 class HobbyServo(Servo):
00231 """ Class to handle services and updates for a single Hobby Servo,
00232 connected to an ArbotiX robocontroller. """
00233 def __init__(self, device, params):
00234 self.device = device
00235 self.name = name
00236 n = "~servos/"+name+"/"
00237
00238 self.id = int(rospy.get_param(n+"id"))
00239 self.neutral = rospy.get_param(n+"neutral",1500)
00240 self.ticks = rospy.get_param(n+"ticks",2000)
00241 self.rad_per_tick = radians(rospy.get_param(n+"range",180.0))/self.ticks
00242
00243 self.max_angle = radians(rospy.get_param(n+"max_angle",90))
00244 self.min_angle = radians(rospy.get_param(n+"min_angle",-90))
00245 self.max_speed = radians(rospy.get_param(n+"max_speed",90.0))
00246
00247 self.invert = rospy.get_param(n+"invert",False)
00248 self.readable = False
00249
00250 self.controller = None
00251 self.status = "OK"
00252 self.level = DiagnosticStatus.OK
00253
00254 self.dirty = False
00255 self.angle = 0.0
00256 self.velocity = 0.0
00257
00258
00259 rospy.Subscriber(params.name+'/command', Float64, self.commandCb)
00260
00261 def commandCb(self, req):
00262 """ Callback to set position to angle, in radians. """
00263 self.dirty = True
00264 self.angle = req.data
00265
00266 def update(self, value):
00267 """ If dirty, update value of servo at device. """
00268 pass
00269
00270 def interpolate(self, frame):
00271 if self.dirty:
00272
00273 if self.angle < self.min_angle:
00274 self.angle = self.min_angle
00275 if self.angle > self.max_angle:
00276 self.angle = self.max_angle
00277
00278 ang = self.angle
00279 if self.invert:
00280 ang = ang * -1.0
00281 ticks = int(round( ang / self.rad_per_tick ))
00282 if not self.device.fake:
00283 self.device.setServo(self.id, ticks)
00284 self.dirty = False
00285
00286 def getDiagnostics(self):
00287 """ Get a diagnostics status. """
00288 msg = DiagnosticStatus()
00289 msg.name = "Joint " + self.name
00290 msg.level = self.level
00291 msg.message = self.status
00292 msg.values.append(KeyValue("Position", str(self.angle)))
00293 return msg
00294
00295
00296 class Servos(dict):
00297
00298 def __init__(self, device):
00299 self.device = device
00300 self.fake = device.fake
00301
00302 dynamixels = rospy.get_param("~dynamixels", dict())
00303 self.dynamixels = dict()
00304 for name in dynamixels.keys():
00305 self.dynamixels[name] = Servo(device, name)
00306
00307 hobbyservos = rospy.get_param("~servos", dict())
00308 self.hobbyservos = dict()
00309 for name in hobbyservos.keys():
00310 self.hobbyservos[name] = HobbyServo(device, name)
00311
00312 self.w_delta = rospy.Duration(1.0/rospy.get_param("~write_rate", 10.0))
00313 self.w_next = rospy.Time.now() + self.w_delta
00314
00315 self.r_delta = rospy.Duration(1.0/rospy.get_param("~read_rate", 10.0))
00316 self.r_next = rospy.Time.now() + self.r_delta
00317
00318 def values(self):
00319 return self.dynamixels.values() + self.hobbyservos.values()
00320
00321 def __getitem__(self, key):
00322 try:
00323 return self.dynamixels[key]
00324 except:
00325 return self.hobbyservos[key]
00326
00327 def __setitem__(self, key, value):
00328 try:
00329 k = self.dynamixels[key]
00330 k.update(value)
00331 except:
00332 k = self.hobbyservos[key]
00333 k.update(value)
00334
00335 def update(self, sync=True):
00336 """ Read servo positions. """
00337 if rospy.Time.now() > self.r_next and not self.fake:
00338
00339 if sync:
00340
00341 synclist = list()
00342 for servo in self.dynamixels.values():
00343 if servo.readable:
00344 synclist.append(servo.id)
00345 else:
00346 servo.update(-1)
00347 if len(synclist) > 0:
00348 val = self.device.syncRead(synclist, P_PRESENT_POSITION_L, 2)
00349 if val:
00350 for servo in self.dynamixels.values():
00351 try:
00352 i = synclist.index(servo.id)*2
00353 servo.update(val[i]+(val[i+1]<<8))
00354 except:
00355
00356 continue
00357 else:
00358
00359 for servo in self.dynamixels.values():
00360 servo.update(None)
00361
00362
00363
00364 self.r_next = rospy.Time.now() + self.r_delta
00365
00366 def updateStats(self, sync=True):
00367 """ Read servo voltages, temperatures. """
00368 if self.fake:
00369 return
00370 try:
00371 if sync:
00372
00373 synclist = list()
00374 for servo in self.dynamixels.values():
00375 if servo.readable:
00376 synclist.append(servo.id)
00377 else:
00378 servo.update(-1)
00379 if len(synclist) > 0:
00380 val = self.device.syncRead(synclist, P_PRESENT_VOLTAGE, 2)
00381 if val:
00382 for servo in self.dynamixels.values():
00383 try:
00384 i = synclist.index(servo.id)*2
00385 servo.voltage = val[i]/10.0
00386 servo.temperature = val[i+1]
00387 except:
00388
00389 continue
00390 else:
00391
00392 for servo in self.dynamixels.values():
00393 if servo.readable:
00394 val = self.device.read(servo.id, P_PRESENT_VOLTAGE, 2)
00395 servo.voltage = val[0]
00396 servo.temperature = val[1]
00397 except:
00398 rospy.logdebug("Error in updating stats.")
00399 return
00400
00401 def interpolate(self, sync=True):
00402 """ Write updated positions to servos. """
00403 if rospy.Time.now() > self.w_next:
00404 if sync and not self.fake:
00405 syncpkt = list()
00406 for servo in self.dynamixels.values():
00407 v = servo.interpolate(1.0/self.w_delta.to_sec())
00408 if v != None:
00409 syncpkt.append([servo.id,int(v)%256,int(v)>>8])
00410 for servo in self.hobbyservos.values():
00411 servo.interpolate(1.0/self.w_delta.to_sec())
00412 if len(syncpkt) > 0:
00413 self.device.syncWrite(P_GOAL_POSITION_L,syncpkt)
00414 else:
00415 for servo in self.dynamixels.values():
00416 v = servo.interpolate(1.0/self.w_delta.to_sec())
00417 if v != None:
00418 self.device.setPosition(servo.id, int(v))
00419 for servo in self.hobbyservos.values():
00420 servo.interpolate(1.0/self.w_delta.to_sec())
00421 self.w_next = rospy.Time.now() + self.w_delta
00422
00423
00424
00425 def getServosFromURDF():
00426 """ Get servo parameters from URDF. """
00427 try:
00428 description = rospy.get_param("robot_description")
00429 robot = xml.dom.minidom.parseString(description).getElementsByTagName('robot')[0]
00430 joints = {}
00431
00432 for child in robot.childNodes:
00433 if child.nodeType is child.TEXT_NODE:
00434 continue
00435 if child.localName == 'joint':
00436 jtype = child.getAttribute('type')
00437 if jtype == 'fixed':
00438 continue
00439 name = child.getAttribute('name')
00440 if jtype == 'continuous':
00441 minval = -pi
00442 maxval = pi
00443 else:
00444 limit = child.getElementsByTagName('limit')[0]
00445 minval = float(limit.getAttribute('lower'))
00446 maxval = float(limit.getAttribute('upper'))
00447
00448 if minval > 0 or maxval < 0:
00449 zeroval = (maxval + minval)/2
00450 else:
00451 zeroval = 0
00452
00453 joint = {'min':minval, 'max':maxval, 'zero':zeroval, 'value':zeroval }
00454 joints[name] = joint
00455 return joints
00456 except:
00457 rospy.loginfo('No URDF defined, proceeding with defaults')
00458 return dict()
00459
00460
00461 def getServoLimits(name, joint_defaults, default_min=-150, default_max=150):
00462 """ Get limits of servo, from YAML, then URDF, then defaults if neither is defined. """
00463 min_angle = radians(default_min)
00464 max_angle = radians(default_max)
00465
00466 try:
00467 min_angle = joint_defaults[name]['min']
00468 except:
00469 pass
00470 try:
00471 min_angle = radians(rospy.get_param("/arbotix/dynamixels/"+name+"/min_angle"))
00472 except:
00473 pass
00474
00475 try:
00476 max_angle = joint_defaults[name]['max']
00477 except:
00478 pass
00479 try:
00480 max_angle = radians(rospy.get_param("/arbotix/dynamixels/"+name+"/max_angle"))
00481 except:
00482 pass
00483
00484 return (min_angle, max_angle)
00485