Author Archives: fangfang12138

ROS2_Foxy学习5——基础编程_C++

里面的例子参考官方教程,然后附带一些解释和一些推荐的便于理解的文章。

1 workspace

1.1 工作空间及其层次

  空间的创建:ROS2与ROS1一样,创建工作空间目录,并在其中创建/src目录,用于存放package功能包。

  层次的概念:ROS2增加了overlay和underlay的概念,用于在多个workspace同时工作时,处理各个workspace之间的层次问题,对于包名相同的package,上层workspace将覆盖(override)下层workspace中的同名package。

  层次的配置:层次是通过环境变量配置的先后决定的,ROS通过setup.bash设置环境变量,ROS2安装路径一般设置为最下层的工作空间,即

$ source /opt/ros/foxy/setup.bash

  运行新建workspace的package功能包,需要在对工作空间编译后的新终端中配置如下

# 设置当前工作空间中功能包的相关环境变量
$ . install/local_setup.sh 

# 设置当前工作空间中功能包的相关环境变量,同时设置该工作空间下其他底层工作空间的环境变量
$ . install/setup.sh    # 就是说这一个捎带着把ROS2安装路径的环境变量也设置了

1.2 检查依赖

  编译前,需要对功能包检查依赖情况。

$ rosdep install -i --from-path src --rosdistro foxy -y

1.3 编译工具 colcon

# 在workspace根目录编译工程
$ colcon build  

# 有选择地编译包
$ colcon build --packages-select <package_name>

  有关编译,可看一下后续的系列第七篇——编译

2 编写 package

  每一个package都能实现一个相对完整的功能。

2.1 功能包创建

# 创建cmake功能包(在~/ws/src下)
$ ros2 pkg create --build-type ament_cmake <package_name>

# 创建cmake功能包(在~/ws/src下)
# --dependencies 参数会将后边的依赖自动添加到package.xml和CMakeLists.txt中
$ ros2 pkg create --build-type ament_cmake <package_name> --dependencies <depend_name>

# 创建cmake功能包(在~/ws/src下)
# --node-name 参数将创建节点,但只能创建一个节点,注意参数位置
$ ros2 pkg create --build-type ament_cmake --node-name <node_name> <package_name>

    # 例如(在~/ws/src下)
    $ ros2 pkg create --build-type ament_cmake --node-name my_node my_package --dependencies rclcpp
    # 将会在~/ws/src创建如下目录
    workspace_folder/
        src/ 
            my_package/ 
                 src/
                    my_node.cpp
                 include/
                    my_package/
                 CMakeLists.txt
                 package.xml

2.2 package.xml 和 CMakeLists.txt

  使用ament编译系统的功能包会对应生成两个文件package.xml和CMakeLists.txt。
  package.xml:描述了功能包的发行信息、依赖信息等,内容如下。在自动创建后,需要手动修改功能包描述、功能包版本、功能包许可证信息等发行信息。以“ _depend”结尾的标签用来描述功能包依赖,在增加新的依赖时,需要添加到其中。

<!--  功能包基础内容,不用修改   -->
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<!--  下面是功能包的信息  -->
<package format="3">
    <!--  包名,新建的时候就定了,不改  -->
    <name>cpp_srvcli</name>
    <!--  关于发行信息   -->
    <version>0.0.0</version>
    <description>这里写功能描述</description>
    <maintainer email="这里写邮箱地址">Your Name</maintainer>
    <license>这里写许可证</license>
    <!--  编译系统的依赖   -->
    <buildtool_depend>ament_cmake</buildtool_depend>
    <!--  程序所需的依赖,根据需要修改添加   -->
    <depend>rclcpp</depend>
    <!--  编译系统,新建功能包的时候可以指定   -->
    <export>
    <build_type>ament_cmake</build_type>
    </export>
</package>

  CMakeLists.txt:制定各文件或包的编译顺序的文档,如何编写CMakeLists.txt参见本系列第七篇——编译。文档基本结构如下。

cmake_minimum_required(VERSION 3.5) //cmake版本要求
project(my_project)                 //功能包名,与同一功能包下package.xml中名字一样

ament_package()                     //配置project,只调用一次,且在最后

2.3 功能包编译、设置环境变量、运行

# 记得检查依赖
$ rosdep install -i --from-path src --rosdistro foxy -y

# 编译,见上一小节
$ colcon build

# 新终端设置环境变量
$ . install/setup.bash

# 运行包里的某个节点,运行多个节点、多个包,可使用launch文件
$ ros2 run <package_name> <node_name>

3 编写 topic

新建工作空间,并新建话题功能包(这里包名是cpp_pubsub)。

3.1 发布 publisher

在功能包的/src目录下,编写话题发布节点,publisher_member_function.cpp

#include <chrono>           //c++11日期和时间库       
#include <functional>       //参见 注1
#include <memory>           //c++内存管理库,使用智能指针必须包含此头文件
#include <string>

#include "rclcpp/rclcpp.hpp"    //ROS2,C++接口的头文件
#include "std_msgs/msg/string.hpp"  //string类型的msg头文件

using namespace std::chrono_literals;

/* This example creates a subclass of Node and uses std::bind() to register a
* member function as a callback from the timer. */

//定义一个节点类的子类
class MinimalPublisher : public rclcpp::Node
{
  public:
        //构造函数:初始化节点名 minimal_publisher,然后创建了一个publisher和定时器
        MinimalPublisher(): Node("minimal_publisher"), count_(0)
        {
            //创建一个发布者,发布的话题名为topic,话题消息是String,保存消息的队列长度是10
            publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
            //创建一个定时器,定时500ms,定时触发回调函数timer_callback。
            //这里用到了<functional>的一个特性
            timer_ = this->create_wall_timer(500ms, std::bind(&MinimalPublisher::timer_callback, this));
        }

  private:
        //定时触发回调函数
        void timer_callback()
        {
            //定义消息
            auto message = std_msgs::msg::String();
            message.data = "Hello, world! " + std::to_string(count_++);
            //打印日志
            RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
            //发布
            publisher_->publish(message);
        }

        //定义定时器和发布者指针
        rclcpp::TimerBase::SharedPtr timer_;
        rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;

        size_t count_;
  };

  int main(int argc, char * argv[])
  {
        rclcpp::init(argc, argv);
        rclcpp::spin(std::make_shared<MinimalPublisher>());
        rclcpp::shutdown();
        return 0;
  }

注:
1、#include < functional > ,感兴趣的读者可以看下这篇博客,写的蛮不错的。

3.2 订阅 subscriber

在功能包的/src目录下,编写话题订阅节点,subscriber_member_function.cpp

#include <memory>

#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
public:
    MinimalSubscriber() : Node("minimal_subscriber")
    {
      subscription_ = this->create_subscription<std_msgs::msg::String>(
      "topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

private:
    void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
    {
      RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
    }
    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char * argv[])
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<MinimalSubscriber>());
  rclcpp::shutdown();
  return 0;
}

3.3 修改 package.xml / CMakeLists.txt

  package.xml 添加如下依赖,如果创建包时,使用了–dependencies,则文件内已有如下语句。

    <depend>rclcpp</depend>
    <depend>std_msgs</depend>

  CMakeLists.txt 添加如下编译规则,如果创建包时,使用了–dependencies,则文件内已有find_package相关的语句。

# 搜索依赖
    find_package(rclcpp REQUIRED)
    find_package(std_msgs REQUIRED)
# 创建可执行程序talker,后续参数是构成可执行文件的所有源文件
    add_executable(talker src/publisher_member_function.cpp)
# 链接find_package()找到的包
    ament_target_dependencies(talker rclcpp std_msgs)

    add_executable(listener src/subscriber_member_function.cpp)
    ament_target_dependencies(listener rclcpp std_msgs)

    install(TARGETS
      talker
      listener
      DESTINATION
      lib/${PROJECT_NAME})

3.4 编译、运行两个节点

# 在工作空间根目录下,检查依赖
$ rosdep install -i --from-path src --rosdistro foxy -y
# 编译
$ colcon build

# 配置环境变量,然后运行
$ . install/setup.bash
$ ros2 run cpp_pubsub talker

# 打开另一个终端,配置环境变量,运行
$ . install/setup.bash
$ ros2 run cpp_pubsub listener

4 编写 service

新建工作空间,并新建服务功能包(这里包名是cpp_srvcli)。

4.1 服务器端 server

在功能包的/src目录下,创建服务器端节点,add_two_ints_server.cpp

#include "example_interfaces/srv/add_two_ints.hpp"
#include "rclcpp/rclcpp.hpp"
#include <memory>

void add(const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
      std::shared_ptr<example_interfaces::srv::AddTwoInts::Response>      response)
{
    response->sum = request->a + request->b;
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld",
            request->a, request->b);
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);

    //创建名为add_two_ints_server的节点
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_server");

    //创建名为add_two_ints的服务,绑定回调函数add
    rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service =
    node->create_service<example_interfaces::srv::AddTwoInts>("add_two_ints", &add);

    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add two ints.");

    //入锁,等待客户端请求
    rclcpp::spin(node);
    rclcpp::shutdown();
}

4.2 客户端 client

在功能包的/src目录下,创建客户端节点,add_two_ints_client.cpp

