path_planner.py
Go to the documentation of this file.
00001 #!/usr/bin/env python
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         # Setup ROS node
00054         rospy.init_node('path_planner')
00055 
00056         # ROS params
00057         self.cut_spacing = rospy.get_param("~coverage_spacing", 0.5)
00058 
00059         # Setup publishers and subscribers
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         # Setup initial variables
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         # Spin until shutdown or we are ready for path following
00084         rate = rospy.Rate(10.0)
00085         while not rospy.is_shutdown():
00086             rate.sleep()
00087             if self.start_path_following:
00088                 # Run until stopped
00089                 heading = 0
00090                 # Setup path following
00091                 self.setup_path_following(heading)    
00092                 # Iterate on path following
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         # Convert the PolygonStamped into a shapely polygon
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         # Get the rotation to align with the longest edge of the polygon
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         # Rotate the field polygon
00133         from automow_planning.maptools import rotate_polygon_to
00134         transformed_field_polygon = rotate_polygon_to(field_polygon, rotation)
00135         # Decompose the rotated field into a series of waypoints
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         # Rotate the transformed path back into the source frame
00146         from automow_planning.maptools import rotate_from
00147         self.path = rotate_from(np.array(transformed_path), rotation)
00148         # Calculate headings and extend the waypoints with them
00149         self.path = self.calculate_headings(self.path)
00150         # Set the path_status to 'not_visited'
00151         self.path_status = []
00152         for waypoint in self.path:
00153             self.path_status.append('not_visited')
00154         # Visualize the data
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             # If the end, copy the previous heading
00165             if index == 0:
00166                 new_path[index].append(0)
00167                 continue
00168             # Calculate the angle between this waypoint and the next
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         # TODO: finish this (path as path viz)
00181         # self.visualize_path_as_path(path, path_status)
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             # Only put 'not_visited', 'visiting', and the most recent 'visited'
00197             # in the path msg
00198             if path_status != None: # If not set, ignore
00199                 if path_status[index] == 'visited': # if this one is visited
00200                     try:
00201                         # if the next one is visited too
00202                         if path_status[index+1] == 'visited':
00203                             # Then continue, because this one doesn't belong
00204                             # in the path msg
00205                             continue
00206                     except KeyError as e: # incase index+1 is too big
00207                         pass
00208             # Otherwise if belongs, put it in
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         # Get the time
00226         now = rospy.Time.now()
00227         # If self.path_markers is None, initialize it
00228         if self.path_markers == None:
00229             self.path_markers = MarkerArray()
00230         # # If there are existing markers, delete them
00231         # markers_to_delete = MarkerArray()
00232         # if len(self.path_markers.markers) > 0:
00233         #     for marker in self.path_markers.markers:
00234         #         marker.action = Marker.DELETE
00235         #         markers_to_delete.markers.append(marker)
00236         #     self.path_marker_pub.publish(markers_to_delete)
00237         # Clear the path_markers
00238         self.path_markers = MarkerArray()
00239         line_strip_points = []
00240         # Create the waypoint markers
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             # Store the point for the line_strip marker
00257             line_strip_points.append(point)
00258             # Set the heading of the ARROW
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             # Color is based on path_status
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             # Put this waypoint Marker in the MarkerArray
00276             self.path_markers.markers.append(waypoint_marker)
00277         # Create the line_strip Marker which connects the waypoints
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         # Publish the marker array
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         # Create the actionlib service
00300         self.move_base_client = SimpleActionClient('move_base', MoveBaseAction)
00301         connected_to_move_base = False
00302         dur = rospy.Duration(1.0)
00303         # If testing prime the robot_pose
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         # Wait for the field shape
00309         while self.field_shape == None:
00310             # Check to make sure ROS is ok still
00311             if rospy.is_shutdown(): return
00312             # Print message about the waiting
00313             msg = "Qualification: waiting on the field shape."
00314             rospy.loginfo(msg)
00315             rospy.Rate(1.0).sleep()
00316         # Wait for the robot position
00317         while self.robot_pose == None:
00318             # Check to make sure ROS is ok still
00319             if rospy.is_shutdown(): return
00320             # Print message about the waiting
00321             msg = "Qualification: waiting on initial robot pose."
00322             rospy.loginfo(msg)
00323             rospy.Rate(1.0).sleep()
00324         # Now we should plan a path using the robot's initial pose
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         # Now wait for move_base
00330         while not connected_to_move_base:
00331             # Wait for the server for a while
00332             connected_to_move_base = self.move_base_client.wait_for_server(dur)
00333             # Check to make sure ROS is ok still
00334             if rospy.is_shutdown(): return
00335             # Update the user on the status of this process
00336             msg = "Path Planner: waiting on move_base."
00337             rospy.loginfo(msg)
00338         # Now we are ready to start feeding move_base waypoints
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         # If I get here then there are no not_visited and we are done.
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         # Visualize the data
00372         self.visualize_path(self.path, self.path_status)
00373         # Get the next (or current) waypoint
00374         current_waypoint_index = self.get_next_waypoint_index()
00375         # If the index is None, then we are done path planning
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         # Get the waypoint and status
00382         current_waypoint = self.path[current_waypoint_index]
00383         current_waypoint_status = self.path_status[current_waypoint_index]
00384         # If the status is visited
00385         if current_waypoint_status == 'visited':
00386             # This shouldn't happen...
00387             return True
00388         # If the status is not_visited then we need to push the goal
00389         if current_waypoint_status == 'not_visited':
00390             # Cancel any current goals
00391             #self.move_base_client.cancel_all_goals()
00392             # Set it to visiting
00393             self.path_status[current_waypoint_index] = 'visiting'
00394             # Push the goal to the actionlib server
00395             destination = MoveBaseGoal()
00396             destination.target_pose.header.frame_id = self.field_frame_id
00397             destination.target_pose.header.stamp = rospy.Time.now()
00398             # Set the target location
00399             destination.target_pose.pose.position.x = current_waypoint[0]
00400             destination.target_pose.pose.position.y = current_waypoint[1]
00401             # Calculate the distance
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             # Set the heading
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             # Send the desired destination to the actionlib server
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         # If the status is visiting, then we just need to monitor the status
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                         # Cancel the goals
00431                         self.move_base_client.cancel_all_goals()
00432                         # Clear the cost maps
00433                         self.clear_costmaps()
00434                         # Wait 1 second
00435                         rospy.Rate(1.0).sleep()
00436                         # Then reset the current goal
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()


heatmap
Author(s): Adrian Bauer
autogenerated on Thu Feb 11 2016 23:05:26