calibrate.py
Go to the documentation of this file.
00001 #! /usr/bin/python
00002 #***********************************************************
00003 # Software License Agreement (BSD License)
00004 #
00005 # Copyright (c) 2008, Willow Garage, Inc.
00006 # All rights reserved.
00007 #
00008 # Redistribution and use in source and binary forms, with or without
00009 # modification, are permitted provided that the following conditions
00010 # are met:
00011 #
00012 #  * Redistributions of source code must retain the above copyright
00013 #    notice, this list of conditions and the following disclaimer.
00014 #  * Redistributions in binary form must reproduce the above
00015 #    copyright notice, this list of conditions and the following
00016 #    disclaimer in the documentation and/or other materials provided
00017 #    with the distribution.
00018 #  * Neither the name of the Willow Garage nor the names of its
00019 #    contributors may be used to endorse or promote products derived
00020 #    from this software without specific prior written permission.
00021 #
00022 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
00023 # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
00024 # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
00025 # FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
00026 # COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
00027 # INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
00028 # BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
00029 # LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
00030 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
00031 # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
00032 # ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
00033 # POSSIBILITY OF SUCH DAMAGE.
00034 #
00035 
00036 # Author: Wim Meeussen
00037 
00038 from __future__ import with_statement
00039 
00040 import roslib; roslib.load_manifest('turtlebot_calibration')
00041 import yaml
00042 import PyKDL
00043 import rospy
00044 from sensor_msgs.msg import Imu
00045 from nav_msgs.msg import Odometry
00046 from geometry_msgs.msg import Twist
00047 from turtlebot_calibration.msg import ScanAngle
00048 from math import *
00049 import threading
00050 import dynamic_reconfigure.client
00051 import os
00052 import subprocess
00053 import yaml
00054 
00055 def quat_to_angle(quat):
00056     rot = PyKDL.Rotation.Quaternion(quat.x, quat.y, quat.z, quat.w)
00057     return rot.GetRPY()[2]
00058         
00059 def normalize_angle(angle):
00060     res = angle
00061     while res > pi:
00062         res -= 2.0*pi
00063     while res < -pi:
00064         res += 2.0*pi
00065     return res
00066 
00067 
00068 class CalibrateRobot:
00069     def __init__(self):
00070         self.lock = threading.Lock()
00071 
00072         self.has_gyro = rospy.get_param("turtlebot_node/has_gyro")
00073         rospy.loginfo('has_gyro %s'%self.has_gyro)
00074         if self.has_gyro:
00075             self.sub_imu  = rospy.Subscriber('imu', Imu, self.imu_cb)
00076 
00077         self.sub_odom = rospy.Subscriber('odom', Odometry, self.odom_cb)
00078         self.sub_scan = rospy.Subscriber('scan_angle', ScanAngle, self.scan_cb)
00079         self.cmd_pub = rospy.Publisher('cmd_vel', Twist)
00080         self.imu_time = rospy.Time()
00081         self.odom_time = rospy.Time()
00082         self.scan_time = rospy.Time()
00083         
00084         # params
00085         self.inital_wall_angle = rospy.get_param("inital_wall_angle", 0.1)
00086         self.imu_calibrate_time = rospy.get_param("imu_calibrate_time", 10.0)
00087         self.imu_angle = 0
00088         self.imu_time = rospy.Time.now()
00089         self.scan_angle = 0
00090         self.scan_time = rospy.Time.now()
00091         self.odom_angle = 0
00092         self.odom_time = rospy.Time.now()
00093 
00094     def calibrate(self, speed, imu_drift=0):
00095         # rotate 360 degrees
00096         (imu_start_angle, odom_start_angle, scan_start_angle, 
00097          imu_start_time, odom_start_time, scan_start_time) = self.sync_timestamps()
00098         last_angle = odom_start_angle
00099         turn_angle = 0
00100         while turn_angle < 2*pi:
00101             if rospy.is_shutdown():
00102                 return
00103             cmd = Twist()
00104             cmd.angular.z = speed
00105             self.cmd_pub.publish(cmd)
00106             rospy.sleep(0.1)
00107             with self.lock:
00108                 delta_angle = normalize_angle(self.odom_angle - last_angle)
00109             turn_angle += delta_angle
00110             last_angle = self.odom_angle
00111         self.cmd_pub.publish(Twist())
00112 
00113         (imu_end_angle, odom_end_angle, scan_end_angle,
00114          imu_end_time, odom_end_time, scan_end_time) = self.sync_timestamps()
00115 
00116         scan_delta = 2*pi + normalize_angle(scan_end_angle - scan_start_angle)
00117 
00118         odom_delta = 2*pi + normalize_angle(odom_end_angle - odom_start_angle)
00119         rospy.loginfo('Odom error: %f percent'%(100.0*((odom_delta/scan_delta)-1.0)))
00120         
00121         if self.has_gyro:
00122             imu_delta = 2*pi + normalize_angle(imu_end_angle - imu_start_angle) - imu_drift*(imu_end_time - imu_start_time).to_sec()
00123             rospy.loginfo('Imu error: %f percent'%(100.0*((imu_delta/scan_delta)-1.0)))
00124             imu_result = imu_delta/scan_delta
00125         else: 
00126             imu_result = None
00127 
00128         return (imu_result, odom_delta/scan_delta)
00129 
00130 
00131     def imu_drift(self):
00132         if not self.has_gyro:
00133             return 0
00134         # estimate imu drift
00135         rospy.loginfo('Estimating imu drift')
00136         (imu_start_angle, odom_start_angle, scan_start_angle, 
00137          imu_start_time, odom_start_time, scan_start_time) = self.sync_timestamps()
00138         rospy.sleep(self.imu_calibrate_time)
00139         (imu_end_angle, odom_end_angle, scan_end_angle,
00140          imu_end_time, odom_end_time, scan_end_time) = self.sync_timestamps()
00141 
00142         imu_drift = normalize_angle(imu_end_angle - imu_start_angle) / ((imu_end_time - imu_start_time).to_sec())
00143         rospy.loginfo(' ... imu drift is %f degrees per second'%(imu_drift*180.0/pi))
00144         return imu_drift
00145 
00146 
00147     def align(self):
00148         self.sync_timestamps()
00149         rospy.loginfo("Aligning base with wall")
00150         with self.lock:
00151             angle = self.scan_angle
00152         cmd = Twist()
00153 
00154         while angle < -self.inital_wall_angle or angle > self.inital_wall_angle:
00155             if rospy.is_shutdown():
00156                 exit(0)
00157             if angle > 0:
00158                 cmd.angular.z = -0.3
00159             else:
00160                 cmd.angular.z = 0.3
00161             self.cmd_pub.publish(cmd)
00162             rospy.sleep(0.05)
00163             with self.lock:
00164                 angle = self.scan_angle
00165 
00166 
00167 
00168     def sync_timestamps(self, start_time=None):
00169         if not start_time:
00170             start_time = rospy.Time.now() + rospy.Duration(0.5)
00171         while not rospy.is_shutdown():
00172             rospy.sleep(0.3)
00173             with self.lock:
00174                 if self.imu_time < start_time and self.has_gyro:
00175                     rospy.loginfo("Still waiting for imu")
00176                 elif self.odom_time < start_time:
00177                     rospy.loginfo("Still waiting for odom")
00178                 elif self.scan_time < start_time:
00179                     rospy.loginfo("Still waiting for scan")
00180                 else:
00181                     return (self.imu_angle, self.odom_angle, self.scan_angle,
00182                             self.imu_time, self.odom_time, self.scan_time)
00183         exit(0)
00184         
00185 
00186     def imu_cb(self, msg):
00187         with self.lock:
00188             angle = quat_to_angle(msg.orientation)
00189             self.imu_angle = angle
00190             self.imu_time = msg.header.stamp
00191 
00192     def odom_cb(self, msg):
00193         with self.lock:
00194             angle = quat_to_angle(msg.pose.pose.orientation)
00195             self.odom_angle = angle
00196             self.odom_time = msg.header.stamp
00197 
00198     def scan_cb(self, msg):
00199         with self.lock:
00200             angle = msg.scan_angle
00201             self.scan_angle = angle
00202             self.scan_time = msg.header.stamp
00203 
00204 def get_usb_to_serial_id():
00205     usbpath = subprocess.check_output("readlink -f /sys/class/tty/ttyUSB0", shell=True)
00206     usbpath = usbpath.strip()
00207     if len(usbpath) == 0:
00208         return None
00209     serialid = ""
00210     try:
00211         f = open(usbpath + "/../../../../serial", "r")
00212         serialid = f.read().strip()
00213         f.close()
00214     except:
00215         pass
00216     try:
00217         f = open(usbpath + "/../../../../idVendor", "r")
00218         serialid += f.read().strip()
00219         f.close()
00220         f = open(usbpath + "/../../../../idProduct", "r")
00221         serialid += f.read().strip()
00222         f.close()
00223     except:
00224         pass
00225     if len(serialid.strip()) == 0:
00226         return None
00227     return serialid
00228 
00229 def get_kinect_serial():
00230     ret = subprocess.check_output("lsusb -v -d 045e:02ae | grep Serial | awk '{print $3}'", shell=True)
00231     if len(ret) > 0:
00232         return ret.strip()
00233     return None
00234    
00235 def getCurrentParams(drclient):
00236     allparams = drclient.get_configuration()
00237     return (allparams['gyro_scale_correction'], allparams['odom_angular_scale_correction'], allparams['gyro_measurement_range'])
00238 
00239 def writeParams(drclient, newparams):
00240     r = drclient.update_configuration(newparams) 
00241     rospy.loginfo("Automatically updated the params in the current running instance of ROS, no need to restart.")
00242 
00243 def writeParamsToCalibrationFile(newparams):
00244     kinect_serial = get_kinect_serial()
00245     if kinect_serial is None:
00246         kinect_serial =  get_usb_to_serial_id()  # can't find a kinect, attempt to use the usb to serial convert's id as a backup
00247         if kinect_serial is None:
00248             return
00249     ros_home = os.environ.get('ROS_HOME')
00250     if ros_home is None:
00251         ros_home = "~/.ros"
00252     calib_dir = os.path.expanduser(ros_home +"/turtlebot_create/")
00253     calib_file = calib_dir +str(kinect_serial) + ".yaml"
00254     # if the file exists, load into a dict, update the new params, and then save
00255     if os.path.isfile(calib_file):
00256         f = open(calib_file, 'r')
00257         docs = yaml.load_all(f)
00258         d = docs.next()
00259         for k,v in newparams.iteritems():
00260             d[k] = v
00261         newparams = d
00262         f.close()
00263     try:
00264         os.makedirs(calib_dir)
00265     except:
00266         pass
00267     with open(calib_file, 'w') as outfile:
00268         outfile.write( yaml.dump(newparams, default_flow_style=False) )
00269     rospy.loginfo("Saved the params to the calibration file: %s" % calib_file)
00270 
00271 def writeParamsToLaunchFile(gyro, odom, gyro_range):
00272     try:
00273         f = open("/etc/ros/distro/turtlebot.launch", "r")
00274         # this is totally NOT the best way to solve this problem.
00275         foo = []
00276         for lines in f:
00277             if "turtlebot_node/gyro_scale_correction" in lines:
00278                 foo.append("  <param name=\"turtlebot_node/gyro_scale_correction\" value=\"%f\"/>\n" % gyro)
00279             elif "turtlebot_node/odom_angular_scale_correction" in lines:
00280                 foo.append("  <param name=\"turtlebot_node/odom_angular_scale_correction\" value=\"%f\"/>\n" % odom)
00281             elif "turtlebot_node/gyro_measurement_range" in lines:
00282                 foo.append("  <param name=\"turtlebot_node/gyro_measurement_range\" value=\"%f\"/>\n" % gyro_range)
00283             else:
00284                 foo.append(lines)
00285         f.close()
00286 
00287         # and... write!
00288         f = open("/etc/ros/distro/turtlebot.launch", "w")
00289         for i in foo:
00290             f.write(i)
00291         f.close()
00292         rospy.loginfo("Automatically updated turtlebot.launch, please restart the turtlebot service.")
00293     except:
00294         rospy.loginfo("Could not automatically update turtlebot.launch, please manually update it.")
00295 
00296 def warnAboutGyroRange(drclient):
00297     params = getCurrentParams(drclient)
00298     rospy.logwarn("***** If you have not manually set the gyro range parameter you must do so before running calibration.  Cancel this run and see http://wiki.ros.org/turtlebot_calibration/Tutorials/Calibrate%20Odometry%20and%20Gyro")
00299     rospy.logwarn("******* turtlebot_node/gyro_measurement_range is currently set to: %d ******" % params[2])
00300     
00301     
00302 def main():
00303     rospy.init_node('scan_to_angle')
00304     robot = CalibrateRobot()
00305     imu_res = 1.0
00306 
00307     drclient = dynamic_reconfigure.client.Client("turtlebot_node")
00308     warnAboutGyroRange(drclient)
00309 
00310     imu_drift = robot.imu_drift()
00311     imu_corr = []
00312     odom_corr = []
00313     for speed in (0.3, 0.7, 1.0, 1.5):
00314         robot.align()
00315         (imu, odom) = robot.calibrate(speed, imu_drift)
00316         if imu:
00317             imu_corr.append(imu)
00318         odom_corr.append(odom)
00319     
00320     (prev_gyro, prev_odom, gyro_range) = getCurrentParams(drclient)
00321     if len(imu_corr)>0:    
00322         imu_res = prev_gyro * (1.0/(sum(imu_corr)/len(imu_corr)))
00323         rospy.loginfo("Set the 'turtlebot_node/gyro_scale_correction' parameter to %f"%imu_res)
00324 
00325     odom_res = prev_odom * (1.0/(sum(odom_corr)/len(odom_corr)))
00326     rospy.loginfo("Set the 'turtlebot_node/odom_angular_scale_correction' parameter to %f"%odom_res)
00327     writeParamsToLaunchFile(imu_res, odom_res, gyro_range)
00328 
00329     newparams = {'gyro_scale_correction' : imu_res, 'odom_angular_scale_correction' : odom_res, 'gyro_measurement_range' : gyro_range}
00330     writeParamsToCalibrationFile(newparams)
00331     writeParams(drclient, newparams)
00332 
00333 if __name__ == '__main__':
00334     main()


turtlebot_calibration
Author(s): Wim Meeussen
autogenerated on Sat Jun 8 2019 20:35:25