#include "rclcpp/rclcpp.hpp"
#include "example_interfaces/srv/add_two_ints.hpp"

#include <chrono>
#include <cstdlib>
#include <memory>

using namespace std::chrono_literals;

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);

    if (argc != 3) 
    {
        RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y");
        return 1;
    }

    //创建名为add_two_ints_client的节点
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_two_ints_client");

    //创建名为add_two_ints的客户端
    rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client =
    node->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");

    //采集request数据
    auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
    request->a = atoll(argv[1]);
    request->b = atoll(argv[2]);

    //search for service
    while (!client->wait_for_service(1s)) 
    {
        if (!rclcpp::ok()) 
        {
            RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
            return 0;
        }
        RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
    }

    //发送request
    auto result = client->async_send_request(request);

    // Wait for the result.
    if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS)
    {
        RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
    } 
    else 
    {
        RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_two_ints");
    }

    rclcpp::shutdown();
    return 0;
}

4.3 修改 package.xml / CMakeLists.txt

  package.xml 添加如下依赖,如果创建包时,使用了–dependencies,则文件内已有如下语句。

    <depend>rclcpp</depend>
    <depend>example_interfaces</depend>

  CMakeLists.txt 添加如下编译规则,如果创建包时,使用了–dependencies,则文件内已有find_package相关的语句。

# 搜索依赖
    find_package(rclcpp REQUIRED)
    find_package(example_interfaces REQUIRED)
# 创建可执行程序server,后续参数是构成可执行文件的所有源文件
    add_executable(server src/add_two_ints_server.cpp)
# 链接find_package()找到的包
    ament_target_dependencies(server rclcpp example_interfaces)

    add_executable(client src/add_two_ints_client.cpp)
    ament_target_dependencies(client rclcpp example_interfaces)

    install(TARGETS
              server
              client
              DESTINATION lib/${PROJECT_NAME})

4.4 编译、运行两个节点

# 在工作空间根目录下,检查依赖
$ rosdep install -i --from-path src --rosdistro foxy -y
# 编译
$ colcon build

# 配置环境变量,然后运行
$ . install/setup.bash
$ ros2 run cpp_srvcli server

# 打开另一个终端,配置环境变量,运行
$ . install/setup.bash
$ ros2 run cpp_srvcli client 2 3

5 自定义消息 msg/srv

5.1 创建消息功能包

1、新建工作空间,并新建消息功能包(这里包名是tutorial_interfaces)。在消息功能包下,创建/msg和/srv文件夹(与/src同级),并在其中分别建立.msg和.srv文件,有关消息内容,详见本系列第三篇《ROS2_Foxy学习(三)核心概念》

Num.msg

int64 num

AddThreeInts.srv

int64 a
int64 b
int64 c
---
int64 sum

2、在package.xml文件中添加

    <build_depend>rosidl_default_generators</build_depend>
    <exec_depend>rosidl_default_runtime</exec_depend>
    <member_of_group>rosidl_interface_packages</member_of_group>

注:rosidl_default_generators为编译依赖,rosidl_default_runtime为运行依赖。
3、在CMakeLists.txt文件中添加

find_package(rosidl_default_generators REQUIRED)

# You can use set to neatly list all of your interfaces:
set(msg_files
  "msg/Num.msg"
  "msg/Message1.msg"
  "msg/Message2.msg"
  # etc
  )

set(srv_files
  "srv/AddThreeInts.srv"
  "srv/Service1.srv"
  "srv/Service2.srv"
   # etc
  )

#And generate all lists at once like so:
rosidl_generate_interfaces(${PROJECT_NAME}
  ${msg_files}
  ${srv_files}
)

ament_export_dependencies(rosidl_default_runtime)

注:可以省略set的步骤,在rosidl_generate_interfaces直接添加,如下,但不建议,还是需要一个良好的编程习惯。

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/Num.msg"
  "srv/AddThreeInts.srv"
)

5.2 测试自定义消息接口

  功能包编译后(没有节点,没有运行),可以通过命令ros2 interface show来查看消息文件。

5.3 应用自定义消息接口

  在同一个工作空间下,除消息功能包外,再创建两个功能包,分别是话题功能包和服务功能包,参考3、4部分。

1、CMakeLists.txt和package.xml与第3、4部分基本一致,其中,需要将消息依赖包(std_msgs和example_interfaces)替换成新建的消息功能包名,如tutorial_interfaces。
2、各节点src文件,需要将消息类型进行替换,src文件修改见附件1。
3、之后,编译运行(查依赖、编译包、配置环境、运行节点)。

5.4 使用同一功能包的消息接口

  若功能包需要使用自己定义的接口,需要在CMakeLists.txt中添加以下内容(In order to use the messages generated in the same package.)。

rosidl_target_interfaces(publish_address_book
  ${PROJECT_NAME} "rosidl_typesupport_cpp")

注:publish_address_book是编译生成的可执行程序名。

6 使用 parameter

新建工作空间,并新建参数功能包(这里包名是cpp_parameters),使用参数dependencies,自动添加依赖rclcpp。

6.1 参数的声明与获取

在功能包的/src目录下,创建节点,cpp_parameters_node.cpp

#include <rclcpp/rclcpp.hpp>
#include <chrono>
#include <string>
#include <functional>

using namespace std::chrono_literals;

class ParametersClass: public rclcpp::Node
{
  public:
    ParametersClass() : Node("parameter_node")
    {
      //声明:定义参数名及其值
      this->declare_parameter<std::string>("my_parameter", "world");
      //
      timer_ = this->create_wall_timer(1000ms, std::bind(&ParametersClass::respond, this));
    }

    //获取:定时打印参数值
    void respond()
    {
      this->get_parameter("my_parameter", parameter_string_);
      RCLCPP_INFO(this->get_logger(), "Hello %s", parameter_string_.c_str());
    }

  private:
    std::string parameter_string_;
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char** argv)
{
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<ParametersClass>());
  rclcpp::shutdown();
  return 0;
}

修改CMakelists.txt,然后编译运行。

add_executable(parameter_node src/cpp_parameters_node.cpp)
ament_target_dependencies(parameter_node rclcpp)

install(TARGETS
  parameter_node
  DESTINATION lib/${PROJECT_NAME}
)

6.2 终端修改参数

有关内容,详见本系列第三篇《ROS2_Foxy学习(三)核心概念》

$ ros2 param list
$ ros2 param set <node_name> <parameter_name> <value>

6.3 launch文件修改参数

在功能包的/launch目录下,创建launch文件,cpp_parameters_launch.py,修改parameters。

from launch import LaunchDescription
from launch_ros.actions import Node

def generate_launch_description():
    return LaunchDescription([
        Node(
            package="cpp_parameters",
            executable="parameter_node",
            name="custom_parameter_node",
            output="screen",
            emulate_tty=True,
            parameters=[
                {"my_parameter": "earth"}
            ]
        )
    ])

修改CMakelists.txt,在6.1部分中CMakelists.txt最后增加以下部分。

install(
  DIRECTORY launch
  DESTINATION share/${PROJECT_NAME}
)

编译,然后运行launch文件。

$ ros2 launch cpp_parameters cpp_parameters_launch.py

附件1 消息文件应用

1、话题功能包——消息发布节点——publisher_member_function.cpp

#include <chrono>
#include <functional>
#include <memory>
#include <string>

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp"

using namespace std::chrono_literals;

/* This example creates a subclass of Node and uses std::bind() to register a* member function as a callback from the timer. */

class MinimalPublisher : public rclcpp::Node
{
public:
    MinimalPublisher(): Node("minimal_publisher"), count_(0)
    {
      publisher_ = this->create_publisher<tutorial_interfaces::msg::Num>("topic", 10); //10 是消息队列长度
      timer_ = this->create_wall_timer(500ms,std::bind(&MinimalPublisher::timer_callback, this));
    }

private:
    void timer_callback()
    {
      auto message = tutorial_interfaces::msg::Num();
      message.num = this->count_++;
      RCLCPP_INFO(this->get_logger(), "Publishing: '%d'", message.num);
      publisher_->publish(message);
    }

    rclcpp::TimerBase::SharedPtr timer_;

    rclcpp::Publisher<tutorial_interfaces::msg::Num>::SharedPtr publisher_;

    size_t count_;
};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<MinimalPublisher>());
    rclcpp::shutdown();
    return 0;
}

2、话题功能包——消息订阅节点——subscriber_member_function.cpp

#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/msg/num.hpp"

using std::placeholders::_1;

class MinimalSubscriber : public rclcpp::Node
{
public:
    MinimalSubscriber(): Node("minimal_subscriber")
    {
      subscription_ = this->create_subscription<tutorial_interfaces::msg::Num>("topic", 10, std::bind(&MinimalSubscriber::topic_callback, this, _1));
    }

private:
    void topic_callback(const tutorial_interfaces::msg::Num::SharedPtr msg) const
    {
      RCLCPP_INFO(this->get_logger(), "I heard: '%d'", msg->num);
    }
    rclcpp::Subscription<tutorial_interfaces::msg::Num>::SharedPtr subscription_;};

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<MinimalSubscriber>());
    rclcpp::shutdown();
    return 0;
}

3、服务功能包——服务器端节点——add_three_ints_server.cpp

