首页 关于
树枝想去撕裂天空 / 却只戳了几个微小的窟窿 / 它透出天外的光亮 / 人们把它叫做月亮和星星
目录

Gazebo的仿真插件

Gazebo通过插件(Plugin)的机制实现灵活的扩展功能。它的插件实际上是一个个的动态链接库,使得我们可以通过C++接触到Gazebo的方方面面。 而插件又是相对独立的,开发人员可以方便共享,使用时也可以方便的插入系统或者从系统中移除。关于它的插件机制, 官方也有很详细的教程API文档

在Gazebo中,针对不同层级的扩展,一共定义了6种插件:World, Model, Sensor, System, Visual, GUI。 这里我们在上一篇文章中实现的激光传感器的基础上,介绍Model的插件。 其它类型的插件,我们会在Gazebo的专题中进行介绍。 在上一篇文章中我们已经创建了一个完整功能的激光传感器模型, 它可以探测前方障碍的距离,具有一个自由度可以实现360°的扫描。如果读者跳过了上一节内容, 可以从这里 或者官方下载模型。

一些准备工作

在开始编写插件之前,我们需要确定已经安装了Gazebo的开发文件。对于Ubuntu系统,不确定的话可以通过如下指令安装,根据自己机器上的Gazebo版本号修改其中的数字7。

        $ sudo apt install libgazebo7-dev

为了方便说明,我们在"~/catkin_ws/src/learning_gazebo/"目录下创建一个子目录"velodyne_plugin"作为编写插件的工作空间。

        $ cd ~/catkin_ws/src/learning_gazebo
        $ mkdir velodyne_plugin
        $ cd velodyne_plugin

1. 从代码到运行的插件之旅

作为一个示例,我们先实现一个Model插件打印与之关联的模型名称。整个过程会经过四个阶段:编写C++程序、创建CMake编译脚本、在SDF文件中插入插件、运行测试。

1.1 编写C++程序

首先让我们创建一个"velodyne_plugin.cc"的文件,并将如下的代码拷贝进去。在代码中有比较详细的注释这里不再做解释了。

        #ifndef _VELODYNE_PLUGIN_HH_
        #define _VELODYNE_PLUGIN_HH_
        
        #include <gazebo/gazebo.hh>
        #include <gazebo/physics/physics.hh>
        
        namespace gazebo
        {
            // 一个控制Velodyne传感器的插件,它是一个Model类型的插件,所以继承自ModelPlugin
            class VelodynePlugin : public ModelPlugin
            {
                // 构造函数
                public: VelodynePlugin() {}
        
                // 在插入本插件的时候,Gazebo就会调用该函数。
                // 该函数是一个虚函数,我们可以通过重载该函数来对插件进行一些初始化的操作。
                // 输入参数_model是一个指向与本插件相关联的模型的指针。
                // 输入参数_sdf则是一个指向本插件SDF元素的指针。
                public: virtual void Load(physics::ModelPtr _model, sdf::ElementPtr _sdf)
                {
                    // 目前我们只是打印出关联模型的名称
                    std::cerr << "\n本插件与模型[" << _model->GetName() << "]相关联" << std::endl;
                }
            };
        
            // 向Gazebo注册本插件
            GZ_REGISTER_MODEL_PLUGIN(VelodynePlugin)
        }
        #endif

1.2 创建CMake编译脚本

接着,我们创建一个"CMakeLists.txt"的文件,将如下的内容拷贝进去,并通过CMake工具编译插件。

        cmake_minimum_required(VERSION 2.8 FATAL_ERROR)
        
        # Find Gazebo
        find_package(gazebo REQUIRED)
        include_directories(${GAZEBO_INCLUDE_DIRS})
        link_directories(${GAZEBO_LIBRARY_DIRS})
        set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${GAZEBO_CXX_FLAGS}")
        
        # Build our plugin
        add_library(velodyne_plugin SHARED velodyne_plugin.cc)
        target_link_libraries(velodyne_plugin ${GAZEBO_libraries})
