【ROS2机器人入门到实战】使用API进行导航

写在前面

  1. 当前平台文章汇总地址:ROS2机器人从入门到实战
  2. 获取完整教程及配套资料代码,请关注公众号<鱼香ROS>获取
  3. 教程配套机器人开发平台:两驱版| 四驱版
  4. 为方便交流,搭建了机器人技术问答社区:地址 fishros.org.cn

Navigation 2 对外提供了动作服务用于导航调用。动作通信是 ROS 2 四大通讯机制之一,前面我们并没有介绍,我们以导航调用为例来简单了解下它。

动作通信和其名字一样,主要用于控制场景,它的优点在于其反馈机制,当客户端发送目标给服务端后,除了等待服务端处理完成,还可以收到服务端的处理进度。启动导航后在终端中使用动作相关命令可以查看当前系统所有动作列表,命令及结果如下:

ros2 action list
--
/assisted_teleop
/backup
/compute_path_through_poses
/compute_path_to_pose
/drive_on_heading
/follow_path
/follow_waypoints
/navigate_through_poses
/navigate_to_pose
/smooth_path
/spin
/wait

其中 /navigate_to_pose 就是用于处理导航到点请求的动作。继续使用命令查看该动作的具体信息。

ros2 action info /navigate_to_pose -t
---
Action: /navigate_to_pose
Action clients: 4
    /bt_navigator [nav2_msgs/action/NavigateToPose]
    /waypoint_follower [nav2_msgs/action/NavigateToPose]
    /rviz2 [nav2_msgs/action/NavigateToPose]
    /rviz2 [nav2_msgs/action/NavigateToPose]

Action servers: 1
    /bt_navigator [nav2_msgs/action/NavigateToPose]

可以看到该动作的客户端、服务端以及消息接口情况,使用指令查看 nav2_msgs/action/NavigateToPose 接口定义,命令及结果如下:

ros2 interface show nav2_msgs/action/NavigateToPose
---
#goal definition
geometry_msgs/PoseStamped pose
	std_msgs/Header header
		builtin_interfaces/Time stamp
			int32 sec
			uint32 nanosec
		string frame_id
	Pose pose
		Point position
			float64 x
			float64 y
			float64 z
		Quaternion orientation
			float64 x 0
			float64 y 0
			float64 z 0
			float64 w 1
string behavior_tree
---
#result definition
std_msgs/Empty result
---
#feedback definition
geometry_msgs/PoseStamped current_pose
	std_msgs/Header header
		builtin_interfaces/Time stamp
			int32 sec
			uint32 nanosec
		string frame_id
	Pose pose
		Point position
			float64 x
			float64 y
			float64 z
		Quaternion orientation
			float64 x 0
			float64 y 0
			float64 z 0
			float64 w 1
builtin_interfaces/Duration navigation_time
	int32 sec
	uint32 nanosec
builtin_interfaces/Duration estimated_time_remaining
	int32 sec
	uint32 nanosec
int16 number_of_recoveries
float32 distance_remaining

从该接口定义可以看出,动作消息的接口分为目标、结果和反馈三个部分,相比服务通信接口的目标和结果多出了反馈这一部分。如果在机器人导航时仔细观察 RViz 左下角 Navigation 2 部分,你会发现它会实时的显示当前导航所花费的时间,距离目标点之间的距离,花费的时间以及脱困次数,这些数据就是来自动作服务的反馈部分。

使用命令行工具可以发送动作请求并接收反馈和结果,以请求机器人移动到地图的指定目标点为例,命令及反馈如下。

ros2 action send_goal /navigate_to_pose nav2_msgs/action/NavigateToPose "{pose: {header: {frame_id: map}, pose: {position: {x: 2, y: 2}}}}" --feedback 
---
Waiting for an action server to become available...
Sending goal:
 pose:
  header:
    stamp:
      sec: 0
      nanosec: 0
    frame_id: map
  pose:
    position:
      x: 2.0
      y: 2.0
      z: 0.0
    orientation:
      x: 0.0
      y: 0.0
      z: 0.0
      w: 1.0