#include "tutorial_interfaces/srv/add_three_ints.hpp"
#include "rclcpp/rclcpp.hpp"
#include <memory>

void add(const std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Request> request,
      std::shared_ptr<tutorial_interfaces::srv::AddThreeInts::Response>      response)
{
    response->sum = request->a + request->b + request->c;
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Incoming request\na: %ld" " b: %ld" " c: %ld",
            request->a, request->b, request->b);
    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "sending back response: [%ld]", (long int)response->sum);
}

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);

    //创建名为add_two_ints_server的节点
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_server");

    //创建名为add_two_ints的服务,绑定回调函数add
    rclcpp::Service<tutorial_interfaces::srv::AddThreeInts>::SharedPtr service =
    node->create_service<tutorial_interfaces::srv::AddThreeInts>("add_three_ints", &add);

    RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Ready to add three ints.");

    //入锁,等待客户端请求
    rclcpp::spin(node);
    rclcpp::shutdown();
}

4、服务功能包——客户端节点——add_three_ints_client.cpp

#include "rclcpp/rclcpp.hpp"
#include "tutorial_interfaces/srv/add_three_ints.hpp"

#include <chrono>
#include <cstdlib>
#include <memory>

using namespace std::chrono_literals;

int main(int argc, char **argv)
{
    rclcpp::init(argc, argv);

    if (argc != 4) 
    {
        RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "usage: add_two_ints_client X Y Z");
        return 1;
    }

    //创建名为add_three_ints_client的节点
    std::shared_ptr<rclcpp::Node> node = rclcpp::Node::make_shared("add_three_ints_client");

    //创建名为add_three_ints的客户端
    rclcpp::Client<tutorial_interfaces::srv::AddThreeInts>::SharedPtr client =
    node->create_client<tutorial_interfaces::srv::AddThreeInts>("add_three_ints");

    //采集request数据
    auto request = std::make_shared<tutorial_interfaces::srv::AddThreeInts::Request>();
    request->a = atoll(argv[1]);
    request->b = atoll(argv[2]);
    request->c = atoll(argv[3]);

    //search for service
    while (!client->wait_for_service(1s)) 
    {
        if (!rclcpp::ok()) 
        {
            RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Interrupted while waiting for the service. Exiting.");
            return 0;
        }
        RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "service not available, waiting again...");
    }

    //发送request
    auto result = client->async_send_request(request);

    // Wait for the result.
    if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS)
    {
        RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "Sum: %ld", result.get()->sum);
    } 
    else 
    {
        RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Failed to call service add_three_ints");
    }

    rclcpp::shutdown();
    return 0;
}

ROS2_Foxy学习4——常用工具

1 rqt

1.1 安装

  不同的版本 安装方式不同。

# apt2.0 Ubuntu 20.04 and newer
    $ sudo apt update
    $ sudo apt install ~nros-<distro>-rqt*
# apt1.x Ubuntu 18.04 and older
    $ sudo apt update
    $ sudo apt install ros-<distro>-rqt*

1.2 rqt_graph

  运行rqt_graph,也可以从rqt打开,rqt_graph清晰地展示了节点、话题等图元素的关系。
在这里插入图片描述

1.3 rqt_console

  记录终端的日志,也是一个节点,收集/rosout的信息,可以根据日志等级过滤,也可以高亮一些内容。

$ ros2 run rqt_console rqt_console

在这里插入图片描述
  上中下一共三个窗口,上面窗口列出了所有的日志,中间窗口可以根据日志等级进行筛选,下面的额窗口可以高亮一些需要的字符串。
  日志等级按严重性,如下排列:

Fatal
Error
Warn
Info
Debug

2 launch file

  Launch files allow you to start up and configure a number of executables containing ROS 2 nodes simultaneously.

$ ros2 launch <launch_file_name>

  launch文件的编写在后续博文中单列一节~

3 ros2bag

  记录topic发布的数据,可选择记录哪些topic,事后可以对ros包进行回放。

# 录包
    # 包存在于 录包终端 所在的目录下
    $ ros2 bag record <topic_name>

    # 同时录多个话题,话题之间使用空格分开
    # 使用 -o 选项可以设置包名
    $ ros2 bag record -o <bag_file_name> <topic1_name> <topic2_name>
    # 使用 -a 选项是记录所有topic,在存在循环依赖的系统中 不建议使用
    $ ros2 bag record -a

# 查看包的信息
    $ ros2 bag info <bag_file_name>
    # 有这些信息
    Files:             subset.db3
    Bag size:          228.5 KiB
    Storage id:        sqlite3
    Duration:          48.47s
    Start:             Oct 11 2019 06:09:09.12 (1570799349.12)
    End                Oct 11 2019 06:09:57.60 (1570799397.60)
    Messages:          3013
    Topic information: Topic: /turtle1/cmd_vel | Type: geometry_msgs/msg/Twist | Count: 9 | Serialization Format: cdr
                     Topic: /turtle1/pose | Type: turtlesim/msg/Pose | Count: 3004 | Serialization Format: cdr

# 回放
    $ ros2 bag play <bag_file_name>

ROS2_Foxy学习3——核心概念

  A series of core ROS 2 concepts that make up what is referred to as the “ROS (2) graph”.

  这部分将介绍组成ROS2图的各个核心概念,包括节点、话题、服务、参数、动作。Graph展示了机器人系统内部各部分配合运行的过程。

1 node 节点

  一个完整的机器人系统,或者图,由许多的节点组成。每个节点都负责一个模块的功能。节点之间互相配合使得系统实现某些更为高级的功能。
  例如,一个基于ROS的轮式移动机器人,节点 /sensor 负责采集周围环境信息,节点 /controler 负责判断障碍物信息,节点 /motion 负责改变车轮的转速和转向,三者配合实现避障的功能,其中三者之间的数据交互可以通过topic、service等实现。

# 节点启动
    $ ros2 run <package_name> <executable_name>
    $ ros2 run turtlesim turtlesim_node
# 列出所有节点
    $ ros2 node list
# 节点信息:查看包括订阅和发布的话题、服务、动作等
    $ ros2 node info <node_name>

2 parameter 参数

  每个节点有自己的参数/配置,参数可以动态调整。

# 列出每个节点的参数
    $ ros2 param list

# 获取某个参数的类型和值
    $ ros2 param get <node_name> <parameter_name>
    # 例如
        $ ros2 param get /turtlesim background_g
    # return
        Integer value is: 86

# 设置某个参数的值
    $ ros2 param set <node_name> <parameter_name> <value>
    # 例如
        $ ros2 param set /turtlesim background_r 150
    # return
        Set parameter successful

# 保存参数到yaml文件
    $ ros2 param dump <node_name>
    # 例如
        $ ros2 param dump /turtlesim
    # return
        Saving to:  ./turtlesim.yaml

# 加载保存的yaml文件
    $ ros2 run <package_name> <executable_name> --ros-args --params-file <file_name>
    # 例如
        $ ros2 run turtlesim turtlesim_node --ros-args --params-file ./turtlesim.yaml

3 topic 话题

在这里插入图片描述

3.1 topic 话题

  topic是节点之间数据交互的一种方式,发送数据的节点采用publish的方式将数据发布到topic,而接收数据的节点采用subscribe的方式从topic订阅数据,可以看下这个官方的gif

# 列出所有话题
    $ ros2 topic list
    $ ros2 topic list -t    # 带topic类型

# 查看话题数据
    $ ros2 topic echo <topic_name>

# 查看话题的类型、发布者和订阅者数量
    $ ros2 topic info <topic_name>
    # 例如
        $ ros2 topic info /turtle1/cmd_vel
    # return
        Type: geometry_msgs/msg/Twist
        Publisher count: 1
        Subscription count: 1

# 查看数据发布频率
    $ ros2 topic hz /turtle1/pose

  These attributes, particularly the type, are how nodes know they’re talking about the same information as it moves over topics.

  roscli相关的节点是命令行程序创建的。

3.2 topic 消息类型 msg

  Nodes send data over topics using messages. Publishers and subscribers must send and receive the same type of message to communicate.

# 查看topic类型
    $ ros2 interface show <type>.msg
    # 例如
        $ ros2 interface show geometry_msgs/msg/Twist
    # return
        # This expresses velocity in free space broken into its linear and angular parts.

        Vector3  linear
        Vector3  angular

3.3 终端发布数据到话题 pub

$ ros2 topic pub <topic_name> <msg_type> '<args>'
    # 例如
    $ ros2 topic pub --once /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0, y: 0.0, z: 0.0}, angular: {x: 0.0, y: 0.0, z: 1.8}}"
    # --once 代表 只发送一次数据即退出
    # --rate 1 代表 以1Hz的频率发送数据

4 service 服务

在这里插入图片描述

4.1 service 服务

  节点之间数据交互的另一种方式是service。话题通过发布与订阅进行(publisher-subscriber model),服务通过请求与应答进行(call-and-response model),只有请求才会应答。
  service包括客户端(service client)和服务器(service server),对于同一个service可以有多个client,但只能有一个server。

# 列出所有服务
    $ ros2 service list
    $ ros2 service list -t  # 带service类型

4.2 service 消息类型 srv

  service的消息不同于topic的消息,前者的消息包括两部分请求消息和应答消息。