然后创建一个build目录并在该目录下编译插件:
        $ mkdir build
        $ cd build
        $ cmake ..
        $ make
如果编译没有问题,我们就会在build目录下看到"libvelodyne_plugin.so"的一个动态链接库。

1.3 在SDF文件中插入插件

接下来,我们回到一开始的工作空间中,创建一个"velodyne.world"的SDF文件,并把如下的内容拷贝进去:

        <?xml version="1.0" ?>
        <sdf version="1.5">
            <world name="default">
                <!-- A global light source -->
                <include><uri>model://sun</uri></include>
                <!-- A ground plane -->
                <include><uri>model://ground_plane</uri></include>
                <!-- A testing model that includes the Velodyne sensor model -->
                <model name="my_velodyne">
                    <include><uri>model://velodyne_hdl32</uri></include>
                    <!-- Attach the plugin to this model -->
                    <plugin name="velodyne_control" filename="libvelodyne_plugin.so"/>
                </model>
            </world>
        </sdf>
这个SDF文件描述了一个叫做"default"的world,其中有三个模型分别描述了光照、地面和velodyne HDL32激光传感器。 激光传感器命名为"my_velodyne",插入了我们创建的插件"libvelodyne_plugin.so"。

1.4 运行测试

在运行之前我们需要确认一下系统能够找到"libvelodyne_plugin.so",我们可以通过export指令把build的子目录添加到环境变量LD_LIBRARY_PATH中。其中`pwd`的作用是获取当前路径。

        $ cd build
        $ export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:`pwd`
        $ gazebo ../velodyne.world
运行上述指令之后就可以看到如下的日志:
        本插件与模型[my_velodyne]相关联

2. 更细致的控制

我们已经实现了一个基本的Gazebo插件,现在通过PID控制器来控制激光传感器的转速。重新打开velodyne_plugin.cc文件,在其中类VelodynePlugin中添加三个私有的变量。 其中model和joint两个变量分别保存我们控制的传感器模型和转动关节,pid则是关节PID控制器。

        private:
            physics::ModelPtr model;
            physics::JointPtr joint;
            common::PID pid;
重写成员函数Load()如下:
        public: virtual void Load(physics::ModelPtr _model, sdf::ElementPtr _sdf)
        {
             if (_model->GetJointCount() == 0) {
                 std::cerr << "没有正确装在velodyne模型\n";
                 return;
             }
             this->model = _model;
             this->joint = _model->GetJoints()[0];
             // 配置PID控制器, 比例增益为0.1.
             this->pid = common::PID(0.1, 0, 0);
             // 应用PID控制器
             this->model->GetJointController()->SetVelocityPID(this->joint->GetScopedName(), this->pid);
             // 设定关节的目标速度
             this->model->GetJointController()->SetVelocityTarget(this->joint->GetScopedName(), 10.0);
        }
重新编译运行之后,我们就可以看到传感器在旋转。如果修改一下上述Load()最后的目标速度,重新编译运行可以看到转速是有变化的。 这种修改速度的方法是很繁琐的,下面我们看一种相对比较灵活的方法,在加载的SDF文件中修改转速。重新打开velodyne.world文件,修改<plugin>标签如下:
        <plugin name="velodyne_control" filename="libvelodyne_plugin.so">
            <velocity>25</velocity>
        </plugin>
然后在Load()函数中添加对SDF文件的解析,从中获取速度的参数:
        double velocity = 0;
        // 解析SDF文件,从中获取速度参数
        if (_sdf->HasElement("velocity"))
          velocity = _sdf->Get<double>("velocity");
        // 设定关节的目标速度
        this->model->GetJointController()->SetVelocityTarget(this->joint->GetScopedName(), velocity);
如此,我们可以修改SDF文件来改变传感器的扫描速度,不用重新编译插件了。但这只是方便了一点,我们还希望在仿真过程中动态地控制传感器。