behavior_tree: ''
Goal accepted with ID: c5c52646de774f55b4ee71ca5ed3267a
...
Feedback:
    current_pose:
  header:
    stamp:
      sec: 4679
      nanosec: 504000000
    frame_id: map
  pose:
    position:
      x: 2.079677095742107
      y: 1.947119186243544
      z: 0.09189999999999998
    orientation:
      x: 0.0
      y: 0.0
      z: 0.054114175706008266
      w: 0.998534754521674
navigation_time:
  sec: 18
  nanosec: 164000000
estimated_time_remaining:
  sec: 0
  nanosec: 0
number_of_recoveries: 2
distance_remaining: 0.10830961167812347
Result:
    result: {}
Goal finished with status: SUCCEEDED

上面的命令用于请求机器人移动到地图坐标系中的 xy 都为 2.0 的点,反馈结果中,Sending goal 部分表示目标,Feedback 部分是反馈,Result 部分为最终结果。

要灵活使用自然还需要学习如何用代码调用,在 Python 中 nav2_simple_commander 库已经将动作客户端代码封装到 BasicNavigator 节点中,使用该节点的对应函数就可以实现导航操作。在 src/fishbot_application/fishbot_application 下新建文件 nav_to_pose.py,然后编写如下代码:


from geometry_msgs.msg import PoseStamped
from nav2_simple_commander.robot_navigator import BasicNavigator, TaskResult
import rclpy
from rclpy.duration import Duration


def main():
    rclpy.init()
    navigator = BasicNavigator()
    # 等待导航启动完成
    navigator.waitUntilNav2Active()
    # 设置目标点坐标
    goal_pose = PoseStamped()
    goal_pose.header.frame_id = 'map'
    goal_pose.header.stamp = navigator.get_clock().now().to_msg()
    goal_pose.pose.position.x = 1.0
    goal_pose.pose.position.y = 1.0
    goal_pose.pose.orientation.w = 1.0
    # 发送目标接收反馈结果
    navigator.goToPose(goal_pose)
    while not navigator.isTaskComplete():
        feedback = navigator.getFeedback()
        navigator.get_logger().info(
            f'预计: {Duration.from_msg(feedback.estimated_time_remaining).nanoseconds / 1e9} s 后到达')
        # 超时自动取消
        if Duration.from_msg(feedback.navigation_time) > Duration(seconds=600.0):
            navigator.cancelTask()
    # 最终结果判断
    result = navigator.getResult()
    if result == TaskResult.SUCCEEDED:
        navigator.get_logger().info('导航结果:成功')
    elif result == TaskResult.CANCELED:
        navigator.get_logger().warn('导航结果:被取消')
    elif result == TaskResult.FAILED:
        navigator.get_logger().error('导航结果:失败')
    else:
        navigator.get_logger().error('导航结果:返回状态无效')

上面的代码中有关键函数有四个,第一个是 navigator.goToPose 用于发布目标,第二个是 navigator.getFeedback() 用于获取状态反馈,第三个是 navigator.cancelTask() 用于中途取消,第四个是 navigator.getResult() 用于获取最终结果。跳转到 goToPose 的源码可以发现最终发送请求是通过 self.nav_to_pose_client.send_goal_async 函数完成的,而 nav_to_pose_client 就是在 BasicNavigator 函数中定义的动作客户端,定义代码如下:

self.nav_to_pose_client = ActionClient(self, NavigateToPose, 'navigate_to_pose')

保存好代码,注册该节点并重新构建功能包,再次运行仿真和导航,初始化位姿后启动该节点,观察 RViz 中机器人运动情况,启动命令及终端打印如下。