# 查看service消息类型
    $ ros2 service type <service_name>
    # 例如
        $ ros2 service type /clear
    # return,Empty 表示 请求和应答都没有数据
        std_srvs/srv/Empty

# 查看service消息类型结构
    $ ros2 interface show <type_name>.srv
    # 例如
        $ ros2 interface show turtlesim/srv/Spawn.srv
    # return,--- 将请求数据和应答数据分开
        float32 x
        float32 y
        float32 theta
        string name # Optional.  A unique name will be created and returned if this is empty
        ---
        string name

# 根据service消息类型找服务
    $ ros2 service find <type_name>

4.3 终端请求服务 call

$ ros2 service call <service_name> <service_type> <arguments>
    # 例如
        $ ros2 service call /clear std_srvs/srv/Empty
    # return
        waiting for service to become available...
        requester: making request: std_srvs.srv.Empty_Request()

        response:
        std_srvs.srv.Empty_Response()

    # 例如
        $ ros2 service call /spawn turtlesim/srv/Spawn "{x: 2, y: 2, theta: 0.2, name: ''}"
    # return
        waiting for service to become available...
        requester: making request: turtlesim.srv.Spawn_Request(x=2.0, y=2.0, theta=0.2, name='')

        response:
        turtlesim.srv.Spawn_Response(name='turtle2')

5 action 动作

在这里插入图片描述

5.1 action 动作

  action是节点间长时间通信的的一种方式,基于topic和service,组成如上图
  首先,action client发出目标服务请求,action server告知action client收到目标请求;然后,action client发出结果服务请求,action server返回反馈流和结果。
  action是可抢占的,也就是可以中途停止,或更换目标。

# 列出所有的action
    $ ros2 action list
    $ ros2 action list -t # 带action类型

# 查看action信息
    $ ros2 action info <action_name>
    # 例如
        $ ros2 action info /turtle1/rotate_absolute
    # return
        Action: /turtle1/rotate_absolute
        Action clients: 1
            /teleop_turtle
        Action servers: 1
            /turtlesim

5.2 action 消息类型 action

# 查看action类型
    $ ros2 interface show <type_name>.action
    # 例如
        $ ros2 interface show turtlesim/action/RotateAbsolute.action
    # return
        # 请求
        float32 theta
        ---
        # 结果
        float32 delta
        ---
        # 反馈
        float32 remaining

5.3 终端发送动作请求 send_goal

    $ ros2 action send_goal <action_name> <action_type> <values>
    # 例如
        $ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: 1.57}"
    # return
        Waiting for an action server to become available...
        Sending goal:
           theta: 1.57

        Goal accepted with ID: f8db8f44410849eaa93d3feb747dd444

        Result:
          delta: -1.568000316619873

        Goal finished with status: SUCCEEDED

    # 带反馈 --feedback
    $ ros2 action send_goal /turtle1/rotate_absolute turtlesim/action/RotateAbsolute "{theta: -1.57}" --feedback

  每个动作目标都有一个对应ID。

6 rqt_graph

  运行rqt_graph,也可以从rqt打开。
在这里插入图片描述

7 小结

  节点是一个完整机器人系统的基础组成部分,每个节点有自己的参数,这些参数可以动态配置。节点间的通信方式包括topic(publisher-subscriber model)、service(call-and-response model)和action(topic-service model),topic适合于描述实时状态的数据交流,service适合于固定数据请求或指令的发布,action适合于发布指令(需要花费一段时间才能完成的指令,for long running tasks)、获取反馈等情形。
  rqt_graph提供了一个可视化界面,能够清晰地描述系统工作过程中各部分的依赖关系。

ROS2_Foxy学习2——环境配置

1 配置环境变量

  使用ROS前需要配置ROS2的环境变量,方式是通过ROS2安装文件的setup.bash:对于二进制文件安装和编译安装,bash文件所在位置不同。

# 二进制文件安装
$ source /opt/ros/foxy/setup.bash
# 编译安装,在install文件夹内
$ source ~/ros2_foxy/install/setup.bash

注意:编译后build文件夹不可删除,~/ros2_foxy/install/setup.bash关联/build内的bash文件。

  每次打开新的终端都需要source一次,以获取ROS2安装文件,也可以选择将source的过程添加到启动脚本中,这样就不需要每次都source。

$ echo "source /opt/ros/foxy/setup.bash" >> ~/.bashrc

  另外,启动脚本的修改只需要打开bashrc文件修改即可

$ sudo gedit ~/.bashrc

注意:启动脚本修改后,需要重启终端以实现。

2 检查环境变量

  如果在编译时找不到包,可以检查环境变量,foxy应该有以下内容。

$ printenv | grep -i ROS

ROS_VERSION=2
ROS_PYTHON_VERSION=3
ROS_DISTRO=foxy

  对于多机问题,为区分各组机器人,需要修改一个环境变量ROS_DOMAIN_ID,其值是0~232的整数。ROS_DOMAIN_ID相同的机器人属于一个组,可以通信,而不同域ID的机器人不能通信且互不影响。环境变量的设置如下:

# 注意 !!! 等号两侧不要有空格
$ export ROS_DOMAIN_ID=<your_domain_id> 
# 也可以写入启动脚本
$ echo "export ROS_DOMAIN_ID=<your_domain_id>" >> ~/.bashrc

ROS2_Foxy学习1——前言与安装

前言

1、关于ROS2.0的发布背景、优势等,可以参考古月居的ROS2探索总结系列

2、相比于ROS1,ROS2的特点主要有以下几点:

(1)跨平台
  支持构建的系统包括Linux、windows、Mac、RTOS等。

(2)DDS
  ROS2取消了master机制,取而代之的是DDS(Data Distribution Service,数据分发服务)。DDS 的技术核心是以数据为核心的发布订阅模型(Data-Centric Publish-Subscribe ,DCPS),这种DCPS模型创建了一个“全局数据空间”(global data space)的概念,所有独立的应用都可以去访问。在DDS中,每一个发布者或者订阅者都成为参与者(participant),类似于ROS中节点的概念。每一个参与者都可以使用某种定义好的数据类型来读写全局数据空间。
  下图分别是ROS1 架构和ROS2 架构:
ROS1 架构

ROS2 架构
(3)对多机系统更为友好

3、学习过程中,主要使用两种运行环境:Ubuntu 20.04 amd64Ubuntu 20.04 mate arm64,其中前者运行在笔记本上,后者运行在树莓派4B上。以amd64为主,对于arm64的差异会有一定的补充说明。

4、在学习过程中,主要参考古月居的文章和ROS2的官方教程。官方教程除了在线阅读之外,还可以下载到本地

$ git clone https://github.com/ros2/ros2_documentation.git
$ cd ~/ros2_documentation   #注意,这个路径可能不一样
$ make html

可能需要安装下面两个Python包

$ pip3 install sphinx
$ pip3 install sphinx_tabs

5、前言就这些,也许还会补充…

安装

  在Ubuntu 20.04 amd64上安装ROS2:根据官方教程Installing ROS 2 on Linux即可。
  在Ubuntu 20.04 mate arm64上装ROS2:根据官方教程Building ROS 2 on Linux即可。

  由于ROS2对arm架构的树莓派4B的支持不够,所以选择编译源码的方式安装,过程如下。

安装环境:基于树莓派4B与Ubuntu20.04 mate

硬件:树莓派4B arm64
系统:Ubuntu 20.04 mate
ROS:ROS Foxy

编译过程非常漫长,不算解决问题的时间,大概需要三到四个小时…

注意 编译后build文件夹不可删除,~/ros2_foxy/install/setup.bash关联build内的bash文件。

所遇问题

1、步骤 Add the ROS 2 apt repository

$ gpg:no valid openPGP data found

解决方案参考,这个问题在amd64安装中也存在:

# 打开hosts文件
$ sudo gedit /etc/hosts
# 在文件末尾添加
151.101.84.133 raw.githubusercontent.com

2、步骤 Install dependencies using rosdep

解决方案同上

ERROR: cannot download default source list from:
https://raw.githubusercontent.com/ros/rosdistro/master/rosdep/sources.list.d/20-default.list
website may be down.

3、步骤 Build the code in the workspace——mimick_vendor

[Processing: mimick_vendor, orocos_kdl, rviz_ogre_vendor, urdfdom]                  
--- stderr: mimick_vendor                                                           
CMake Error at CMakeLists.txt:88 (message):
  Architecture 'armv7l' is not supported.

make[2]: *** [CMakeFiles/mimick-ext.dir/build.make:106: mimick-ext-prefix/src/mimick-ext-stamp/mimick-ext-configure] Error 1
make[1]: *** [CMakeFiles/Makefile2:137: CMakeFiles/mimick-ext.dir/all] Error 2
make: *** [Makefile:141: all] Error 2
---
Failed   <<< mimick_vendor [57.1s, exited with code 2]

原因:

(1)mimick不支持armv7l架构。
(2)GitHub有两个mimick,分别是Snaipe/Mimickros2/Mimick,后者在前者的基础上修改应用到ros2。
(3)根据这里,arm7l是一个不同于arm32的体系结构,如果没有明确表示为单独的体系结构选项,它将不会被检测为兼容的体系结构。
(4)解决方案参考Snaipe/Mimick#17:在Snaipe/Mimickros2/Mimick的CMakeLists.txt中,有如下部分,可以看到不支持armv7l架构。