在Gazebo的仿真环境中,也有类似ROS的消息发布和订阅模式的消息传递方法。我们可以在插件中订阅一个主题,它描述了传感器的速度。 我们需要修改速度时,就发布一个该主题的消息,插件接收到消息后就会做出相应的动作。这是一种很方便的通信控制方式。

首先我们定义一个函数用来修改传感器的实际速度:

        // 设定Velodyne的转速
        // 输入参数_vel定义了控制的目标转速
        void SetVelocity(const double &_vel)
        {
            this->model->GetJointController()->SetVelocityTarget(this->joint->GetScopedName(), _vel);
        }
接下来我们要实现一个消息传递机制,先定义两个私有变量分别记录新建的节点和订阅器:
        private:
            transport::NodePtr node;
            transport::SubscriberPtr sub;
然后在Load()函数中实例化这两个对象:
        this->node = transport::NodePtr(new transport::Node());
        this->node->Init(this->model->GetWorld()->GetName());
        // 订阅主题名字
        std::string topicName = "~/" + this->model->GetName() + "/vel_cmd";
        // 订阅主题,并注册回调函数OnMsg
        this->sub = this->node->Subscribe(topicName, &VelodynePlugin::OnMsg, this);
我们在回调函数中解析接收到的消息,将其中的速度数据作为参数调用SetVelocity()。
        // 处理接收的消息 
        // 输入参数_msg记录了接收到的消息内容,是一个Vector3的消息,这里只用到其中的x分量。
        private: void OnMsg(ConstVector3dPtr &_msg)
        {
            this->SetVelocity(_msg->x());
        }
此外我们还要包含两个头文件:
        #include <gazebo/transport/transport.hh>
        #include <gazebo/msgs/msgs.hh>
至此,我们对插件的修改就告一段落了,修改后的C++代码可以从这里下载。但是编译运行之后看不出有什么作用, 我们还需要实现一个消息的发布器,以后每次需要修改速度的时候就通过这个发布器发送一条消息。为此,我们再创建一个C++的程序vel.cc, 读者可以从这里下载,这里不再列出了,其中的主要内容就是通过Gazebo的API发布一个"~/my_velodyne/vel_cmd"的主题, 这也正是我们模型插件订阅的主题。 接下来,我们对CMakeLists.txt做出一些简单的修改,添加编译vel.cc的规则:
        # Build the stand-alone test program
        add_executable(vel vel.cc)
        
        if (${gazebo_VERSION_MAJOR} LESS 6)
          include(FindBoost)
          find_package(Boost ${MIN_BOOST_VERSION} REQUIRED system filesystem regex)
          target_link_libraries(vel ${GAZEBO_LIBRARIES} ${Boost_LIBRARIES})
        else()
          target_link_libraries(vel ${GAZEBO_LIBRARIES})
        endif()
编译之后运行仿真测试:
        $ cd build
        $ cmake ..
        $ make
        $ gazebo ../velodyne.world
我们再打开一个终端,切换到一开始的工作空间下的build子目录中,运行vel控制传感器的参数。
        $ cd ~/catking_ws/src/learning_gazebo/velodyne_plugin/build
        $ ./vel 2
我们可以从Gazebo的仿真环境中看到传感器以一个比较慢的速度转动,我们也可以尝试修改运行vel时的参数,可以看到传感器以不同的速度转动。

3. 通过ROS接口控制传感器

Gazebo的一个优势就是可以与ROS方便的连接,使得我们可以在真实的世界与模拟的世界之间自如的切换。 下面我们继续丰富激光传感器的插件功能,通过ROS来调整传感器的扫描速度。

