lib_dynamixel.py
Go to the documentation of this file.
00001 #!/usr/bin/python
00002 #
00003 # Copyright (c) 2009, Georgia Tech Research Corporation
00004 # All rights reserved.
00005 #
00006 # Redistribution and use in source and binary forms, with or without
00007 # modification, are permitted provided that the following conditions are met:
00008 #     * Redistributions of source code must retain the above copyright
00009 #       notice, this list of conditions and the following disclaimer.
00010 #     * Redistributions in binary form must reproduce the above copyright
00011 #       notice, this list of conditions and the following disclaimer in the
00012 #       documentation and/or other materials provided with the distribution.
00013 #     * Neither the name of the Georgia Tech Research Corporation nor the
00014 #       names of its contributors may be used to endorse or promote products
00015 #       derived from this software without specific prior written permission.
00016 #
00017 # THIS SOFTWARE IS PROVIDED BY GEORGIA TECH RESEARCH CORPORATION ''AS IS'' AND
00018 # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
00019 # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
00020 # DISCLAIMED. IN NO EVENT SHALL GEORGIA TECH BE LIABLE FOR ANY DIRECT, INDIRECT,
00021 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
00022 # LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,
00023 # OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
00024 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
00025 # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
00026 # ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
00027 #
00028 
00029 ## Controlling Robotis Dynamixel RX-28, RX-64, and MX-64 servos from python
00030 ## using the USB2Dynamixel adaptor.
00031 
00032 ## Authors: Travis Deyle, Advait Jain, Marc Killpack, and Phillip Grice (Healthcare Robotics Lab, Georgia Tech.)
00033 
00034 import serial
00035 import struct
00036 import sys, optparse
00037 import math
00038 import numpy as np
00039 
00040 class USB2Dynamixel_Device():
00041     ''' Class that manages serial port contention between servos on same bus
00042     '''
00043     def __init__( self, dev_name = '/dev/ttyUSB0', baudrate = 57600 ):
00044         try:
00045             self.dev_name = string.atoi( dev_name ) # stores the serial port as 0-based integer for Windows
00046         except:
00047             self.dev_name = dev_name # stores it as a /dev-mapped string for Linux / Mac
00048 
00049         self.servo_dev = self._open_serial( baudrate )
00050 
00051     def _open_serial(self, baudrate):
00052         servo_dev = None
00053         try:
00054             servo_dev = serial.Serial(self.dev_name, baudrate, timeout=1.0)
00055             # Closing the device first seems to prevent "Access Denied" errors on WinXP
00056             # (Conversations with Brian Wu @ MIT on 6/23/2010)
00057             servo_dev.close()
00058             servo_dev.setParity('N')
00059             servo_dev.setStopbits(1)
00060             servo_dev.open()
00061 
00062             servo_dev.flushOutput()
00063             servo_dev.flushInput()
00064             servo_dev.flush()
00065 
00066         except (serial.serialutil.SerialException), e:
00067             raise RuntimeError('lib_dynamixel: Serial port not found!\n')
00068         if(servo_dev == None):
00069             raise RuntimeError('lib_dynamixel: Serial port not found!\n')
00070         return servo_dev
00071 
00072     def _write_serial(self, msg):
00073         self.servo_dev.flushInput()
00074         self.servo_dev.write( msg )
00075 
00076     def _send_serial(self, msg):
00077         """ sends the command to the servo
00078         """
00079         out = struct.pack('%dB' %len(msg), *msg)
00080         self._write_serial( out )
00081 
00082     def _read_serial(self, nBytes=1):
00083         ''' Reads data from the servo
00084         '''
00085         self.servo_dev.flushOutput()
00086         return self.servo_dev.read( nBytes )
00087 
00088     def _receive_reply(self, id):
00089         ''' Reads the status packet returned by the servo
00090         '''
00091         start = self._read_serial( 2 )
00092         if start != '\xff\xff':
00093             raise RuntimeError('lib_dynamixel: Failed to receive start bytes\n')
00094         servo_id = ord(self._read_serial( 1 ))
00095         if servo_id != id:
00096             raise RuntimeError('lib_dynamixel: Incorrect servo ID received: %d\n' %servo_id)
00097         data_len = ord(self._read_serial( 1 ))
00098         err = ord(self._read_serial( 1 ))
00099         raw_data = self._read_serial( data_len - 2 )
00100         data = [ord(d) for d in raw_data]
00101         chksum = servo_id + data_len + err + sum(data)
00102         chksum = ( ~chksum ) % 256
00103         checksum = ord(self._read_serial( 1 ))
00104         if checksum != chksum:
00105             raise RuntimeError('lib_dynamixel: Error in Received Checksum')
00106         return id, data, err
00107 
00108     def __calc_checksum(self, msg):
00109         chksum = sum(msg)
00110         return ( ~chksum ) % 256
00111 
00112     def _send_instruction(self, instruction, id, status_return=True):
00113         ''' Fills out packet metadata, manages mutex, sends packet, and handles response.
00114         '''
00115         msg = [ id, len(instruction) + 1 ] + instruction # instruction includes the command (1 byte + parameters. length = parameters+2)
00116         chksum = self.__calc_checksum( msg )
00117         msg = [ 0xff, 0xff ] + msg + [chksum]
00118         try:
00119             self._send_serial( msg )
00120             if status_return:
00121                 id, data, err = self._receive_reply(id)
00122             else:
00123                 id = 0xFE
00124                 data = []
00125                 err = 0 #No Error Received
00126         except:
00127             raise
00128         if err != 0:
00129             self._process_err( err, id )
00130         return data
00131 
00132     def _process_err( self, err, id ):
00133         ''' Process and raise errors received from the robotis servo.
00134         '''
00135         msg = "Error(s) reported by Dynamixel ID %d:" %id
00136         if err & 1: #bitwise & -- check each bit 'flag'
00137             msg += "\n\tInput Voltage Error: Applied Voltage outside of set operating range."
00138         if err & 2:
00139           msg += "\n\tAngle Limit Error: Received Goal position outside of set limits."
00140         if err & 4:
00141           msg += "\n\tOverHeating Error: Temperature exceeds set temperature limit."
00142         if err & 8:
00143           msg += "\n\tRange Error: Received command beyond usage range."
00144         if err & 16:
00145           msg += "\n\tCheckSum Error: Invalid Checksum in Instruction Packet."
00146         if err & 32:
00147           msg += "\n\tOverload Error: Current load exceeds set torque limit"
00148         if err & 64:
00149             msg += "\n\tInstruction Error: Received undefined instruction, "+\
00150             "or action command received before command was registered\n"
00151         raise RuntimeError(msg)
00152 
00153     def read_address(self, id, address, nBytes=1):
00154         ''' reads nBytes from address on the servo at id.
00155             returns [n1,n2 ...] (list of parameters)
00156         '''
00157         msg = [ 0x02, address, nBytes ]
00158         return self._send_instruction( msg, id )
00159 
00160     def write_address(self, id, address, data):
00161         ''' writes data at the address on the servo of id.
00162             data = [n1,n2 ...] list of numbers.
00163             return [n1,n2 ...] (list of return parameters)
00164         '''
00165         msg = [ 0x03, address ] + data
00166         return self._send_instruction( msg, id )
00167 
00168     def ping(self, id):
00169         ''' pings servo at id
00170         '''
00171         msg = [ 0x01 ]
00172         return self._send_instruction( msg, id )
00173 
00174     def reset_to_factory(self, id):
00175         '''Send the reset instruction to the servo at id.
00176            This will return the servo to ID 1 with a baudrate of 57600,
00177            and reset the EEPROM to factory defaults.  REQUIRES CONFIRMATION.
00178         '''
00179         msg = [0x06]
00180         resp = raw_input("This will reset all parameters on the Dynamixel "+\
00181                          "to factory defaults.\n"+\
00182                          "Are you sure you wish to continue? (y/n)")
00183         if resp == 'y' or resp == 'Y' or resp == 'yes':
00184             print "Resetting to factory defaults: ID = 1, baudrate=57600"
00185             return self._send_instruction(msg, id)
00186         else:
00187             print "Aborting..."
00188             return None
00189 
00190     def sync_write(self, data):
00191         '''writes data to address 0xFE (254), the broadcast address.
00192            sends data to all servos on a bus
00193         '''
00194         msg = [ 0x83 ] + data
00195         return self._send_instruction(msg, id=0xFE, status_return=False)
00196 
00197 
00198 class Dynamixel_Chain(USB2Dynamixel_Device):
00199     ''' Class that manages multiple servos on a single Dynamixel Device
00200     '''
00201     def __init__(self, dev='/dev/ttyUSB0', baudrate='57600', ids=None):
00202         ''' Accepts device file, baudrate, and a list of id numbers for servos (if known).
00203         '''
00204         USB2Dynamixel_Device.__init__(self, dev, baudrate)
00205 
00206         valid_servo_ids = self._find_servos(ids)
00207         if len(valid_servo_ids) == 0:
00208             raise RuntimeError("No valid servo IDs Found")
00209 
00210         series = [self._determine_series(self.read_model_number(id)) for id in valid_servo_ids]
00211 
00212         self.servos = {}
00213         for id, series in zip(valid_servo_ids, series):
00214             self.servos[id] = Robotis_Servo(id, series)
00215 
00216     def _find_servos(self, ids=None):
00217         ''' Finds all servo IDs on the USB2Dynamixel, or check given ids
00218         '''
00219         if ids is None:
00220             print 'Scanning for servos on all possible ID\'s'
00221             ids = range(254)
00222             suggested_ids = False
00223         else:
00224             print 'Scanning for servos with ID(\'s): %s' %ids
00225             suggested_ids = True
00226         self.servo_dev.setTimeout( 0.05 ) # To make the scan faster
00227         servos = []
00228         for i in ids:
00229             if self._id_on_device(i):
00230                 print '\n FOUND A SERVO @ ID %d\n' % i
00231                 servos.append(i)
00232             else:
00233                 if suggested_ids:
00234                     print "Cannot find ID %s on %s" %(i, self.dev_name)
00235 
00236         self.servo_dev.setTimeout( 1.0 ) # Restore to original
00237         return servos
00238 
00239     def _id_on_device(self, id):
00240         '''check for a specific id on the dynamixel device. pings servo and reads id.
00241         '''
00242         try:
00243             self.ping(id)
00244             servo_id = self.read_address(id,3)[0]
00245             assert servo_id == id, "ID value received from servo did not match id of call"
00246             return True
00247         except:
00248             return False
00249 
00250     def _determine_series(self, code):
00251         ''' Determine the series from the model number of servo with id.
00252         '''
00253         if code in [29, 310, 320]: #MX-28, MX-64, and MX-106
00254             if code == 320: #MX-106
00255                 print ("Warning: MX-106 Not fully supported. (Drive Mode/Dual Motor Joints)")
00256             return 'MX'
00257         elif code in [24, 28, 64]: #RX-24, RX-28, RX-64
00258             return 'RX'
00259         elif code == 107: #EX-106+
00260             print ("WARNING: EX-106 Devices not directly supported. Treating as 'MX'.")
00261             return 'MX'
00262         elif code in [300, 12, 18]: #AX-12W, AX-12, AX-18
00263             print ("WARNING: AX-series devices not directly supported. Treating as 'MX'.")
00264             return 'MX'
00265         else:
00266             raise RuntimeError("Servo ID: %d has unknown servo model code: %d" %(id, code))
00267 
00268     def read_model_number(self, id):
00269         ''' Read the model number (byte-code) of the servo at id.
00270         '''
00271         data = self.read_address(id, 0x00, 2)
00272         return data[0] + data[1]*256
00273 
00274     def read_firmware_version(self, id):
00275         '''  Read the firmware version on the servo at id.
00276         '''
00277         return self.read_address(id, 0x02)[0]
00278 
00279     def set_id(self, current_id, new_id):
00280         ''' changes the servo id from current_id to new_id
00281         '''
00282         if (new_id < 0) or (new_id > 253):
00283             raise RuntimeWarning("Robotis ID must be between 0 and 253")
00284         resp = self.write_address(current_id, 0x03, [new_id])
00285         valid_servo_ids = self._find_servos([new_id])
00286         self.servos[new_id] = Robotis_Servo(new_id, series=self.servos[current_id].series)
00287         self.servos.pop(current_id)
00288         return resp
00289 
00290     def set_baudrate(self, id, baudrate=0x22):
00291         ''' Set the baudrate of the servo at id. Smaller == Faster. Default: 34 -> 57600.
00292         '''
00293         return self.write_address(id, 0x04, [baudrate])
00294 
00295     def read_baudrate(self, id):
00296         ''' Read the currently set baudrate digit of servo with ID.
00297         '''
00298         code = self.read_address(id, 0x04)[0]
00299         if code < 249:
00300             return int(2000000./(code+1))
00301         elif code == 250:
00302             return 2250000
00303         elif code == 251:
00304             return 2500000
00305         elif code == 252:
00306             return 3000000
00307         else:
00308             raise RuntimeError("Received unknown code for baudrate: %d" %code)
00309 
00310     def set_return_delay(self, id, delay=250):
00311         ''' Set Return Delay Time (0-500 microseconds). Default=250.
00312         '''
00313         if delay < 0:
00314             delay = 0
00315             print("Return Delay Time must be non-negative. Setting to 0 microseconds.")
00316         elif delay > 500:
00317             delay = 500
00318             print("Return Delay Time must be less than 500 (microseconds). Setting to 500us.")
00319         elif (delay % 2 != 0):
00320             delay = 2*int(delay/2)
00321             print("Return Delay Time must be specified by 2 microsecond increments. Rounding to %dus" %delay)
00322         return self.write_address(id, 0x05, [int(delay/2)])
00323 
00324     def read_return_delay(self, id):
00325         '''Read the currently set Return Delay Time (in microseconds)'''
00326         ret_dly =  self.read_address(id, 0x05)
00327         return 2*ret_dly[0]
00328 
00329     def set_angle_limits(self, id, cw_limit=0., ccw_limit=2*math.pi):
00330         ''' Set the angular limits (in radians) on the motor. Should specify both cw and ccw limits
00331         '''
00332         cw_enc = self.servos[id].angle_to_encoder(cw_limit)
00333         ccw_enc = self.servos[id].angle_to_encoder(ccw_limit)
00334         cw_hi, cw_lo = self.__encoder_to_bytes(id, cw_enc)
00335         ccw_hi, ccw_lo = self.__encoder_to_bytes(id, ccw_enc)
00336         return self.write_address(id, 0x06, [cw_lo, cw_hi, ccw_lo, ccw_hi])
00337 
00338     def read_angle_limits(self, id):
00339         ''' Read the angle limits (in radians) set on the servo.  Returns [cw_limit, ccw_limit]
00340         '''
00341         data = self.read_address(id, 0x06, 4)
00342         cw = data[0]+data[1]*256
00343         ccw = data[2]+data[3]*256
00344         cw_lim = self.servos[id].encoder_to_angle(cw)
00345         ccw_lim = self.servos[id].encoder_to_angle(ccw)
00346         return [cw_lim, ccw_lim]
00347 
00348     def is_cont_turn_enabled(self, id):
00349         ''' Return whether continuous turn is enabled based on the joint angle limits.
00350         '''
00351         return (self.read_address(id, 6, 4) == [0]*4)
00352 
00353     def enable_cont_turn(self, id):
00354         ''' Sets angle limits to zero, allowing continuous turning (good for wheels).
00355         After calling this method, simply use 'set_angvel' to command rotation.  This
00356         rotation is proportional to torque according to Robotis documentation.
00357         '''
00358         return self.write_address(id, 6, [0]*4)
00359 
00360     def disable_cont_turn(self, id):
00361         ''' Resets CCW angle limits to defaults to allow commands through 'move_angle' again.
00362         '''
00363         return self.set_angle_limits(id)
00364 
00365     def read_temperature_limit(self, id):
00366         ''' Read the temperature alarm threshold in degrees C.  Default: 80C.
00367             Should not change.
00368         '''
00369         return self.read_address(id, 0x0B, 1)[0]
00370 
00371     def read_temperature(self, id):
00372         ''' returns the temperature (Celcius) of servo (id).
00373         '''
00374         data = self.read_address(id, 0x2B, 1 )
00375         return data[0]
00376 
00377     def read_voltage_limits(self, id):
00378         ''' Read the lower and upper voltage alarm limits. Defaults: 6.0V - 16.0V.
00379         '''
00380         data = self.read_address(id, 0x0C, 2)
00381         return  [ data[0]*0.1, data[1]*0.1 ] #0.1V/unit
00382 
00383     def set_voltage_limits(self, id, lower=6.0, upper=16.0):
00384         ''' Set the lower and upper voltage alarm limits. Defaults: 6.0V - 16.0V.
00385         '''
00386         low_limit = int( 10. * lower ) #0.1V/unit
00387         high_limit = int( 10. * upper )
00388         return self.write_address(id, 0x0C, [low_limit, high_limit])
00389 
00390     def read_voltage(self, id):
00391         ''' returns voltage (Volts) seen by servo (id).
00392         '''
00393         data = self.read_address(id, 0x2A, 1 )
00394         return data[0] / 10.
00395 
00396     def read_max_torque(self, id):
00397         ''' Read the current max torque setting (as a percentage of possible torque).
00398         '''
00399         data = self.read_address(id, 0x0E, 2)
00400         return (data[0] + data[1]*256) / 10.23
00401 
00402     def set_max_torque(self, id, percent=100):
00403         ''' Set the max torque as a percent of possible torque.
00404             Will trigger alarm and shutdown if exceeded.
00405         '''
00406         data = 10.23 * percent
00407         hi = int( data / 256 )
00408         lo = int( data % 256 )
00409         return self.write_address(id, 0x0E, [lo, hi])
00410 
00411     def read_status_return_level(self, id):
00412         ''' Read the current status return label of servo at id.
00413             0 - returns status packet only for PING command
00414             1 - returns status packet only for read commands
00415             2 - returns sttaus packet for all commands
00416         '''
00417         return self.read_address(id, 0x10)
00418 
00419     def set_status_return_level(self, id, level=2):
00420         ''' Set the current status return label of servo at id.
00421             0 - returns status packet only for PING command
00422             1 - returns status packet only for read commands
00423             2 - returns sttaus packet for all commands
00424         '''
00425         if not level in [0, 1, 2]:
00426             raise RuntimeError('Status Return Level must be one of: \n' \
00427                                '\t0 - No return except ping \n'\
00428                                '\t1 - return only for read commands \n'\
00429                                '\t2 - return for all commands')
00430         return self.write_address(id, 0x10, [level])
00431 
00432     def is_torque_enabled(self, id):
00433         ''' Return True if sending power to motor, False otherwise.
00434         '''
00435         data = self.read_address(id, 0x18)
00436         return bool( data[0] )
00437 
00438     def enable_torque(self, id):
00439         ''' Enable torque production by impressing power to the motor.
00440         '''
00441         return self.write_address(id, 0x18, [1])
00442 
00443     def disable_torque(self, id):
00444         ''' Disable torque production by interrupting power to the motor.
00445         '''
00446         return self.write_address(id, 0x18, [0])
00447 
00448     def read_load(self, id):
00449         ''' Alias for read_torque.
00450         '''
00451         return self.read_torque(id)
00452 
00453     def read_torque(self, id):
00454         ''' Read the current load as a percentage of (possibly) maximum torque.
00455             CW -> load < 0.  CCW -> load > 0.
00456             Should be used for direction of torque only.
00457         '''
00458         data = self.read_address(id, 0x28, 2)
00459         hi = data[1] & 3 #grab left two bits
00460         val = data[0] + hi*256
00461         load =  val/10.24 #percent of 0-1024 range
00462         if (data[1] & 4) != self.servos[id].settings['flipped']: #Check direction bit and servo flipped. If only one =True, then flip.
00463             load *= -1.
00464         return load
00465 
00466     def is_led_on(self, id):
00467         ''' Return True if LED is ON, False if LED is off.
00468         '''
00469         data = self.read_address(id, 0x19, 1)
00470         return bool( data[0] )
00471 
00472     def set_led(self, id, on=True):
00473         ''' Set the status of the LED on or off.
00474         '''
00475         return self.write_address(id, 0x19, [on])
00476 
00477     def read_compliance_margins(self, id):
00478         ''' Read the compliance margin (deadband around goal position) of servo with id.
00479             Returns [CW, CCW] angular deadband in radians (always positive).
00480         '''
00481         cw, ccw = self.read_address(id, 0x1A, 2)
00482         cw_rad = cw * self.servos[id].settings['rad_per_enc']
00483         ccw_rad = ccw * self.servos[id].settings['rad_per_enc']
00484         return [cw_rad, ccw_rad]
00485 
00486     def set_compliance_margins(self, id, cw=None, ccw=None):
00487         if (cw is None) and (ccw is None):
00488             print("Setting CW and CCW compliance margins to %f (one encoder step)."
00489                     %self.servos[id].settings['rad_per_enc'])
00490             cw_enc = ccw_enc = 1
00491         elif (cw is None) or (ccw is None):
00492             print("Setting both CW and CCW compliance margins to %f."
00493                     %max(cw, ccw)) #select whichever is not None.
00494             cw_enc = ccw_enc = int(round(max(cw, ccw) / self.servos[id].settings['rad_per_enc']))
00495         else:
00496             cw_enc = int(round( cw / self.servos[id].settings['rad_per_enc']))
00497             ccw_enc = int(round( ccw / self.servos[id].settings['rad_per_enc']))
00498 
00499         encs = [cw_enc, ccw_enc]
00500         for i, enc in enumerate(encs):
00501             if (enc > 254):
00502                 print("WARNING: Compliance margin must be less than {0}, clipping to {0}".format(
00503                         254*self.servos[id].settings['rad_per_enc']))
00504                 encs[i] = 254
00505         return self.write_address(id,  0x1A, encs)
00506 
00507     def _compliance_slope_to_step(self, val):
00508         if val in range(3):
00509             return 1
00510         elif val in range(4,7):
00511             return 2
00512         elif val in range(8, 15):
00513             return 3
00514         elif val in range(16, 31):
00515             return 4
00516         elif val in range(32, 63):
00517             return 5
00518         elif val in range(64, 127):
00519             return 6
00520         elif val in range(128, 254):
00521             return 7
00522         else:
00523             raise RuntimeError("Received out-of-range compliance slope: %s. Must be in range(254)" %val)
00524 
00525     def _compliance_step_to_slope(self, step):
00526         try:
00527             return {1: 2,
00528                     2: 4,
00529                     3: 8,
00530                     4: 16,
00531                     5: 32,
00532                     6: 64,
00533                     7: 128}[step]
00534         except:
00535             print "Compliance slope must be in range(7)"
00536             raise
00537 
00538     def read_compliance_slopes(self, id):
00539         ''' Read the CW and CCW compliance slopes as steps from 1-7 (1=stiffer, 7=more flexible').
00540         '''
00541         data = self.read_address(id, 0x1C, 2)
00542         return [self._compliance_slope_to_step(v) for v in data]
00543 
00544     def set_compliance_slopes(self, id, cw=None, ccw=None):
00545         if (cw is None) and (ccw is None):
00546             print("Setting CW and CCW compliance slopes to level 5 (default).")
00547             data = [32, 32]
00548         elif (cw is None) or (ccw is None):
00549             data = [self._compliance_step_to_slope(max(cw, ccw))] * 2
00550             print("Setting both CW and CCW compliance slopes to level %d." %data[0])
00551         else:
00552             cw_step = self._compliance_step_to_slope(cw)
00553             ccw_step = self._compliance_step_to_slope(ccw)
00554             data = [cw_step, ccw_step]
00555         return self.write_address(id, 0x1C, data)
00556 
00557     def read_pid_gains(self, id):
00558         ''' Read the PID gains currently set on the servo.
00559             Returns: [kp, ki, kd] (gain coefficients)
00560         '''
00561         data = self.read_address(id, 0x1A, 3)
00562         kd = data[0] * 4. / 1000.
00563         ki = data[1] * 1000. / 2048.
00564         kp = data[2] / 8.
00565         return [kp, ki, kd]
00566 
00567     def set_pid_gains(self, id, kp=4., ki=0., kd=0.):
00568         ''' Set the PID gain coefficients on the servo.
00569             0 <= kp < 31.875
00570             0 <= ki < 124.5177
00571             0 <= kd < 1.02
00572         '''
00573         if (kp < 0) or (kd < 0 ) or (ki < 0 ):
00574             raise RuntimeError('All PID gains must be positive.')
00575         if kp >= 32.:
00576             print "Warning: Kp gain must be within: 0 <= kp < 31.875. Setting to max."
00577             p_data = 254
00578         else:
00579             p_data = int( kp * 8. )
00580         if ki >= 125.:
00581             print "Warning: Ki gain must be within: 0 <= ki < 124.5177. Setting to max."
00582             i_data = 254
00583         else:
00584             i_data = int( ki * 2048. / 1000. )
00585         if kd >= 1.02:
00586             print "Warning: Kd gain must be within: 0 <= kd < 1.02. Setting to max."
00587             d_data = 254
00588         else:
00589             d_data = int( kd * 1000. / 4. )
00590         return self.write_address(id, 0x1A, [d_data, i_data, p_data])
00591 
00592     def read_goal_position(self, id):
00593         ''' Read the currently set goal angle in radians of servo with id.
00594         '''
00595         data = self.read_address(id, 0x1E, 2)
00596         enc_goal = data[0] + data[1]*256
00597         return self.servos[id].encoder_to_angle(enc_goal)
00598 
00599     def read_goal_angvel(self, id):
00600         ''' Read the currently set desired moving speed of servo with id.
00601         '''
00602         data = self.read_address(id, 0x20, 2)
00603         return self.servos[id].bytes_to_angvel(data[1], data[0])
00604 
00605     def read_torque_limit(self, id):
00606         ''' Read the currently set torque limit as a percentage of acheivable torque.
00607             Torque produced by the motor will be capped to this value.
00608         '''
00609         data = self.read_address(id, 0x22, 2)
00610         return (data[0] + data[1]*256) / 10.23
00611 
00612     def set_torque_limit(self, id, percent=None):
00613         ''' Set the torque limit as a percent of possible torque.
00614             Torque produced by the motor will be capped to this value.
00615         '''
00616         if percent is None:
00617             percent = self.read_max_torque(id)
00618             print "No percent specified.  Setting to %s, "\
00619                    "the current torque alarm threshold." %percent
00620         data = 10.23 * percent
00621         hi = int( data / 256 )
00622         lo = int( data % 256 )
00623         return self.write_address(id, 0x22, [lo, hi])
00624 
00625     def read_encoder(self, id):
00626         ''' returns position in encoder ticks of servo at id.
00627         '''
00628         data = self.read_address(id, 0x24, 2)
00629         enc_val = data[0] + data[1] * 256
00630         return enc_val
00631 
00632     def read_angle(self, id):
00633         ''' returns the angle (radians) of servo at id.
00634         '''
00635         return self.servos[id].encoder_to_angle(self.read_encoder(id))
00636 
00637     def read_angles(self, ids=None):
00638         ''' return a list of current joint angles for servos with given ids
00639         '''
00640         if ids is None:
00641             ids = self.servos.keys()
00642         angles = [self.read_angle(id) for id in ids]
00643         return angles, ids
00644 
00645     def read_angvel(self, id):
00646         ''' returns the angular velocity (rad/s) of servo at id.
00647         '''
00648         data = self.read_address(id, 38, 2)
00649         angvel = self.servos[id].bytes_to_angvel(data[1], data[0])
00650         return angvel
00651 
00652     def read_angvels(self, ids=None):
00653         '''return a list of current angular velocities for servos with given ids
00654         '''
00655         if ids is None:
00656             ids = self.servos.keys()
00657         angvels = [self.read_angvel(id) for id in ids]
00658         return angvels, ids
00659 
00660     def read_ang_angvel(self, id):
00661         '''returns the angular position and angular velocity from a single read
00662         '''
00663         data = self.read_address(id, 36, 4)
00664         enc_val = data[0] + data[1] * 256
00665         ang = self.servos[id].encoder_to_angle(enc_val)
00666         angvel = self.servos[id].bytes_to_angvel(data[3], data[2])
00667         return ang, angvel
00668 
00669     def read_angs_angvels(self, ids=None):
00670         '''return lists of current angular positions and velocities for given ids
00671         '''
00672         if ids is None:
00673             ids = self.servos.keys()
00674         angles = []
00675         angvels = []
00676         for id in ids:
00677             ang, angvel = self.read_ang_angvel(id)
00678             angles.append(ang)
00679             angvels.append(angvel)
00680         return angles, angvels, ids
00681 
00682     def move_angle(self, id, ang, angvel=None, blocking=False):
00683         ''' move servo with id to angle (radians) with velocity (rad/s)
00684         '''
00685         if angvel is None:
00686             angvel = self.servos[id].settings['max_speed']
00687         else:
00688             angvel = self.servos[id].clip_angvel(angvel)
00689         av_hi, av_lo = self.servos[id].angvel_to_bytes(angvel)
00690         ang = self.servos[id].clip_angle(ang)
00691         enc_val = self.servos[id].angle_to_encoder(ang)
00692         ang_hi, ang_lo = self.__encoder_to_bytes(id, enc_val)
00693         self.write_address(id, 30, [ang_lo, ang_hi, av_lo, av_hi])
00694 
00695         if blocking == True:
00696             while(self.is_moving(id)):
00697                 continue
00698 
00699     def move_angles_sync(self, ids, angs, angvels=None) :
00700         ''' move servos with id's to angles with angvels using a single sync_write.
00701             clips angles to allowed range, and limits angvel to max allowed.
00702         '''
00703         if angvels is None:
00704             angvels = [self.servos[id_].settings['max_speed'] for id_ in ids]
00705         else:
00706             if len(angvels) != len(ids):
00707                 raise RuntimeError("Number of ids and anvels do not match.")
00708             else:
00709                 angvels  = [ self.servos[id_].clip_angvel(angvel) for id_, angvel in zip(ids, angvels) ]
00710         #Check that there is an angle, angvel for each id
00711         assert len(ids) == len(angvels) ,  "Number of ids and angvels do not match"
00712         assert len(ids) == len(angs) , "Number of ids and angles do not match"
00713 
00714         msg = [0x1E, 0x04] #Start address, length of data per servo (4 bytes)
00715         for id, ang, vel in zip(ids, angs, angvels):
00716             servo = self.servos[id]
00717             ang = servo.clip_angle(ang)
00718             enc_tics = servo.angle_to_encoder(ang)
00719             ang_hi, ang_lo = self.__encoder_to_bytes(id, enc_tics)
00720             new_vel = servo.clip_angvel(vel)
00721             vel_hi, vel_lo = self.servos[id].angvel_to_bytes(vel)
00722             msg.extend([id, ang_lo, ang_hi, vel_lo, vel_hi])
00723         self.sync_write(msg)
00724 
00725     def move_to_encoder(self, id, n):
00726         ''' move to encoder position n
00727         '''
00728         hi, lo = self.__encoder_to_bytes(id, n)
00729         return self.write_address(id, 0x1E, [lo,hi] )
00730 
00731     def __encoder_to_bytes(self, id, n):
00732         ''' convert encoder value to hi, lo bytes
00733         '''
00734         # In some border cases, we can end up above/below the encoder limits.
00735         # eg. int(round(math.radians(180) / (math.radians(360)/0xFFF ))) + 0x7FF => -1
00736         n = min( max( n, 0 ), self.servos[id].settings['max_encoder'] )
00737         hi, lo = n / 256, n % 256
00738         return hi, lo
00739 
00740 
00741     def set_angvel(self, id, angvel):
00742         ''' set angvel (rad/s) of servo id
00743         '''
00744         hi, lo =  self.servos[id].angvel_to_bytes(angvel)
00745         return self.write_address(id, 0x20, [lo, hi] )
00746 
00747     def is_moving(self, id):
00748         ''' Returns True if servo (id) is moving, False otherwise.
00749         '''
00750         data = self.read_address(id, 0x2e, 1 )
00751         return data[0] != 0
00752 
00753     def is_eeprom_locked(self, id):
00754         ''' Return True if the EEPROM of servo at id is locked, False if not.
00755         '''
00756         return bool( self.read_address(id, 0x2F)[0] )
00757 
00758     def lock_eeprom(self, id):
00759         ''' Lock the EEPROM of servo at ID (will prevent changes to EEPROM bits).
00760             Lock can only be reset by power-cycling the servo.
00761         '''
00762         return self.write_address(id, 0x2F, [0x01])
00763 
00764     def read_punch(self, id):
00765         ''' Read the currently set minimum motor current.
00766             UNITS UNKNOWN.  Values in range 0 - 1023. Default: 0.
00767         '''
00768         data = self.read_address(id, 0x30, 2)
00769         return data[0] + data[1]*256
00770 
00771     def set_punch(self, id, value=0):
00772         ''' Set the minimum motor current.
00773             UNITS UNKNOWN.  Values in range 0 - 1023. Default: 0.
00774         '''
00775         hi, lo = value / 256, value % 256
00776         return self.write_address(id, 0x30, [lo, hi])
00777 
00778     def read_current(self, id):
00779         ''' Read the current (in Amps) currently in the motor.
00780         '''
00781         data = self.read_address(id, 0x44, 2)
00782         val = data[0] + data[1]*256
00783         return 0.0045 * (val - 2048.) #4.5mA/digit
00784 
00785     def is_torque_control_enabled(self, id):
00786         ''' Return True if servo at id is currently set for torque control, False otherwise.
00787         '''
00788         return bool( self.read_address(id, 0x46, 1)[0] )
00789 
00790     def enable_torque_control(self, id):
00791         ''' Enable torque control mode.  Goal position and moving speed will be ignored.
00792         '''
00793         return self.write_address(id, 0x46, [1])
00794 
00795     def disable_torque_control(self, id):
00796         ''' Disable torque control mode.  Goal position and moving speed will be used instead.
00797         '''
00798         return self.write_address(id, 0x46, [0])
00799 
00800     def read_goal_torque(self, id):
00801         ''' Read the currently set goal torque (used in torque control mode).  Units in Amps.  Torque produces is a function of motor current.
00802         '''
00803         data = self.read_address(id, 0x47, 2)
00804         hi = data[1] & 3 #grab left two bits
00805         val = data[0] + hi*256
00806         pct =  val/10.24 #percent of 0-1024 range
00807         if data[1] & 4: #Check 3rd bit in second byte -- gives direction
00808             return -pct
00809         else:
00810             return pct
00811 
00812     def set_goal_torque(self, id, percent=100):
00813         ''' Set the goal torque as a percentage of the maximum torque.
00814             100 % -> Max torque CCW.  -100 % -> Max torque CW.
00815         '''
00816         if percent <= 0:
00817             hi = 4
00818             percent *= -1
00819         else:
00820             hi = 0
00821         val = int(round(percent * 10.23))
00822         hi = hi | (val >> 8) #place left two bits on upper byte
00823         lo = val & 255 #grab only lower byte
00824         return self.write_address( id, 0x47, [lo, hi] )
00825 
00826     def read_goal_acceleration(self, id):
00827         data = self.read_address(id, 0x49)
00828         return data[0] * 8.583 #8.583 rad/sec^2 per unit
00829 
00830     def set_goal_acceleration(self, id, ang_acc=0):
00831         ''' Set the goal acceleration (0 <--> 2018 rads/sec^2)
00832             If goal angular velocity is set to 0, will move with max acceleration.
00833             If goal acceleration is set to 0 (default), will move with max acceleration.
00834         '''
00835         val = int(round(ang_acc/8.583))
00836         return self.write_address(id, 0x49, [val])
00837 
00838 class Robotis_Servo():
00839     ''' Class to use a robotis RX-28 or RX-64 servo.
00840     '''
00841     def __init__(self, servo_id, series = 'RX' ):
00842         '''servo_id - servo ids connected to USB2Dynamixel 1,2,3,4 ... (1 to 253)
00843                        [0 is broadcast if memory serves]
00844             series - Just a convenience for defining "good" defaults on MX series.
00845                      When set to "MX" it uses these values, otherwise it uses values
00846                      better for AX / RX series.  Any of the defaults can be overloaded
00847                      on a servo-by-servo bases in servo_config.py
00848         '''
00849         self.servo_id = servo_id
00850         self.series = series
00851         # To change the defaults, load some or all changes into servo_config.py
00852         if series == 'MX':
00853             defaults = {
00854                 'home_encoder': 0x7FF,
00855                 'max_encoder': 0xFFF,
00856                 'rad_per_enc': 2*math.pi / 0xFFF,
00857                 'max_ang': math.pi,
00858                 'min_ang': -math.pi,
00859                 'flipped': False,
00860                 'max_speed': 0.
00861                 }
00862         elif series == 'RX': # Common settings for RX-series.  Can overload in servo_config.py
00863             defaults = {
00864                 'home_encoder': 0x200,
00865                 'max_encoder': 0x3FF,  # Assumes 0 is min.
00866                 'rad_per_enc': math.radians(300.0) / 1024.0,
00867                 'max_ang': math.radians(150),
00868                 'min_ang': math.radians(-150),
00869                 'flipped': False,
00870                 'max_speed': 0.
00871                 }
00872         else:
00873             raise RuntimeError('Servo ID %d has unrecognized Series name: %s' %(self.servo_id, self.series))
00874 
00875         # Set various parameters.  Load from servo_config.
00876         self.settings = defaults
00877         try:
00878             import servo_config as sc
00879             if sc.servo_param.has_key( self.servo_id ):
00880                 print "Using servo_config.py settings for Servo %d" %self.servo_id
00881                 for key, val in sc.servo_param [ self.servo_id ].iteritems():
00882                     self.settings[key] = val
00883             else:
00884                 print 'Servo ID %d not found in servo_config.py.  Using defaults.' %self.servo_id
00885         except:
00886             print 'Servo_config.py configuration file not found.  Using defaults.'
00887         print "Created new %s-series Robotis Servo at ID %d" %(series, servo_id)
00888 
00889         #If max speed is negative or above possible limit, set to 0 (always use max speed)
00890         if (self.settings['max_speed'] < 0) or (self.settings['max_speed'] > 12.2595):
00891             print "Servo %d: Setting default servo angular velocity to maximum possible." %self.servo_id
00892             self.settings['max_speed'] = 0
00893 
00894     def angle_to_encoder(self, ang):
00895         ''' return encoder position for given angle (radians)
00896         '''
00897         if self.settings['flipped']:
00898             ang *= -1.0
00899         enc_tics = int(round( ang / self.settings['rad_per_enc'] ))
00900         enc_tics += self.settings['home_encoder']
00901         return enc_tics
00902 
00903     def encoder_to_angle(self, enc_val):
00904         '''return angular position (rad) from given encoder position
00905         '''
00906         ang = ((enc_val - self.settings['home_encoder']) *
00907                 self.settings['rad_per_enc'])
00908         if self.settings['flipped']:
00909             ang *= -1.0
00910         return ang
00911 
00912     def clip_angle(self, ang):
00913         ''' Clip commanded joint angles to within the allowed range.
00914         '''
00915         if ang < self.settings['min_ang']:
00916             print "Servo %d: Commanded angle (%f) below minimum (%f), commanding to minimum."\
00917                     %(self.servo_id, ang, self.settings['min_ang'])
00918             return self.settings['min_ang']
00919         elif ang > self.settings['max_ang']:
00920             print "Servo %d: Commanded angle (%f) above maximum (%f), commanding to maximum."\
00921                     %(self.servo_id, ang, self.settings['max_ang'])
00922             return self.settings['max_ang']
00923         else:
00924             return ang
00925 
00926     def angvel_to_bytes(self, angvel):
00927         ''' Convert Angular velocity, in rad/sec, to hi, lo bytes.
00928         '''
00929         if self.settings['flipped']:
00930             angvel *= -1.
00931         rpm = angvel / (2 * math.pi) * 60.0
00932         angvel_enc = int(round( rpm / 0.11443 ))
00933         hi = abs(angvel_enc) / 256
00934         lo = abs(angvel_enc) % 256
00935         if angvel_enc < 0:
00936             hi += 4 #correct direction bit
00937         return hi, lo
00938 
00939     def bytes_to_angvel(self, hi, lo):
00940         '''returns the current angular velocity from hi, lo bytes
00941         '''
00942         val = lo + hi * 256
00943         raw_mag = (val % 1024) * 0.11443 #binary value ~= 0.11rpm/unit
00944         mag = (raw_mag / 60.) * 2 * math.pi
00945         if hi >> 2 == 1: #check direction bit
00946             mag *= -1
00947         if self.settings['flipped']:
00948             mag *= -1
00949         return mag
00950 
00951     def clip_angvel(self, angvel):
00952         '''Clip commanded velocity to below the allowed maximum.
00953            negative angvels will be set to maximum.
00954         '''
00955         if self.settings['max_speed'] == 0.:
00956             if abs(angvel) > 12.2595:
00957                 print("Servo %d: Tried to set ang vel to %f, "
00958                         "above robotis allowed range (%f), "
00959                         "setting to maximum (%f)."
00960                         %(self.servo_id, angvel, 12.2595 , 12.2595))
00961                 return np.clip(angvel, -12.2595, 12.2595)
00962             else:
00963                 return angvel
00964         elif abs(angvel) > self.settings['max_speed']:
00965             print("Servo %d: Tried to set ang vel to %f, "
00966                   "above configured maximum (%f), "
00967                   "setting to maximum (%f)."
00968                   %(self.servo_id, angvel,
00969                     self.settings['max_speed'], self.settings['max_speed']))
00970             return np.clip(angvel, -self.settings['max_speed'], self.settings['max_speed'])
00971         else:
00972             return angvel
00973 
00974 
00975 def discover_servos(dev='/dev/ttyUSB0', ids=None, baudrates=None, number=255):
00976     '''Discover all servos on a USB2Dynamixel_Device using PING command.
00977        Checks all servo IDs at all Baudrates, stopping after 'number' of servos are found.
00978        Can specify smaller ranges to check instead.
00979     '''
00980     if baudrates is None:
00981         baudrates = [9600, 19200, 57600, 115200, 200000, 250000, 400000, 500000, 1000000, 2250000, 2500000, 3000000]
00982     print "Searching for ID's on %s" %dev
00983     num = 0
00984     for baudrate in baudrates:
00985         print "\nBaudrate %d:" %baudrate
00986         try:
00987             dyn = Dynamixel_Chain(dev, baudrate=baudrate, ids = ids)
00988             num += len(dyn.servos.keys())
00989             dyn.servo_dev.close()
00990             del(dyn)
00991             if num >= number:
00992                 print "Found %d servos. Stopping." %num
00993                 break
00994         except RuntimeError as rte:
00995             print rte
00996 
00997 def recover_servo(dyn):
00998     import time
00999     ''' Recovers a bricked servo by booting into diagnostic bootloader and resetting '''
01000     raw_input('Make sure only one servo connected to USB2Dynamixel Device [ENTER]')
01001     raw_input('Disconnect power from the servo, but leave USB2Dynamixel connected to USB. [ENTER]')
01002 
01003     dyn.servo_dev.setBaudrate( 57600 )
01004 
01005     print 'Get Ready.  Be ready to reconnect servo power when I say \'GO!\''
01006     print 'After a second, the red LED should become permanently lit.'
01007     print 'After that happens, Ctrl + C to kill this program.'
01008     print
01009     print 'Then, you will need to use a serial terminal to issue additional commands.',
01010     print 'Here is an example using screen as serial terminal:'
01011     print
01012     print 'Command Line:  screen /dev/robot/servo_left 57600'
01013     print 'Type: \'h\''
01014     print 'Response: Command : L(oad),G(o),S(ystem),A(pplication),R(eset),D(ump),C(lear)'
01015     print 'Type: \'C\''
01016     print 'Response:  * Clear EEPROM '
01017     print 'Type: \'A\''
01018     print 'Response: * Application Mode'
01019     print 'Type: \'G\''
01020     print 'Response:  * Go'
01021     print
01022     print 'Should now be able to reconnect to the servo using ID 1'
01023     print
01024     print
01025     raw_input('Ready to reconnect power? [ENTER]')
01026     print 'GO!'
01027 
01028     while True:
01029         s.write('#')
01030         time.sleep(0.0001)
01031 
01032 def test_servos(dyn, ids=None):
01033     ''' An incomplete test script.  Will call most, but not all,
01034         functions on a servo and check for obvious problems.
01035     '''
01036     def confirm():
01037         rsp = raw_input("Do you wish to proceed? (y/n)")
01038         if rsp == 'y':
01039             return True
01040         elif rsp == 'n':
01041             return False
01042         else:
01043             print "Please enter 'y' or 'n'"
01044             return confirm()
01045 
01046     print("This test function will test almost all functions available on the "
01047             "specified servos, including continuous motion, and should not be "
01048             "performed if the servo is attached to external hardware "
01049             "that cannot rotate freely.")
01050     if not confirm():
01051         print "Aborting..."
01052         return
01053 
01054     import time
01055     if ids is None:
01056         ids = dyn.servos.keys()
01057     for id in ids:
01058         servo = dyn.servos[id]
01059         assert dyn.ping(id) == []
01060         assert dyn.dev_name == dyn.servo_dev.port
01061         print "Device Name: %s" %dyn.dev_name
01062         model_code = dyn.read_model_number(id)
01063         print "Model Code: %s" %model_code
01064         assert dyn._determine_series(model_code) ==  servo.series
01065         print "Series: %s" %servo.series
01066         print "Firmware version: %s" %dyn.read_firmware_version(id)
01067         assert dyn.read_address(id, 3, 1) == [id]
01068         print "ID: %d" %id
01069         assert np.allclose(dyn.read_baudrate(id), dyn.servo_dev.baudrate, rtol=0.01) #within 1% diff
01070         print "Baudrate: %s" %dyn.servo_dev.baudrate
01071         assert dyn.read_return_delay(id) in range(0,502,2)
01072         print "Return Delay Time: %s (microseconds)" %dyn.read_return_delay(id)
01073         print "Is EEPROM Locked? %s" %dyn.is_eeprom_locked(id)
01074         cw, ccw = dyn.read_angle_limits(id)
01075         print "Servo EEPROM angle limits: %s" %[cw, ccw]
01076         print "Software angle limits: %s" %[servo.settings['min_ang'], servo.settings['max_ang']]
01077         assert np.allclose([cw, ccw], [servo.settings['min_ang'], servo.settings['max_ang']], rtol=0.005)
01078         print "Temperature Alarm Limit: %d C" %dyn.read_temperature_limit(id)
01079         print "Present Temperature: %d C" %dyn.read_temperature(id)
01080         assert dyn.read_temperature(id) < dyn.read_temperature_limit(id)
01081 
01082         ## Voltage
01083         voltage_limits = dyn.read_voltage_limits(id)
01084         print "Voltage Limits: %sV -- %sV" %(voltage_limits[0], voltage_limits[1])
01085         voltage = dyn.read_voltage(id)
01086         print "Current Voltage: %sV" %voltage
01087         assert (voltage >= voltage_limits[0]) and (voltage <= voltage_limits[1])
01088         print
01089 
01090         ## Torque controls/settings
01091         torque_limit = dyn.read_max_torque(id)
01092         print "Torque Limit: %s%%" %torque_limit
01093         torque_cap = dyn.read_torque_limit(id)
01094         print "Torque output cap: %s%%" %torque_cap
01095         load = dyn.read_load(id)
01096         torque = dyn.read_torque(id)
01097         print "Current Torque: %s%%" %torque
01098         assert torque == load
01099         assert torque < torque_limit
01100         assert torque < torque_cap
01101         assert torque_cap <= torque_limit
01102         print "Enabling Torque"
01103         dyn.enable_torque(id)
01104         print "Torque Enabled: %s" %dyn.is_torque_enabled(id)
01105         assert dyn.is_torque_enabled(id)
01106         print "Disabling Torque"
01107         dyn.disable_torque(id)
01108         print "Torque Enabled: %s" %dyn.is_torque_enabled(id)
01109         assert not dyn.is_torque_enabled(id)
01110         print "Re-enabling torque"
01111         dyn.enable_torque(id)
01112         print
01113 
01114         #Status Return Level
01115         dyn.set_status_return_level(id, 2)
01116         srl = dyn.read_status_return_level(id)[0]
01117         print "Status Return Level: %s" %srl
01118         assert srl == 2
01119         print
01120 
01121         ## LED
01122         print "Turning LED On"
01123         dyn.set_led(id, True)
01124         assert dyn.is_led_on(id)
01125         time.sleep(2)
01126         dyn.set_led(id, False)
01127         assert not dyn.is_led_on(id)
01128         print
01129 
01130         #Gains
01131         if servo.series == 'MX':
01132             p, i, d = dyn.read_pid_gains(id)
01133             print "PID Gains:\n\tP:%s\n\tI:%s\n\tD:%s" %(p, i, d)
01134             print "Setting PID Gains to near max. (31, 124, 1)"
01135             dyn.set_pid_gains(id, kp=31, ki=124, kd=1)
01136             p1, i1, d1 = dyn.read_pid_gains(id)
01137             print "PID Gains:\n\tP:%s\n\tI:%s\n\tD:%s" %(p1, i1, d1)
01138             assert np.allclose(p1, 31, rtol=0.01)
01139             assert np.allclose(i1, 124, rtol=0.01)
01140             assert np.allclose(d1, 1., rtol=0.01)
01141             dyn.set_pid_gains(id, kp=p, ki=i, kd=d)
01142         elif servo.series == 'RX':
01143             cms = dyn.read_compliance_margins(id)
01144             print "Compliance Margins:\n\tCW: %s\n\tCCW: %s" %tuple(cms)
01145             print "Setting compliance margins to [PI/8, PI/7]"
01146             dyn.set_compliance_margins(id, cw=np.pi/8, ccw=np.pi/7)
01147             print "Compliance Margins:\n\tCW: %s\n\tCCW: %s" %tuple(dyn.read_compliance_margins(id))
01148             assert np.allclose([np.pi/8, np.pi/7], dyn.read_compliance_margins(id), rtol=0.01)
01149             dyn.set_compliance_margins(id, cw=cms[0], ccw=cms[1])
01150             css = dyn.read_compliance_slopes(id)
01151             print "Compliance Slopes:\n\tCW: %s\n\tCCW: %s" %tuple(css)
01152             print "Setting compliance slopes to steps: [2, 4]"
01153             dyn.set_compliance_slopes(id, cw=2, ccw=4)
01154             assert dyn.read_compliance_slopes(id) == [2,4]
01155             print "Compliance Slopes:\n\tCW: %s\n\tCCW: %s" %tuple(dyn.read_compliance_slopes(id))
01156             dyn.set_compliance_slopes(id, css[0], css[1])
01157         print "Punch: %s" %dyn.read_punch(id)
01158         print
01159 
01160         ##Goal position/velocity
01161         gp = dyn.read_goal_position(id)
01162         print "Goal Angle: %s" %gp
01163         gav = dyn.read_goal_angvel(id)
01164         print "Goal Angular Velocity: %s" %gav
01165         ang = dyn.read_angle(id)
01166         print "Present Position: %s" %ang
01167         av =  dyn.read_angvel(id)
01168         print "Present Angular Velocity: %s" %av
01169         assert np.allclose(gp, ang, rtol=0.01)
01170         assert av <= gav
01171         print "Currently Moving? %s" %dyn.is_moving(id)
01172         while dyn.is_moving(id):
01173             time.sleep(0.1)
01174         assert dyn.read_angvel(id) == 0
01175         print
01176 
01177         ## Move to min, max
01178         for goal in [servo.settings['min_ang'], servo.settings['max_ang']]:
01179             print "Moving to angle: %s at %s rad/sec." %(goal, np.pi/4)
01180             dyn.move_angle(id, goal, np.pi/4)
01181             time.sleep(0.5)
01182             while dyn.is_moving(id):
01183                 poss, vels, ids = dyn.read_angs_angvels()
01184                 idx = ids.index(id)
01185                 print "Position: %s\nVelocity: %s\n" %(poss[idx], vels[idx])
01186                 time.sleep(1.5)
01187 
01188         ## Move angles sync
01189         print "Returning to 0 at %s rad/sec via syncronous call." %(np.pi/4)
01190         dyn.move_angles_sync(ids=[id], angs=[0], angvels=[np.pi/4])
01191         time.sleep(0.25)
01192         while dyn.is_moving(id):
01193             poss, vels, ids = dyn.read_angs_angvels()
01194             idx = ids.index(id)
01195             print "Position: %s\nVelocity: %s\n" %(poss[idx], vels[idx])
01196             time.sleep(1)
01197         print
01198 
01199         ## Continuous turn
01200         print "Enabling Continuous turn"
01201         als = dyn.read_angle_limits(id)
01202         dyn.enable_cont_turn(id)
01203         assert dyn.is_cont_turn_enabled(id)
01204         print "Moving clockwise at 180 deg/sec (1.57 rad/s)"
01205         dyn.set_angvel(id, -np.pi)
01206         time.sleep(0.5)
01207         av = dyn.read_angvel(id)
01208         print "Moving Velocity: %s" %av
01209         time.sleep(4)
01210         print "Moving counter-clockwise at 180 deg/sec (1.57 rad/s)"
01211         dyn.set_angvel(id, np.pi)
01212         time.sleep(0.5)
01213         av = dyn.read_angvel(id)
01214         print "Moving Velocity: %s" %av
01215         time.sleep(4)
01216         print "Disabling Continuous turn"
01217         dyn.disable_cont_turn(id)
01218         assert not dyn.is_cont_turn_enabled(id)
01219         dyn.set_angle_limits(id, als[0], als[1])
01220         print "Resetting angle limits to: ", dyn.read_angle_limits(id)
01221         assert dyn.read_angle_limits(id) == als
01222         print
01223 
01224         ## Only do the remainder for 'MX' series
01225         if servo.series == 'RX':
01226             print "Tests completed successfully"
01227             return
01228 
01229         ##Torque Control
01230         print "Is torque contro enabled? %s" %dyn.is_torque_enabled(id)
01231         print "Enabling torque control"
01232         dyn.enable_torque_control(id)
01233         print "Is torque contro enabled? %s" %dyn.is_torque_enabled(id)
01234         assert dyn.is_torque_control_enabled(id)
01235 
01236         gt = dyn.read_goal_torque(id)
01237         print "Goal Torque: %s" %gt
01238         print "Setting goal torque to ~ +50%"
01239         dyn.set_goal_torque(id, 50)
01240         assert np.allclose(dyn.read_goal_torque(id), 50, rtol=0.01)
01241         time.sleep(1)
01242         print "Present torque: %s" %dyn.read_torque(id)
01243         print "Present current: %sA" %dyn.read_current(id)
01244         time.sleep(1)
01245 
01246         print "Setting goal torque to ~ -50% (reverse)"
01247         dyn.set_goal_torque(id, -50)
01248         assert np.allclose(dyn.read_goal_torque(id), -50, rtol=0.01)
01249         time.sleep(1)
01250         print "Present torque: %s" %dyn.read_torque(id)
01251         time.sleep(1)
01252 
01253         print "Disabling torque control"
01254         dyn.disable_torque_control(id)
01255         assert not dyn.is_torque_control_enabled(id)
01256         print "Present current: %sA" %dyn.read_current(id)
01257         print
01258 
01259         ga = dyn.read_goal_acceleration(id)
01260         print "Goal Acceleration: %s rad/s^2" %ga
01261         print "Setting goal acceleration to ~450 deg/s^2"
01262         dyn.set_goal_acceleration(id, 450)
01263         nga = dyn.read_goal_acceleration(id)
01264         assert np.allclose(nga, 450, rtol=0.01)
01265         print "Setting goal acceleration back to %s" %ga
01266         dyn.set_goal_acceleration(id, ga)
01267         assert np.allclose(dyn.read_goal_acceleration(id), ga, rtol=0.01)
01268         print "Tests completed successfully"
01269 
01270 if __name__ == '__main__':
01271     usage =  ("Interface for controlling one or more robotis servos on a single bus\n"+
01272              "\tUse as below from the commandline, or:\n"+
01273              "\t\timport lib_dynamixel as ld\n"+
01274              "\t\tdyn = ld.Dynamixel_Chain()\n"+
01275              "\t\tdyn.move_angle(ang, id)")
01276     p = optparse.OptionParser(usage=usage)
01277     p.add_option('-d', action='store', type='string', dest='dev_name',
01278                  help='Required: Device string for USB2Dynamixel. [i.e. /dev/ttyUSB0 for Linux, \'0\' (for COM1) on Windows]')
01279     p.add_option('--scan', action='store_true', dest='scan', default=False,
01280                  help='Scan the device for servo IDs attached.')
01281     p.add_option('--recover', action='store_true', dest='recover', default=False,
01282                  help='Recover from a bricked servo (restores to factory defaults).')
01283     p.add_option('--ang', action='store', type='float', dest='ang',
01284                  help='Angle to move the servo to (degrees).')
01285     p.add_option('--ang_vel', action='store', type='float', dest='ang_vel',
01286                  help='angular velocity. (degrees/sec) [default = 50]', default=50)
01287     p.add_option('--id', action='store', type='int', dest='id',
01288                  help='id of servo to connect to, [default = 1]', default=1)
01289     p.add_option('--baud', action='store', type='int', dest='baud',
01290                  help='baudrate for USB2Dynamixel connection [default = 57600]', default=57600)
01291 
01292     opt, args = p.parse_args()
01293 
01294     if opt.dev_name == None:
01295         p.print_help()
01296         sys.exit(0)
01297 
01298     dyn = Dynamixel_Chain(opt.dev_name, opt.baud)
01299 
01300     if opt.scan:
01301         discover_servos( dyn )
01302 
01303     if opt.recover:
01304         recover_servo( dyn )
01305 
01306     if opt.ang != None:
01307         dyn.move_angle(opt.id, math.radians(opt.ang), math.radians(opt.ang_vel) )


robotis
Author(s): Travis Deyle, Advait Jain, Marc Killpack, Advisor: Prof. Charlie Kemp, Lab: Healthcare Robotics Lab at Georgia Tech
autogenerated on Wed Nov 27 2013 11:37:53