if (MSVC)
  //省略
else ()
  set (CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=c99 -Wall -Wextra -Wno-unused-parameter")

  enable_language (ASM)
  if (_ARCH MATCHES "${I386}")
    //省略
  elseif (_ARCH MATCHES "${AMD64}")
    //省略
  elseif (_ARCH MATCHES "${ARM32}")
    //省略
  elseif (_ARCH MATCHES "${ARM64}")
    //省略
  //加在这里***  
  else ()
    message (FATAL_ERROR "Architecture '${_ARCH}' is not supported.")
  endif ()
  set (ASM_EXTENSION ".S")
endif ()

解决方法是在对应位置增加以下代码段:

elseif (_ARCH MATCHES "armv7l")
    set (MMK_ARCH "arm")
    set (MMK_ABI "arm")
    set (MMK_BITS 32)
    set (MMK_ARCH_ARM 1)

(5)但是,这个CMakeLists.txt并不在/src/ros2/mimick_verdor文件夹内,而是在 /build/mimick_vendor/mimick-ext-prefix/src/mimick-ext,/src内只提供了url。需要打补丁的CMakeLists.txt可以在编译出错后找到,修改,再编译即可。
(6)这个问题估计很快就会被官方修改,期待…

4、步骤 Build the code in the workspace

如果觉得编译时间太长,可以选择不编译一些包,如一些demo,方法是在对应目录中,添加一个文件

$ touch AMENT_IGNORE

5、步骤 Build the code in the workspace——tracetools

--- stderr: rcl                                                                                                                           
    CMake Error at CMakeLists.txt:13 (find_package):
      By not providing "Findtracetools.cmake" in CMAKE_MODULE_PATH this project
      has asked CMake to find a package configuration file provided by
      "tracetools", but CMake did not find one.

      Could not find a package configuration file provided by "tracetools" with
      any of the following names:

        tracetoolsConfig.cmake
        tracetools-config.cmake

      Add the installation prefix of "tracetools" to CMAKE_PREFIX_PATH or set
      "tracetools_DIR" to a directory containing one of the above files.  If
      "tracetools" provides a separate development package or SDK, be sure it has
      been installed.
    ---

检查发现目录/src/ros-tracing/ros2_tracing下是空的,查看repos,找到如下内容

ros-tracing/ros2_tracing:
    type: git
    url: https://gitlab.com/ros-tracing/ros2_tracing.git
    version: foxy

然后,重新在对应的地址下载源码,解压到对应目录,再编译。

运动规划学习笔记5——ROS包_tf2

在这里分享了运动规划方面的一些基本的算法原理和伪代码实现,其中内容可能存在不完善和错误之处,如有读者发现,欢迎批评指正。

1、坐标系

  1. 一个机器人拥有多个坐标系,这些坐标系在tf2中构成坐标树,tf2通过坐标树维护多个坐标系之间的坐标变换。
  2. ROS坐标系为右手坐标系,前X、左Y、上Z。
  3. 两坐标系之间关系用6自由度相对位姿表示,tf2中由tf2::Vector3tf2::Quaternion表示。
  4. 查看坐标树
    # 方法一:view_frames工具
    rosrun tf view_frames 
    # 方法二:rqt
    rosrun rqt_tf_tree rqt_tf_tree
  5. 查看坐标转换关系
    # 变换关系
    rosrun tf tf_echo /world /turtle1
  6. 查看坐标转换时间延迟
    # 时间延迟
    rosrun tf tf_monitor /world /turtle1

2、坐标变换

2.1、坐标变换

  1. 坐标变换库:tf2_ros,http://docs.ros.org/en/jade/api/tf2_ros/html/c++/namespacetf2__ros.html
  2. 坐标转换消息类型:geometry_msgs/TransformStamped,包括父坐标系、子坐标系、转换关系
    std_msgs/Header header
    uint32 seq
    time stamp
    string frame_id
    string child_frame_id
    geometry_msgs/Transform transform
    geometry_msgs/Vector3 translation
    float64 x
    float64 y
    float64 z
    geometry_msgs/Quaternion rotation
    float64 x
    float64 y
    float64 z
    float64 w

2.2、广播坐标变换

  1. 广播坐标转换的两种情况
    广播静态坐标转换
    广播器:tf2_ros::StaticTransformBroadcaster
    广播方法:tf2_ros::StaticTransformBroadcaster::sendTransform(geometry_msgs/TransformStamped)
    广播动态坐标转换
    广播器:tf2_ros::TransformBroadcaster
    广播方法:tf2_ros::TransformBroadcaster::sendTransform(geometry_msgs/TransformStamped)
    广播方法:
    1、发送一个TransformStamped消息
    void sendTransform (const geometry_msgs::TransformStamped &transform)
    2、发送一组TransformStamped消息
    void sendTransform (const std::vector< geometry_msgs::TransformStamped > &transforms)
  2. 广播静态坐标转换

    static tf2_ros::StaticTransformBroadcaster static_br;
    geometry_msgs::TransformStamped static_tfStamped;
    
    static_tfStamped.header.stamp = ros::Time::now();
    static_tfStamped.header.frame_id = "parent_frame";
    static_tfStamped.child_frame_id = "child_frame";
    static_tfStamped.transform.translation.x = atof(x);
    static_tfStamped.transform.translation.y = atof(y);
    static_tfStamped.transform.translation.z = atof(z);
    
    tf2::Quaternion quat;
    quat.setRPY(atof(wx), atof(wy), atof(wz);
    static_tfStamped.transform.rotation.x = quat.x();
    static_tfStamped.transform.rotation.y = quat.y();
    static_tfStamped.transform.rotation.z = quat.z();
    static_tfStamped.transform.rotation.w = quat.w();
    
    static_br.sendTransform(static_tfStamped);
  3. 广播动态坐标转换

    void callback(const xxx& msg)
    {
     static tf2_ros::TransformBroadcaster br;
     geometry_msgs::TransformStamped tfStamped;
    
     tfStamped.header.stamp = ros::Time::now();
     tfStamped.header.frame_id = "parent_frame";
     tfStamped.child_frame_id = "child_frame";
     tfStamped.transform.translation.x = msg->x;
     tfStamped.transform.translation.y = msg->y;
     tfStamped.transform.translation.z = msg->z;
    
     tf2::Quaternion q;
     q.setRPY(msg->wx, msg->wy, msg->wz);
     tfStamped.transform.rotation.x = q.x();
     tfStamped.transform.rotation.y = q.y();
     tfStamped.transform.rotation.z = q.z();
     tfStamped.transform.rotation.w = q.w();
    
     br.sendTransform(tfStamped);
    }

2.3、监听坐标转换

  1. 监听坐标转换

    监听坐标转换
    监听器:tf2_ros::TransformListener
    监听缓存设置
    缓存器:tf2_ros::Buffer 默认10s缓存
    监听方法
    geometry_msgs::TransformStamped     lookupTransform (const std::string &target_frame, const std::string &source_frame, const ros::Time &time, const ros::Duration timeout) const
    #include <tf2_ros/transform_listener.h>
    
    tf2_ros::Buffer tfBuffer;
    tf2_ros::TransformListener tfListener(tfBuffer);
    
    try
    {
        transformStamped = tfBuffer.lookupTransform("turtle2", "turtle1",ros::Time(0));
    }
    catch (tf2::TransformException &ex) 
    {
        ROS_WARN("%s",ex.what());
        ros::Duration(1.0).sleep();
        continue;
    }

3、问题

/usr/bin/env: “python”: 没有那个文件或目录

  1. 背景
    ubuntu20.04
    roslaunch turtle_tf turtle_tf_demo.launch
  2. 解决
    sudo update-alternatives --install /usr/bin/python python /usr/bin/python3 1000

    TypeError: cannot use a string pattern on a bytes-like object

    https://blog.csdn.net/xiaowang_tongxue/article/details/108298544

  3. 问题
    Listening to /tf for 5.0 seconds
    Done Listening
    b'dot - graphviz version 2.43.0 (0)\n'
    Traceback (most recent call last):
    File "/opt/ros/noetic/lib/tf/view_frames", line 119, in <module>
    generate(dot_graph)
    File "/opt/ros/noetic/lib/tf/view_frames", line 89, in generate
    m = r.search(vstr)
    TypeError: cannot use a string pattern on a bytes-like object
  4. 解决
    sudo gedit /opt/ros/noetic/lib/tf/view_frames
    88       print(vstr)
          vstr=str(vstr)  //加入这个...
    89       m = r.search(vstr)

运动规划学习笔记4——探索OMPL

在这里分享了运动规划方面的一些基本的算法原理和伪代码实现,其中内容可能存在不完善和错误之处,如有读者发现,欢迎批评指正。

A、OMPL编译与安装

  1. 方式一:通过官网下载install-ompl-ubuntu.sh文件,运行后,自动编译安装 https://ompl.kavrakilab.org/installation.html,此方法在使用库文件时,cmakeconfig文件可能有问题,还是建议方法二。
  2. 方式二:通过Github下载源码,自己编译安装, https://github.com/ompl/ompl
  3. 检查安装成功 及 ROS中使用 https://blog.csdn.net/zghforever/article/details/106688410
  4. 官方教程 https://ompl.kavrakilab.org/tutorials.html

B、OMPL使用

B1、基本定义

  1. 求解方式
    求解问题的过程中,根据是否使用ompl::geometric::SimpleSetup类,有两种方式,简单规划和完整规划,见代码部分C1
  2. 求解过程:利用OMPL提供的类和方法来规划,基本过程包括以下几个部分
    ♥ 确认实际问题的状态空间 (如SE(3))
    ♥ 从OMPL提供的类中构建一个状态空间(如ompl::base::SE3StateSpace is appropriate)
    ♥ 设置状态空间的边界
    ♥ 定义状态有效性的判断方法
    ♥ 定义起点和终点的状态
    ♥ 求解和可视化
  3. 状态与状态空间

    //状态与状态空间的基本定义
    ompl::base::StateSpacePtr space(new T());
    ompl::base::ScopedState<> state1(space);    //直接通过状态空间StateSpace初始化
    ompl::base::SpaceInformationPtr si(space);
    ompl::base::ScopedState<T> state2(si);      //通过SpaceInformation初始化
    //举个栗子
    ompl::base::StateSpacePtr space(new ompl::base::SE2StateSpace());
    ompl::base::ScopedState<ompl::base::SE2StateSpace> state(space);
        state->setX(0.1);
        state->setY(0.2);
        state->setYaw(0.0);
    ompl::base::ScopedState<> backup = state;   //state == backup, backup本质是State*, 作用是备份参数, 因此setX()等函数无效
    //状态空间的合并
    //合并方式1
    ompl::base::CompoundStateSpace *cs = new ompl::base::CompoundStateSpace();  //利用CompoundStateSpace类来添加不同状态空间
    cs->addSubspace(ompl::base::StateSpacePtr(new ompl::base::SO2StateSpace()), 1.0);
    cs->addSubspace(ompl::base::StateSpacePtr(new ompl::base::SO3StateSpace()), 1.0);
    ompl::base::StateSpacePtr space(cs);
    //合并方式2
    // define the individual state spaces
    ompl::base::StateSpacePtr so2(new ompl::base::SO2StateSpace());
    ompl::base::StateSpacePtr so3(new ompl::base::SO3StateSpace());
    ompl::base::StateSpacePtr space = so2 + so3;
    
    //虽然空间可以混合,但对于状态仍然需要指定具体的类型
    ompl::base::ScopedState<ompl::base::CompoundStateSpace> state(space);
    state->as<ompl::base::SO2StateSpace::StateType>(0)->setIdentity();
  4. 有效性检查
    OMPL不提供具体检查方式,需要根据具体的问题具体确定。用户指定有效性检查的方式有两种,见代码部分C2
    ♥ 继承OMPL定义的两个抽象类
    ♥ 直接定义有效性检查函数

    // 方法2:直接定义判断函数
    bool myStateValidityCheckerFunction(const base::State *state)
    {
     return ...;
    }
    //方法2:使用
    base::SpaceInformationPtr si(space);
    si->setStateValidityChecker(myStateValidityCheckerFunction);
    si->setStateValidityCheckingResolution(0.03); // 3%
    si->setup();
  5. 采样
    采样的两种方式:基于ompl::base::StateSampler和基于ompl::base::ValidStateSampler
    ♥ 基于ompl::base::StateSampler:生成状态空间的一个状态,生成状态附近的一个状态,生成高斯分布的一组状态。
    ♥ 基于ompl::base::ValidStateSampler:以ompl::base::StateSampler为基础,不断采样,直到采到一个有效状态或者到达迭代最大值,有效性通过ompl::base::SpaceInformation::isValid判断。
    ♥ OMPL提供了几个继承自ompl::base::ValidStateSampler的类,如下,可以使用这些类或继承ompl::base::ValidStateSampler重新编写采样器,基本使用方法如下,具体代码与解释见代码部分C3
ompl::base::UniformValidStateSampler 默认采样器
ompl::base::ObstacleBasedValidStateSampler 采样一个有效状态和一个无效状态,在两者之间插值,返回最接近无效状态的一个有效的插值状态,即逼近障碍物
ompl::base::GaussianValidStateSampler 采样一对状态,第一个状态均匀随机采集,第二个状态在以第一个状态为中心的高斯分布上采集,两状态都有效或都无效,则再取一对,否则返回有效状态,也在逼近障碍物
ompl::base::MaximizeClearanceValidStateSampler 在默认采样器的基础上,出于安全考虑,最大化路径间隙,使得状态与障碍物之间的间隙最大化
//基本使用方式
//***************************定义状态分配器(函数)***************************
ompl::base::ValidStateSamplerPtr allocOBValidStateSampler(const ompl::base::SpaceInformation *si)
{
    // we can perform any additional setup / configuration of a sampler here,
    // but there is nothing to tweak in case of the ObstacleBasedValidStateSampler.
    return std::make_shared<ompl::base::ObstacleBasedValidStateSampler>(si);
}
//***************************使用***************************
ss.getSpaceInformation()->setValidStateSamplerAllocator(allocOBValidStateSampler);

B2、路径可视化

  1. OMPL不提供路径可视化工具,直接一点的方式是作为矩阵打印输出,保存成文本文件。
  2. OMPL提供两种路径:ompl::geometric::PathGeometricompl::control::PathControl,两个类可用成员函数printAsMatrix()打印,前者每行打印一个状态,后者每行除了状态,还有控制指令和控制间隔时间。
  3. 打印方式如下:

    bool solved = ss.solve(20.0);
    if (solved)
    {
    // if ss is a ompl::geometric::SimpleSetup object
    ss.getSolutionPath().printAsMatrix(std::cout);
    
    // if ss is a ompl::control::SimpleSetup object
    ss.getSolutionPath().asGeometric().printAsMatrix(std::cout);
    }

B3、API Overview

在这里插入图片描述

C、代码附录

C1、Geometric Planning for a Rigid Body in 3D

//注:这是在ROS里面写的,虽然这一部分跟ROS暂时没关系。
#include <ros/ros.h>
#include <iostream>

#include <ompl/base/spaces/SE3StateSpace.h>

//使用ompl::geometric::SimpleSetup类
#include <ompl/geometric/SimpleSetup.h>
//不使用ompl::geometric::SimpleSetup类
#include <ompl/base/SpaceInformation.h>
#include <ompl/geometric/planners/rrt/RRTConnect.h>

namespace ob = ompl::base;
namespace og = ompl::geometric;

//状态检查函数
bool isStateValid(const ob::State *state)
{
    // cast the abstract state type to the type we expect
    // 将抽象状态类型转换为我们期望的类型
    const auto *se3state = state->as<ob::SE3StateSpace::StateType>();

    // extract the first component of the state and cast it to what we expect 
    // 提取状态的第一个组件并将其转换为我们所期望的
    const auto *pos = se3state->as<ob::RealVectorStateSpace::StateType>(0);

    // extract the second component of the state and cast it to what we expect
    // 提取状态的第二个组件并将其转换为我们所期望的
    const auto *rot = se3state->as<ob::SO3StateSpace::StateType>(1);

    // check validity of state defined by pos & rot

    // return a value that is always true but uses the two variables we define, so we avoid compiler warnings
    // 返回一个始终为true但使用我们定义的两个变量的值
    return (const void*)rot != (const void*)pos;
}

//简单规划:使用ompl::geometric::SimpleSetup类
void planWithSimpleSetup()
{
    // 状态空间:构建
    auto space(std::make_shared<ob::SE3StateSpace>());

    // 状态空间:边界
    ob::RealVectorBounds bounds(3);
    bounds.setLow(-1);
    bounds.setHigh(1);
    space->setBounds(bounds);

    // 配置类:构建
    og::SimpleSetup ss(space);

    // 配置类:设置状态有效性检查器
    ss.setStateValidityChecker([](const ob::State *state) { return isStateValid(state); });

    // 配置类:设置起终点 
    ob::ScopedState<> start(space);
    start.random();
    ob::ScopedState<> goal(space);
    goal.random();
    ss.setStartAndGoalStates(start, goal);

    // this call is optional, but we put it in to get more output information
    ss.setup();
    ss.print();

    // attempt to solve the problem within one second of planning time
    ob::PlannerStatus solved = ss.solve(1.0);

    if (solved)
    {
        std::cout << "Found solution:" << std::endl;
        // print the path to screen
        ss.simplifySolution();
        ss.getSolutionPath().print(std::cout);
    }
    else
        std::cout << "No solution found" << std::endl;
}

//完整规划:不使用ompl::geometric::SimpleSetup类
void plan()
{
    // 状态空间:构建
    auto space(std::make_shared<ob::SE3StateSpace>());

    // 状态空间:边界
    ob::RealVectorBounds bounds(3);
    bounds.setLow(-1);
    bounds.setHigh(1);
    space->setBounds(bounds);

    // 空间信息:构建
    auto si(std::make_shared<ob::SpaceInformation>(space));

    // 空间信息:设置状态有效性检查器
    si->setStateValidityChecker(isStateValid);

    // 问题实例:构建
    auto pdef(std::make_shared<ob::ProblemDefinition>(si));

    // 问题实例:设置起终点
    ob::ScopedState<> start(space);
    start.random();
    ob::ScopedState<> goal(space);
    goal.random();
    pdef->setStartAndGoalStates(start, goal);

    // 规划方法:构建
    auto planner(std::make_shared<og::RRTConnect>(si));

    // 规划方法:实际问题
    planner->setProblemDefinition(pdef);

    // 规划方法:初始化
    planner->setup();

    // 打印空间信息和问题实例
    si->printSettings(std::cout);
    pdef->print(std::cout);

    // 求解
    ob::PlannerStatus solved = planner->ob::Planner::solve(1.0);

    if (solved)
    {
        // get the goal representation from the problem definition (not the same as the goal state)
        // and inquire about the found path
        ob::PathPtr path = pdef->getSolutionPath();
        std::cout << "Found solution:" << std::endl;

        // print the path to screen
        path->print(std::cout);
    }
    else
        std::cout << "No solution found" << std::endl;
}

int main(int argc, char** argv)
{
    ros::init(argc, argv, "rigid_body_planning");
    ros::NodeHandle nh;

    std::cout << "OMPL version: " << OMPL_VERSION << std::endl;
    plan();
    std::cout << std::endl << std::endl;
    planWithSimpleSetup();  

    return 0;
}

C2、有效性检查方式

//方法1:继承OMPL定义的抽象类 base::StateValidityChecker
class myStateValidityCheckerClass : public base::StateValidityChecker
{
public:
     myStateValidityCheckerClass(const base::SpaceInformationPtr &si) :
       base::StateValidityChecker(si)
        {
     }

     virtual bool isValid(const base::State *state) const
     {
             return ...;
     }
};
//方法1:使用
base::SpaceInformationPtr si(space);
si->setStateValidityChecker(std::make_shared<myStateValidityCheckerClass>(si));
si->setup();

//方法1:继承OMPL定义的抽象类 base::MotionValidator
class myMotionValidator : public base::MotionValidator
{
public:
    // implement checkMotion()
};
//方法1:使用
base::SpaceInformationPtr si(space);
si->setMotionValidator(std::make_shared<myMotionValidator>(si));
si->setup();

// 方法2:直接定义判断函数
bool myStateValidityCheckerFunction(const base::State *state)
{
     return ...;
}
//方法2:使用
base::SpaceInformationPtr si(space);
si->setStateValidityChecker(myStateValidityCheckerFunction);
si->setStateValidityCheckingResolution(0.03); // 3%
si->setup();

C3、采样方式

1 已有采样器

不能直接在SimpleSetup或者SpaceInformation类中配置采样方式,需要通过定义下面类型的函数,输入ompl::base::SpaceInformation,返回ompl::base::ValidStateSamplerPtr。

//***************************定义状态分配器(函数)***************************
ompl::base::ValidStateSamplerPtr allocOBValidStateSampler(const ompl::base::SpaceInformation *si)
{
    // we can perform any additional setup / configuration of a sampler here,
    // but there is nothing to tweak in case of the ObstacleBasedValidStateSampler.
    return std::make_shared<ompl::base::ObstacleBasedValidStateSampler>(si);
}
//***************************使用***************************
ss.getSpaceInformation()->setValidStateSamplerAllocator(allocOBValidStateSampler);

2 自定义采样器

继承ompl::base::ValidStateSampler重新编写采样器,继承后的类与OMPL提供的类的使用方法相同。

//***************************定义子类***************************
namespace ob = ompl::base;
namespace og = ompl::geometric;

// This is a problem-specific sampler that automatically generates valid
// states; it doesn't need to call SpaceInformation::isValid. This is an
// example of constrained sampling. If you can explicitly describe the set valid
// states and can draw samples from it, then this is typically much more
// efficient than generating random samples from the entire state space and
// checking for validity.
class MyValidStateSampler : public ob::ValidStateSampler
{
public:
    MyValidStateSampler(const ob::SpaceInformation *si) : ValidStateSampler(si)
    {
        name_ = "my sampler";
    }
    // Generate a sample in the valid part of the R^3 state space
    // Valid states satisfy the following constraints:
    // -1<= x,y,z <=1
    // if .25 <= z <= .5, then |x|>.8 and |y|>.8
    bool sample(ob::State *state) override
    {
        double* val = static_cast<ob::RealVectorStateSpace::StateType*>(state)->values;
        double z = rng_.uniformReal(-1,1);

        if (z>.25 && z<.5)
        {
            double x = rng_.uniformReal(0,1.8), y = rng_.uniformReal(0,.2);
            switch(rng_.uniformInt(0,3))
            {
                case 0: val[0]=x-1;  val[1]=y-1;  break;
                case 1: val[0]=x-.8; val[1]=y+.8; break;
                case 2: val[0]=y-1;  val[1]=x-1;  break;
                case 3: val[0]=y+.8; val[1]=x-.8; break;
            }
        }
        else
        {
            val[0] = rng_.uniformReal(-1,1);
            val[1] = rng_.uniformReal(-1,1);
        }
        val[2] = z;
        assert(si_->isValid(state));
        return true;
    }
    // We don't need this in the example below.
    bool sampleNear(ob::State* /*state*/, const ob::State* /*near*/, const double /*distance*/) override
    {
        throw ompl::Exception("MyValidStateSampler::sampleNear", "not implemented");
        return false;
    }
protected:
    ompl::RNG rng_;
};
//***************************定义状态分配器(函数)***************************
ob::ValidStateSamplerPtr allocMyValidStateSampler(const ob::SpaceInformation *si)
{
    return std::make_shared<MyValidStateSampler>(si);
}
//***************************使用***************************
ss.getSpaceInformation()->setValidStateSamplerAllocator(allocMyValidStateSampler);

运动规划学习笔记3——Dubins Curve

实际问题

在这里插入图片描述

Dubins曲线基础

在这里插入图片描述

问题关键

在这里插入图片描述

CSC型

在这里插入图片描述

在这里插入图片描述

CCC型

在这里插入图片描述

Dubins曲线的一些讨论

在这里插入图片描述
下面是写的一段MATLAB代码,只是简单实现,没有考虑最优。
1、CSC

%Dubins Curve CSC型
%目标定义
    %定义起终点[x y dir]
    I=[1 1 7*pi/4];
    G=[6 8 3*pi/4];
    %定义或计算转弯半径
    ri=1;
    rg=2;
    %组合
    k=-1;    % 1:RSX,-1:LSX
    i=-1;   % 1:XSR,-1:XSL
    j=i*k;  % 1:RSR/LSL, -1:RSL/LSR
%计算首尾圆心坐标
    xi=I(1,1)-ri*k*cos(I(1,3));
    yi=I(1,2)+ri*k*sin(I(1,3));  
    xg=G(1,1)-rg*i*cos(G(1,3));
    yg=G(1,2)+rg*i*sin(G(1,3));
%计算法向量
    %首尾圆圆心之间的向量V1
    v1x=xg-xi;              %向量坐标x
    v1y=yg-yi;              %向量坐标y
    D=sqrt(v1x*v1x+v1y*v1y);%向量模长 
    v1x=v1x/D;              %单位化
    v1y=v1y/D;
    %计算法向量n
    c=(j*ri-rg)/D;            %定义中间量
    nx=v1x*c-i*v1y*sqrt(1-c*c);
    ny=v1y*c+i*v1x*sqrt(1-c*c);
%计算切点
    xit=xi+j*ri*nx;
    yit=yi+j*ri*ny;
    xgt=xg+rg*nx;
    ygt=yg+rg*ny;
%绘图
    %初始方向
    xiDir=[I(1,1)-ri*cos(I(1,3)-pi/2),I(1,1)];
    yiDir=[I(1,2)+ri*sin(I(1,3)-pi/2),I(1,2)];
    xgDir=[G(1,1)-rg*cos(G(1,3)-pi/2),G(1,1)];
    ygDir=[G(1,2)+rg*sin(G(1,3)-pi/2),G(1,2)];
    %切线
    xxx=[xit xgt];
    yyy=[yit ygt];
    %首尾圆
    t=0:0.01:2*pi;
    %t=I(1,3)-pi/2:0.01:3*pi/2-atan(ny/nx);
    xxi=xi+ri*sin(t);
    yyi=yi+ri*cos(t);
    %t=-pi/2-atan(ny/nx):0.01:G(1,3)-pi/2;
    xxg=xg+rg*sin(t);
    yyg=yg+rg*cos(t);
    %法向量
    nxi=[xi xi+ri*nx];
    nyi=[yi yi+ri*ny];
    nxg=[xg xg+rg*nx];
    nyg=[yg yg+rg*ny];
    %绘图
    plot(I(1,1),I(1,2), 'go' ,G(1,1),G(1,2),'go',...      %初始位置
            xiDir,yiDir,'-g',xgDir,ygDir,'-g',...       %初始方向
            xi,yi,'k*',xg,yg,'k*',...                   %圆心
            xxi,yyi,'-r',xxg,yyg,'-r',...               %首尾圆
            nxi,nyi,'-b',nxg,nyg,'-b',...               %法向量
            xxx,yyy,'-ro')                              %切线
    axis([-3 15 -3 15])

2、CCC

%Dubins Curve CCC型

%目标定义
    %定义起终点[x y dir] 
    I=[1 1 7*pi/4];
    G=[4 5 3*pi/4];
    %定义或计算转弯半径
    ri=1;
    rg=1;
    rmid=4;
    %组合
    i=1;    % 1:RLR  -1:LRL

%三圆心相关量
    %计算首尾圆心坐标及其连线向量V12
    xi=I(1,1)-ri*i*cos(I(1,3));
    yi=I(1,2)+ri*i*sin(I(1,3));
    xg=G(1,1)-rg*i*cos(G(1,3));
    yg=G(1,2)+rg*i*sin(G(1,3));

    V12=[xg-xi,yg-yi];
    angleV12=atan(V12(1,2)/V12(1,1));

    %计算中间圆坐标及三圆心连线向量V13、V32
    d12=sqrt((xg-xi)^2+(yg-yi)^2);
    rmid=max([rmid (d12-ri-rg)*0.5]);
    d13=ri+rmid;
    d32=rmid+rg;
    angleP213=acos((d12^2+d13^2-d32^2)/(2*d12*d13));

    xmid=xi+d13*cos(angleV12-angleP213);
    ymid=yi+d13*sin(angleV12-angleP213);

    V13=[xmid-xi,ymid-yi];
    V32=[xg-xmid,yg-ymid];

    V13=V13/d13;    %单位化
    V32=V32/d32;

%计算切点坐标
    xt1=xi+ri*V13(1,1);
    yt1=yi+ri*V13(1,2);
    xt2=xmid+rmid*V32(1,1);
    yt2=ymid+rmid*V32(1,2);

%绘图
    %初始方向
    xiDir=[I(1,1)-ri*cos(I(1,3)-pi/2),I(1,1)];
    yiDir=[I(1,2)+ri*sin(I(1,3)-pi/2),I(1,2)];
    xgDir=[G(1,1)-rg*cos(G(1,3)-pi/2),G(1,1)];
    ygDir=[G(1,2)+rg*sin(G(1,3)-pi/2),G(1,2)];
    %首尾圆
    t=0:0.01:2*pi;
    xxi=xi+ri*sin(t);
    yyi=yi+ri*cos(t);
    xxg=xg+rg*sin(t);
    yyg=yg+rg*cos(t);
    xxmid=xmid+rmid*sin(t);
    yymid=ymid+rmid*cos(t);
    %绘图
    plot(I(1,1),I(1,2),'go',G(1,1),G(1,2),'go',...          %初始位置
            xiDir,yiDir,'-g',xgDir,ygDir,'-g',...           %初始方向
            xi,yi,'k*',xg,yg,'k*',xmid,ymid,'k*',...        %圆心
            xt1,yt1,'bo',xt2,yt2,'bo',...                   %切点
            xxi,yyi,'-r',xxg,yyg,'-r',xxmid,yymid,'-r')     %三圆
    axis([-3 15 -3 15])

参考文献

1、http://planning.cs.uiuc.edu/node821.html
2、http://www.banbeichadexiaojiubei.com/index.php/2020/03/15/自动驾驶运动规划-dubins曲线
3、https://gieseanw.files.wordpress.com/2012/10/dubins.pdf

运动规划学习笔记2——基于采样

在这里分享了运动规划方面的一些基本的算法原理和伪代码实现,其中内容可能存在不完善和错误之处,如有读者发现,欢迎批评指正。

基于采样

这一部分是基于采样的路径规划算法,依次介绍RRT、RRT-connect、RRT*。另外,有关RRT*的其他衍生的算法,日后更新…

RRT

1、RRT (Rapidly-exploring Random Trees),经典的RRT是单树RRT,算法示意图如下:
在这里插入图片描述
在这里插入图片描述
2、算法伪代码解释如下:
在这里插入图片描述
在这里插入图片描述
3、RRT的不足
①收敛慢,目标性不强,这一点相似于BFS,为了整体上向目标点方向生长,一种思路是根据随机的概率,采样点随机地选择通过随机采样还是直接使用目标点作为x_rand,即 x_rand = chooseTarget(sample(map),x_goal,p),另一种思路是双树RRT。
②没有最优解,→RRT*渐进最优解
③采样范围为整个空间

RRT-connect

1、为提高计算效率,可以同时从起点和终点“生长树枝”。就像这样
在这里插入图片描述

2、基本的过程,如图所示:
在这里插入图片描述
3、伪代码,这个还比较详细,基本过程如上图,不再细标
在这里插入图片描述

RRT*

1、RRT*对RRT的改进
RRT存在一个问题,虽然能找到一条可行路径,但不保证最优。RRT*在RRT的基础上,进行改进,使路径渐进最优,方法是x_near和x_new不会直接连接起来,而是做一个优化处理,令x_new在一定范围内重新选择父节点,同时rewire(随机树重布线)。
2、RRT*示意与伪代码
①示意
重选父节点&随机树重布线的图示点这里,描述的还比较清晰。
②伪代码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
(虽然说是伪代码,可能没有很伪代码)

③可参考的更简洁的伪代码
在这里插入图片描述
下面是用QT写的一个简单实现:
在这里插入图片描述

参考文献

1、https://blog.csdn.net/weixin_43465857/article/details/96451631 RRT算法原理图解
2、https://blog.csdn.net/gophae/article/details/103231053 全局路径规划:图搜索算法介绍4(RRT/RRT*)
3、https://zhuanlan.zhihu.com/p/30333530 RRT算法是如何实现的?有哪些优缺点?
4、https://zhuanlan.zhihu.com/p/133224593 基于采样的运动规划算法-RRT(Rapidly-exploring Random Trees)
5、https://www.cnblogs.com/21207-iHome/p/7210543.html RRT路径规划算法
6、https://blog.csdn.net/a735148617/article/details/103647804 从零开始的OMPL库算法学习(2)RRT-connect算法
7、https://zhuanlan.zhihu.com/p/51087819 路径规划——改进RRT算法
8、https://www.jianshu.com/p/489b0b39f3f2 运动规划笔记——邱强

运动规划学习笔记1——基于搜索

在这里分享了运动规划方面的一些基本的算法原理和伪代码实现,其中内容可能存在不完善和错误之处,如有读者发现,欢迎批评指正。

基于搜索

这一部分是基于搜索的路径规划算法,依次介绍BFS、Dijkstra、A* 和Hybrid A*,从顺序上说,这四种算法也是一个不断迭代和优化的过程。另外,有关A*的其他衍生的算法,日后更新…

BFS

BFS(Breadth-First-Searc),基本思想是以放射状扩散的方式遍历所有点。
图解BFS点这里
BFS的伪代码解释如下:
1、基本过程
在这里插入图片描述
2、为生成路径,引入父节点的定义
在这里插入图片描述
3、为避免枚举的问题,引入启发式搜索,以当前点到终点的距离为代价(优先级)
在这里插入图片描述
4、路径回放,生成最终路径
在这里插入图片描述
5、BFS的一个问题是容易先入为主地认为最先遍历的就是最短路径上的点。

Dijkstra

Dijkstra也采用了BFS类似的方法,其代价的计算方式为以当前点到起点的距离为代价(优先级)。
在这里插入图片描述

A*

A*算法结合了BFS和Dijkstra的思想,综合当前点到起点和终点的距离和(代价和/优先级)。
在这里插入图片描述

Hybrid A*

A*算法规划的轨迹经过方格中心,现实车辆路径无法满足这一情况,因此提出了Hybrid A*。

1、这里整理了几种车辆的运动学模型

① 自行车模型的基本假设
·不考虑车辆垂直方向的运动,认为车辆在二维平面上运动
·左右车轮在转速和转角上一致,两个车轮可以合为一个
·车速缓慢,忽略前后轴载荷的转移
·车身和悬架均为刚性
·车辆转向和驱动由前轮完成
② 以后轴中心为原点的车辆运动学模型——输入(a,φ) 输出车辆位姿(x,y,θ)
在这里插入图片描述
③ 以质心为中心的车辆运动学模型——输入(a,δ) 输出车辆位姿(x,y,ψ)
在这里插入图片描述
④ 前轮驱动的车辆运动学模型——输入(a,δ) 输出车辆位姿(x,y,ψ)
在这里插入图片描述
这里假设汽车是前轮驱动的,所以认为方向盘的转角就等于前轮的转角,后轮偏角为0。

2、一种运动学模型下的Hybrid A*算法伪代码实现
①运动学模型:4-D 离散网格中的车辆状态 <x, y, θ, is_foward>,以后轴为中心的车辆运动学模型(离散)。
在这里插入图片描述
②伪代码
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
③与A*的不同
0x00:二维变三维,新增偏航角这一维
0x01:寻找当前位置周围四点,变成寻找当前位置在不同前轮转角下的多个点(range(-35,40,5))

下面是用QT写的一个简单实现:
在这里插入图片描述

参考文献

1、https://www.redblobgames.com/pathfinding/a-star/introduction.html
2、https://blog.csdn.net/AdamShan/article/details/79945175 无人驾驶汽车系统入门(十六)——最短路径搜索之A*算法
3、https://zhuanlan.zhihu.com/p/103834150 自动驾驶中的车辆运动学模型
4、https://zhuanlan.zhihu.com/p/139489196 自动驾驶运动规划-Hybird A* 算法
5、https://blog.csdn.net/Rickey_lee09/article/details/104795530 无人驾驶学习笔记–路径规划(一)【Trajectory Generation – Hybrid A*】