首先,我们修改velodyne_plugin.cc文件(修改之后的文件可以从这里下载。)添加一些头文件用于访问ROS的接口, 如下左部分中的代码所示。接着在类VelodynePlugin中添加几个私有的成员变量, rosNode用于在ROS中注册一个节点,唯一地标识激光传感器插件。rosSub为一个ROS订阅器,用于订阅控制指令。 rosQueue和rosQueueThread分别是一个ROS回调函数队列和回调函数处理线程。看教程的意思是,没接收到一个消息都会创建一个线程处理。

        #include <thread>
        #include "ros/ros.h"
        #include "ros/callback_queue.h"
        #include "ros/subscribe_options.h"
        #include "std_msgs/Float32.h"
        private:
            std::unique_ptr<ros::NodeHandle> rosNode;
            ros::Subscriber rosSub;
            ros::CallbackQueue rosQueue;
            std::thread rosQueueThread;

接着,需要在函数Load()的最后添加一些ROS系统的初始化和注册的语句。主要是创建注册ROS节点,订阅ROS主题,同时注册两个回调函数。 OnRosMsg()用于响应接收消息,QueueThread则是一个ROS队列辅助线程的函数。

        // 如果没有初始化ROS系统,则初始化.
        if (!ros::isInitialized()) {
            int argc = 0;
            char **argv = NULL;
            ros::init(argc, argv, "gazebo_client", ros::init_options::NoSigintHandler);
        }
        // 创建并注册ROS节点
        this->rosNode.reset(new ros::NodeHandle("gazebo_client"));
        
        // 订阅一个ROS主题.
        ros::SubscribeOptions so = ros::SubscribeOptions::create<std_msgs::Float32>(
              "/" + this->model->GetName() + "/vel_cmd",            // 主题名称
              1,
              boost::bind(&VelodynePlugin::OnRosMsg, this, _1),     // 回调函数
              ros::VoidPtr(), &this->rosQueue);
        // 订阅主题
        this->rosSub = this->rosNode->subscribe(so);
        // ROS队列辅助线程
        this->rosQueueThread = std::thread(std::bind(&VelodynePlugin::QueueThread, this));

然后,为类VelodynePlugin实现OnRosMsg()和QueueThread()两个回调函数:

        // 处理ROS传来的消息
        // _msg: 一个float数据,用于设定传感器转动速度
        public: void OnRosMsg(const std_msgs::Float32ConstPtr &_msg)
        {
            this->SetVelocity(_msg->data);
        }
        
        // ROS处理消息的辅助函数
        private: void QueueThread()
        {
            static const double timeout = 0.01;
            while (this->rosNode->ok())
                this->rosQueue.callAvailable(ros::WallDuration(timeout));
        }

最后,我们需要对CMakeLists.txt做一些修改(修改之后的文件可以从这里下载), 添加对ROS的访问。在CMakeLists.txt的开始添加roscpp和std_msgs两个包的依赖关系和搜索路径:

        find_package(roscpp REQUIRED)
        find_package(std_msgs REQUIRED)
        include_directories(${roscpp_INCLUDE_DIRS})
        include_directories(${std_msgs_INCLUDE_DIRS})
为velodyne_plugin添加链接库:
        target_link_libraries(velodyne_plugin ${GAZEBO_libraries} ${roscpp_LIBRARIES})

重新编译:

        $ mkdir build
        $ cd build
        $ cmake ..
        $ make

打开一个新的终端,以后台的形式运行roscore,前台的形式运行gazebo加载velodyne.world

        $ export LD_LIBRARY_PATH=${LD_LIBRARY_PATH}:`pwd`
        $ roscore&
        $ gazebo ../velodyne.world
再打开一个终端,通过rostopic的形式发布"/my_velodyne/vel_cmd"主题的消息,设置传感器转动速度,读者可以自行修改最后的参数调整转速。
        rostopic pub /my_velodyne/vel_cmd std_msgs/Float32 10.0

4. 总结

在本文中,我们先介绍了创建、编译、插入、运行一个插件的全过程。然后利用Gazebo提供的各种API实现了比较灵活的控制激光传感器的转动速度。 最后介绍了Gazebo与ROS系统之间的一种交互方式。




Copyright @ 高乙超. All Rights Reserved. 京ICP备16033081号-1