ros2 run fishbot_application nav_to_pose
---
[INFO] [1685599886.677153365] [basic_navigator]: Nav2 is ready for use!
[INFO] [1685599886.707280550] [basic_navigator]: Navigating to goal: 1.0 1.0...
[INFO] [1685599886.852565845] [basic_navigator]: 预计剩余: 0.0 s
[INFO] [1685599891.432155882] [basic_navigator]: 预计剩余: 170.591972225 s
[INFO] [1685599891.532646643] [basic_navigator]: 预计剩余: 98.418445514 s
[INFO] [1685599891.635079916] [basic_navigator]: 预计剩余: 77.541805557 s
[INFO] [1685599914.104374413] [basic_navigator]: 预计剩余: 1.833780316 s
...
[INFO] [1685599918.156745102] [basic_navigator]: 导航结果:成功

使用 Python 可以通过调用 nav2_simple_commander 库方便的实现导航,但如果项目需要换成 C++ 也并不复杂,使用动作客户端也可以方便的调用,如何实现我并不打算再操作一遍,但我将提供给你一份详细的 C++ 调用导航服务的实现代码。

#include <memory>
#include "nav2_msgs/action/navigate_to_pose.hpp"  // 导入导航动作消息的头文件
#include "rclcpp/rclcpp.hpp"  // 导入ROS 2的C++客户端库
#include "rclcpp_action/rclcpp_action.hpp"  // 导入ROS 2的C++ Action客户端库

using NavigationAction = nav2_msgs::action::NavigateToPose;  // 定义导航动作类型为NavigateToPose

class NavToPoseClient : public rclcpp::Node {
 public:
  using NavigationActionClient = rclcpp_action::Client<NavigationAction>;  // 定义导航动作客户端类型
  using NavigationActionGoalHandle =
      rclcpp_action::ClientGoalHandle<NavigationAction>;  // 定义导航动作目标句柄类型

  NavToPoseClient() : Node("nav_to_pose_client") {
    // 创建导航动作客户端
    action_client_ = rclcpp_action::create_client<NavigationAction>(
        this, "navigate_to_pose");
  }

  void sendGoal() {
    // 等待导航动作服务器上线,等待时间为5秒
    while (!action_client_->wait_for_action_server(std::chrono::seconds(5))) {
      RCLCPP_INFO(get_logger(), "等待Action服务上线。");
    }
    // 设置导航目标点
    auto goal_msg = NavigationAction::Goal();
    goal_msg.pose.header.frame_id = "map";  // 设置目标点的坐标系为地图坐标系
    goal_msg.pose.pose.position.x = 2.0f;  // 设置目标点的x坐标为2.0
    goal_msg.pose.pose.position.y = 2.0f;  // 设置目标点的y坐标为2.0

    auto send_goal_options =
        rclcpp_action::Client<NavigationAction>::SendGoalOptions();
    // 设置请求目标结果回调函数
    send_goal_options.goal_response_callback =
        [this](NavigationActionGoalHandle::SharedPtr goal_handle) {
          if (goal_handle) {
            RCLCPP_INFO(get_logger(), "目标点已被服务器接收");
          }
        };
    // 设置移动过程反馈回调函数
    send_goal_options.feedback_callback =
        [this](
            NavigationActionGoalHandle::SharedPtr goal_handle,
            const std::shared_ptr<const NavigationAction::Feedback> feedback) {
          (void)goal_handle;  // 假装调用,避免 warning: unused
          RCLCPP_INFO(this->get_logger(), "反馈剩余距离:%f",
                      feedback->distance_remaining);
        };
    // 设置执行结果回调函数
    send_goal_options.result_callback =
        [this](const NavigationActionGoalHandle::WrappedResult& result) {
          if (result.code == rclcpp_action::ResultCode::SUCCEEDED) {
            RCLCPP_INFO(this->get_logger(), "处理成功!");
          }
        };
    // 发送导航目标点
    action_client_->async_send_goal(goal_msg, send_goal_options);
  }

  NavigationActionClient::SharedPtr action_client_;
};

int main(int argc, char** argv) {
  rclcpp::init(argc, argv);
  auto node = std::make_shared<NavToPoseClient>();
  node->sendGoal();
  rclcpp::spin(node);
  rclcpp::shutdown();
  return 0;
}
  • 5
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值