MoffKalast / vizanti

A mission planner and visualizer for controlling outdoor ROS robots.
https://wiki.ros.org/vizanti
BSD 3-Clause "New" or "Revised" License
163 stars 33 forks source link

Virtual obstacles in move base #98

Closed gglaspell closed 3 weeks ago

gglaspell commented 1 month ago

Time permitting, would it be possible to have the virtual fence in vizanti output in well-known text representation of geometry?

As an example of wkt: - POLYGON ((-12 30, 9 30, 9 -2, -12 -2))

The reason I ask is I often use this virtual obstacle package to create virtual fences in move_base for my UGV: https://github.com/GMahmoud/virtual_costmap_layer/tree/feature-big-refactoring

Note that the main branch works with Melodic, but the feature-big-refactoring branch works in Noetic.

The idea is to draw the virtual fence in vizanti and the virtual obstacles package would add the virtual fence to the virtual obstacle layer in move_base.

Note there is also a service call to clear the virtual obstacles which I think could be assigned to a custom button in vizanti.

Please let me know what you think. In my head, it is another way to tie vizanti's features into move_base. This is mainly for ROS Noetic. I think NAV2 has something built in to deal with no-go zones, but I haven't looked into it yet.

MoffKalast commented 1 month ago

Hmm given that it's Noetic specific and not entirely a part of the ROS convention I'm not entirely sure if it would make sense to add it natively.

I think a more practical option would be to instead have a helper node that does the conversion. One that subscribes to a Path/PoseArray topic and calls those services with the correct formatting, then you also have a lot more control over what gets called when, and you can also convert path data from other sources than just Vizanti. On the ros2 branch there is a demo node that does a similar thing for Nav2's NavigateThroughPoses.

Here's an example from gpt-4o that looks roughly correct at first glance but might needs some tweaking (to give you a general idea anyway):

#!/usr/bin/env python

import rospy
from nav_msgs.msg import Path
from virtual_costmap_layer.srv import AddElement, AddElementRequest, AddElementResponse
from virtual_costmap_layer.msg import Form
from geometry_msgs.msg import PoseStamped
import time

def path_to_wkt(path):
    """
    Converts a ROS Path message into Well-Known Text (WKT) POLYGON representation.
    """
    # Extract points from the Path message
    points = []
    for pose_stamped in path.poses:
        x = pose_stamped.pose.position.x
        y = pose_stamped.pose.position.y
        points.append((x, y))

    # Close the polygon by repeating the first point at the end
    if points[0] != points[-1]:
        points.append(points[0])

    # Create WKT POLYGON string
    wkt_polygon = "POLYGON ((" + ", ".join(f"{x} {y}" for x, y in points) + "))"
    return wkt_polygon

def path_callback(path_msg):
    """
    Callback function that is called when a new Path message is received.
    """
    rospy.loginfo("Received a new Path message. Converting to WKT...")

    # Convert the Path message to WKT format (Polygon)
    wkt_polygon = path_to_wkt(path_msg)

    # Prepare the AddElementRequest message for the service call
    add_element_req = AddElementRequest()
    form = Form()
    form.type = Form.TYPE_POLYGON  # Set the type as POLYGON
    form.data = wkt_polygon
    form.lifetime = rospy.Duration(0)  # Set lifetime to 0 (forever)

    add_element_req.form = form

    # Call the service to add the virtual costmap layer element
    try:
        rospy.loginfo("Calling the virtual_costmap_layer/add service...")
        add_element_service = rospy.ServiceProxy('virtual_costmap_layer/add', AddElement)
        response = add_element_service(add_element_req)

        if response.success:
            rospy.loginfo(f"Successfully added element to virtual costmap layer. UUID: {response.uuid}")
        else:
            rospy.logwarn(f"Failed to add element: {response.message}")
    except rospy.ServiceException as e:
        rospy.logerr(f"Service call failed: {e}")

def main():
    """
    Main function to initialize the ROS node and start the Path subscriber.
    """
    rospy.init_node('path_to_wkt_node')

    # Wait for the virtual_costmap_layer/add service to become available
    rospy.wait_for_service('virtual_costmap_layer/add')

    # Subscribe to the /fence topic that provides Path messages
    rospy.Subscriber('/fence', Path, path_callback)

    # Keep the node running
    rospy.loginfo("path_to_wkt_node is running and waiting for Path messages...")
    rospy.spin()

if __name__ == '__main__':
    try:
        main()
    except rospy.ROSInterruptException:
        pass

Note there is also a service call to clear the virtual obstacles which I think could be assigned to a custom button in vizanti.

Well it seems to be Trigger service call, and the button currently only works with Empty/Bool topic messages. It would be very useful to add that sometime though. Or just some general service manager where you can trigger any service similar to the reconfigure widget.

gglaspell commented 3 weeks ago

I am going to mark this as closed. I plan on trying this soon. I like the fact that your suggestion uses area planner to select the no go zones.