00001
00002
00003 """
00004 /* Original work Team '"Moe" The Autonomous Lawnmower' - Auburn University
00005 * Modified work Copyright 2015 Institute of Digital Communication Systems - Ruhr-University Bochum
00006 * Modified by: Adrian Bauer
00007 *
00008 * This program is free software; you can redistribute it and/or modify it under the terms of
00009 * the GNU General Public License as published by the Free Software Foundation;
00010 * either version 3 of the License, or (at your option) any later version.
00011 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
00012 * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
00013 * See the GNU General Public License for more details.
00014 * You should have received a copy of the GNU General Public License along with this program;
00015 * if not, see <http://www.gnu.org/licenses/>.
00016 *
00017 * This file is a modified version of the original file taken from the au_automow_common ROS stack
00018 **/
00019
00020 This ROS node is responsible for planning a path.
00021
00022 This node takes the field shape as a geometry_msgs/PolygonStamped
00023 and publishes the path as a set of visualization markers.
00024
00025 Once the path has been generated the node can, by configuration or
00026 a service call, start feeding path waypoints as actionlib goals to move base.
00027 """
00028
00029 import roslib;
00030 import rospy
00031 import tf
00032 from tf.transformations import quaternion_from_euler as qfe
00033 from actionlib import SimpleActionClient
00034
00035 import numpy as np
00036 from math import radians
00037
00038 from geometry_msgs.msg import PolygonStamped, Point, PoseStamped
00039 from std_msgs.msg import ColorRGBA
00040 from visualization_msgs.msg import Marker, MarkerArray
00041 from nav_msgs.msg import Path, Odometry
00042 from move_base_msgs.msg import MoveBaseAction, MoveBaseGoal
00043 from std_srvs.srv import Empty
00044
00045 import shapely.geometry as geo
00046
00047 class PathPlannerNode(object):
00048 """
00049 This is a ROS node that is responsible for planning and executing
00050 the a path through the field.
00051 """
00052 def __init__(self):
00053
00054 rospy.init_node('path_planner')
00055
00056
00057 self.cut_spacing = rospy.get_param("~coverage_spacing", 0.5)
00058
00059
00060 rospy.Subscriber('heatmap_area', PolygonStamped, self.field_callback)
00061 self.path_marker_pub = rospy.Publisher('visualization_marker',
00062 MarkerArray,
00063 latch=True)
00064 rospy.Subscriber('odom', Odometry, self.odom_callback)
00065
00066
00067 self.field_shape = None
00068 self.field_frame_id = None
00069 self.path = None
00070 self.path_status = None
00071 self.path_markers = None
00072 self.start_path_following = False
00073 self.robot_pose = None
00074 self.goal_state = None
00075 self.current_destination = None
00076 self.testing = False
00077 self.current_distance = None
00078 self.previous_destination = None
00079 self.clear_costmaps = rospy.ServiceProxy('move_base/clear_costmaps', Empty)
00080 self.just_reset = False
00081 self.timeout = False
00082
00083
00084 rate = rospy.Rate(10.0)
00085 while not rospy.is_shutdown():
00086 rate.sleep()
00087 if self.start_path_following:
00088
00089 heading = 0
00090
00091 self.setup_path_following(heading)
00092
00093 while not rospy.is_shutdown():
00094 if not self.step_path_following():
00095 break
00096 self.start_path_following = False
00097
00098
00099
00100 def field_callback(self, msg):
00101 """
00102 Handles new field polygons, has to be called
00103 at least once before planning happens.
00104 """
00105
00106 temp_points = []
00107 for point in msg.polygon.points:
00108 temp_points.append( (float(point.x), float(point.y)) )
00109 self.field_shape = geo.Polygon(temp_points)
00110 self.field_frame_id = msg.header.frame_id
00111 self.start_path_following = True
00112
00113 def odom_callback(self, msg):
00114 """
00115 Watches for the robot's Odometry data, which is used in the path
00116 planning as the initial robot position.
00117 """
00118 self.robot_pose = msg
00119
00120 def plan_path(self, field_polygon, origin=None, degrees=0):
00121 """
00122 This is called after a field polygon has been received.
00123
00124 This uses the automow_planning.coverage module to plan a
00125 coverage path using the field geometry. The path consists of
00126 a series of waypoints.
00127 """
00128
00129 from automow_planning.maptools import rotation_tf_from_longest_edge, RotationTransform
00130 rotation = rotation_tf_from_longest_edge(field_polygon)
00131 rotation = RotationTransform(rotation.w + degrees)
00132
00133 from automow_planning.maptools import rotate_polygon_to
00134 transformed_field_polygon = rotate_polygon_to(field_polygon, rotation)
00135
00136 from automow_planning.coverage import decompose
00137 print origin
00138 if origin is not None:
00139 point_mat = np.mat([[origin[0], origin[1], 0]], dtype='float64').transpose()
00140 origin = rotation.irm * point_mat
00141 origin = (origin[0,0], origin[1,0])
00142 transformed_path = decompose(transformed_field_polygon,
00143 origin=(origin[0], origin[1]),
00144 width=self.cut_spacing)
00145
00146 from automow_planning.maptools import rotate_from
00147 self.path = rotate_from(np.array(transformed_path), rotation)
00148
00149 self.path = self.calculate_headings(self.path)
00150
00151 self.path_status = []
00152 for waypoint in self.path:
00153 self.path_status.append('not_visited')
00154
00155 self.visualize_path(self.path, self.path_status)
00156
00157 def calculate_headings(self, path):
00158 """
00159 Calculates the headings between paths and adds them to the waypoints.
00160 """
00161 new_path = []
00162 for index, waypoint in enumerate(path):
00163 new_path.append(list(path[index]))
00164
00165 if index == 0:
00166 new_path[index].append(0)
00167 continue
00168
00169 dx = path[index][0] - path[index-1][0]
00170 dy = path[index][1] - path[index-1][1]
00171 from math import atan2, pi
00172 heading = atan2(dy, dx)
00173 new_path[index].append(heading)
00174 return new_path
00175
00176 def visualize_path(self, path, path_status=None):
00177 """
00178 Convenience function, calls both visualize_path{as_path, as_marker}
00179 """
00180
00181
00182 self.visualize_path_as_marker(path, path_status)
00183
00184 def visualize_path_as_path(self, path, path_status=None):
00185 """
00186 Publishes a nav_msgs/Path msg containing the planned path.
00187
00188 If path_status is not None then the only waypoints put in the
00189 nav_msgs/Path msg will be ones that are 'not_visited' or 'visiting'.
00190 """
00191 now = rospy.Time.now()
00192 msg = Path()
00193 msg.header.stamp = now
00194 msg.header.frame_id = self.field_frame_id
00195 for index, waypoint in enumerate(path):
00196
00197
00198 if path_status != None:
00199 if path_status[index] == 'visited':
00200 try:
00201
00202 if path_status[index+1] == 'visited':
00203
00204
00205 continue
00206 except KeyError as e:
00207 pass
00208
00209 pose_msg = PoseStamped()
00210 pose_msg.header.stamp = now
00211 pose_msg.header.frame_id = self.field_frame_id
00212 pose_msg.pose.position.x = waypoint[0]
00213 pose_msg.pose.position.y = waypoint[1]
00214 msg.poses.append(pose_msg)
00215
00216 def visualize_path_as_marker(self, path, path_status):
00217 """
00218 Publishes visualization Markers to represent the planned path.
00219
00220 Publishes the path as a series of spheres connected by lines.
00221 The color of the spheres is set by the path_status parameter,
00222 which is a list of strings of which the possible values are in
00223 ['not_visited', 'visiting', 'visited'].
00224 """
00225
00226 now = rospy.Time.now()
00227
00228 if self.path_markers == None:
00229 self.path_markers = MarkerArray()
00230
00231
00232
00233
00234
00235
00236
00237
00238 self.path_markers = MarkerArray()
00239 line_strip_points = []
00240
00241 for index, waypoint in enumerate(path):
00242 waypoint_marker = Marker()
00243 waypoint_marker.header.stamp = now
00244 waypoint_marker.header.frame_id = self.field_frame_id
00245 waypoint_marker.ns = "waypoints"
00246 waypoint_marker.id = index
00247 waypoint_marker.type = Marker.ARROW
00248 if index == 0:
00249 waypoint_marker.type = Marker.CUBE
00250 waypoint_marker.action = Marker.MODIFY
00251 waypoint_marker.scale.x = 1
00252 waypoint_marker.scale.y = 1
00253 waypoint_marker.scale.z = 0.25
00254 point = Point(waypoint[0], waypoint[1], 0)
00255 waypoint_marker.pose.position = point
00256
00257 line_strip_points.append(point)
00258
00259 quat = qfe(0, 0, waypoint[2])
00260 waypoint_marker.pose.orientation.x = quat[0]
00261 waypoint_marker.pose.orientation.y = quat[1]
00262 waypoint_marker.pose.orientation.z = quat[2]
00263 waypoint_marker.pose.orientation.w = quat[3]
00264
00265 status = path_status[index]
00266 if status == 'not_visited':
00267 waypoint_marker.color = ColorRGBA(1,0,0,0.5)
00268 elif status == 'visiting':
00269 waypoint_marker.color = ColorRGBA(0,1,0,0.5)
00270 elif status == 'visited':
00271 waypoint_marker.color = ColorRGBA(0,0,1,0.5)
00272 else:
00273 rospy.err("Invalid path status.")
00274 waypoint_marker.color = ColorRGBA(1,1,1,0.5)
00275
00276 self.path_markers.markers.append(waypoint_marker)
00277
00278 line_strip = Marker()
00279 line_strip.header.stamp = now
00280 line_strip.header.frame_id = self.field_frame_id
00281 line_strip.ns = "lines"
00282 line_strip.id = 0
00283 line_strip.type = Marker.LINE_STRIP
00284 line_strip.action = Marker.ADD
00285 line_strip.scale.x = 0.1
00286 line_strip.color = ColorRGBA(0,0,1,0.5)
00287 line_strip.points = line_strip_points
00288 self.path_markers.markers.append(line_strip)
00289
00290 self.path_marker_pub.publish(self.path_markers)
00291
00292 def setup_path_following(self, degrees=0):
00293 """
00294 Sets up the node for following the planned path.
00295
00296 Will wait until the initial robot pose is set and until
00297 the move_base actionlib service is available.
00298 """
00299
00300 self.move_base_client = SimpleActionClient('move_base', MoveBaseAction)
00301 connected_to_move_base = False
00302 dur = rospy.Duration(1.0)
00303
00304 if self.testing:
00305 self.robot_pose = Odometry()
00306 self.robot_pose.pose.pose.position.x = 0
00307 self.robot_pose.pose.pose.position.y = 0
00308
00309 while self.field_shape == None:
00310
00311 if rospy.is_shutdown(): return
00312
00313 msg = "Qualification: waiting on the field shape."
00314 rospy.loginfo(msg)
00315 rospy.Rate(1.0).sleep()
00316
00317 while self.robot_pose == None:
00318
00319 if rospy.is_shutdown(): return
00320
00321 msg = "Qualification: waiting on initial robot pose."
00322 rospy.loginfo(msg)
00323 rospy.Rate(1.0).sleep()
00324
00325 origin = (self.robot_pose.pose.pose.position.x,
00326 self.robot_pose.pose.pose.position.y)
00327 self.plan_path(self.field_shape, origin, degrees)
00328 rospy.loginfo("Path Planner: path planning complete.")
00329
00330 while not connected_to_move_base:
00331
00332 connected_to_move_base = self.move_base_client.wait_for_server(dur)
00333
00334 if rospy.is_shutdown(): return
00335
00336 msg = "Path Planner: waiting on move_base."
00337 rospy.loginfo(msg)
00338
00339 return
00340
00341 def get_next_waypoint_index(self):
00342 """
00343 Figures out what the index of the next waypoint is.
00344
00345 Iterates through the path_status's and finds the visiting one,
00346 or the next not_visited one if not visiting on exists.
00347 """
00348 for index, status in enumerate(self.path_status):
00349 if status == 'visited':
00350 continue
00351 if status == 'visiting':
00352 return index
00353 if status == 'not_visited':
00354 return index
00355
00356 return None
00357
00358 def distance(self, p1, p2):
00359 """Returns the distance between two points."""
00360 from math import sqrt
00361 dx = p2.target_pose.pose.position.x - p1.target_pose.pose.position.x
00362 dy = p2.target_pose.pose.position.y - p1.target_pose.pose.position.y
00363 return sqrt(dx**2 + dy**2)
00364
00365 def step_path_following(self):
00366 """
00367 Steps the path following system, checking if new waypoints
00368 should be sent, if a timeout has occurred, or if path following
00369 needs to be paused.
00370 """
00371
00372 self.visualize_path(self.path, self.path_status)
00373
00374 current_waypoint_index = self.get_next_waypoint_index()
00375
00376 if current_waypoint_index == None:
00377 rospy.loginfo("Path Planner: Done.")
00378 return False
00379 if current_waypoint_index == 0:
00380 self.path_status[current_waypoint_index] = 'visited'
00381
00382 current_waypoint = self.path[current_waypoint_index]
00383 current_waypoint_status = self.path_status[current_waypoint_index]
00384
00385 if current_waypoint_status == 'visited':
00386
00387 return True
00388
00389 if current_waypoint_status == 'not_visited':
00390
00391
00392
00393 self.path_status[current_waypoint_index] = 'visiting'
00394
00395 destination = MoveBaseGoal()
00396 destination.target_pose.header.frame_id = self.field_frame_id
00397 destination.target_pose.header.stamp = rospy.Time.now()
00398
00399 destination.target_pose.pose.position.x = current_waypoint[0]
00400 destination.target_pose.pose.position.y = current_waypoint[1]
00401
00402 if self.previous_destination == None:
00403 self.current_distance = 5.0
00404 else:
00405 self.current_distance = self.distance(self.previous_destination, destination)
00406
00407 quat = qfe(0, 0, current_waypoint[2])
00408 destination.target_pose.pose.orientation.x = quat[0]
00409 destination.target_pose.pose.orientation.y = quat[1]
00410 destination.target_pose.pose.orientation.z = quat[2]
00411 destination.target_pose.pose.orientation.w = quat[3]
00412
00413 rospy.loginfo("Sending waypoint (%f, %f)@%f" % tuple(current_waypoint))
00414 self.current_destination = destination
00415 self.move_base_client.send_goal(destination)
00416 self.previous_destination = destination
00417
00418 temp_state = self.move_base_client.get_goal_status_text()
00419 if current_waypoint_status == 'visiting':
00420 if temp_state in ['ABORTED', 'SUCCEEDED']:
00421 self.path_status[current_waypoint_index] = 'visited'
00422 else:
00423 duration = rospy.Duration(2.0)
00424 from math import floor
00425 count = 0
00426 self.move_base_client.wait_for_result()
00427 if self.timeout == True:
00428 if count == 6+int(self.current_distance*4):
00429 rospy.logwarn("Path Planner: move_base goal timeout occurred, clearing costmaps")
00430
00431 self.move_base_client.cancel_all_goals()
00432
00433 self.clear_costmaps()
00434
00435 rospy.Rate(1.0).sleep()
00436
00437 if not self.just_reset:
00438 self.just_reset = True
00439 self.path_status[current_waypoint_index] = 'not_visited'
00440 else:
00441 self.just_reset = False
00442 self.path_status[current_waypoint_index] = 'visited'
00443 return True
00444 self.path_status[current_waypoint_index] = 'visited'
00445 return True
00446
00447 if __name__ == '__main__':
00448 ppn = PathPlannerNode()