【ROS】2.2 信息、类和服务器

2.2.1 自定义消息

(1)定义一条自定义消息

  在~/ros_ws/src目录下利用以下命令创建包:

cs_create_pkg example_ros_msg roscpp std_msgs

​  在~/ros_ws/src/example_ros_msg目录下创建一个msg子目录,该子目录下创建一个ExampleMessage.msg文件,其内容填入:

Header header
int32 demo_int
float64 demo_double

  编辑~/ros_ws/src/example_ros_msg目录下的package.xml,将以下两行取消注释(使得通知编译器知道需要生成新的消息头):

 <build_depend>message_generation</build_depend>
 <run_depend>message_runtime</run_depend> 

  编辑~/ros_ws/src/example_ros_msg目录下的CMakeLists.txt

cmake_minimum_required(VERSION 2.8.3)
project(example_ros_msg)

find_package(catkin_simple REQUIRED)

catkin_simple()

# Executables
# cs_add_executable(example_ros_message_publisher src/example_ros_message_publisher.cpp)

cs_install()
cs_export()

  在~/ros_ws目录下运行终端,执行以下命令进行编译(生成适合的C++文件包含相应的头文件):

catkin_make

  该文件存放在~/ros_ws/devel/include/example_ros_msg/ExampleMessage.h下。如果想要使用此新消息类型的节点源代码应依赖于程序包example_ros_msg,并且使用该消息类型时,在节点的C++源代码中应该包含以下头文件:

#include <example_ros_msg/ExampleMessage.h>

  在~/ros_ws/src/example_ros_msg目录下创建一个使用自定义消息类型的发布器程序example_ros_message_publisher.cpp

// 使用自定义消息类型的发布器:example_ros_message_publisher.cpp
#include <ros/ros.h>
#include <example_ros_msg/ExampleMessage.h>    // 导入含有自定义消息类型的头文件
#include <math.h>

int main(int argc, char **argv) {
    ros::init(argc, argv, "example_ros_message_publisher"); // 节点名
    ros::NodeHandle n;    // 建立网络通信

	// 初始化话题example_topic,将消息数据发布到话题
    ros::Publisher my_publisher_object = n.advertise<example_ros_msg::ExampleMessage>("example_topic", 1);
    
	// 实例化自定义消息类型
    example_ros_msg::ExampleMessage  my_new_message;
   
    ros::Rate naptime(1.0);   
	
	// 填充自定义消息数据的各个字段
    my_new_message.header.stamp = ros::Time::now();   
    my_new_message.header.seq=0; 
    my_new_message.header.frame_id = "base_frame"; 
    my_new_message.demo_int= 1;
    my_new_message.demo_double=100.0;
    
    double sqrt_arg;
    while (ros::ok()) 
    {
        my_new_message.header.seq++;      // 递增序列计数器
        my_new_message.header.stamp = ros::Time::now();   // 更新时间戳
	    // 具体的数据变化:不再做注释
        my_new_message.demo_int*=2.0;  
        sqrt_arg = my_new_message.demo_double;
        my_new_message.demo_double = sqrt(sqrt_arg);
        
		// 发布器发布数据
        my_publisher_object.publish(my_new_message);
		naptime.sleep(); 
    }
}

  取消CMakeList.txt文件中以下注释:

cs_add_executable(example_ros_message_publisher src/example_ros_message_publisher.cpp)

  编译后运行(此时应该保证ROS系统在运行中):

catkin_make
rosrun example_ros_msg example_ros_message_publisher

  开启另外一个终端查看运行情况:

rostopic echo example_topic

显示结果如下:

header: 
  seq: 18
  stamp: 
    secs: 1658328650
    nsecs: 275196150
  frame_id: "base_frame"
demo_int: 524288
demo_double: 1.0000087837
---
header: 
  seq: 19
  stamp: 
    secs: 1658328651
    nsecs: 275239555
  frame_id: "base_frame"
demo_int: 1048576
demo_double: 1.00000439184
---

(2)定义一条变长的信息

  在~/ros_ws/src目录下利用以下命令创建包:

cs_create_pkg custom_msgs roscpp std_msgs

  在~/ros_ws/src/custom_msgs目录下创建一个msg子目录,该子目录下创建一个VecOfDoubles.msg文件,其内容填入:

float64[] dbl_vec

  编辑~/ros_ws/src/custom_msgs目录下的package.xml,将以下两行取消注释(使得通知编译器知道需要生成新的消息头):

 <build_depend>message_generation</build_depend>
 <run_depend>message_runtime</run_depend> 

  在~/ros_ws目录下运行终端,执行以下命令进行编译(生成适合的C++文件包含相应的头文件):

catkin_make

  在~/ros_ws/src/example_ros_msg目录下(注意此处目录)创建一个使用该变长消息类型的发布器程序vector_publisher.cpp

// 使用变长消息类型的发布器程序:vector_publisher.cpp
#include <ros/ros.h>
#include <custom_msgs/VecOfDoubles.h>   // 导入新消息类型头文件

int main(int argc, char **argv) {
    ros::init(argc, argv, "vector_publisher");    // 节点名 
    ros::NodeHandle n;     // 建立网络通信
	// 初始化发布器,将会把数据发到话题vec_topic上
    ros::Publisher my_publisher_object = n.advertise<custom_msgs::VecOfDoubles>("vec_topic", 1);
    
    custom_msgs::VecOfDoubles vec_msg;   // 实例化新消息类型
    double counter=0; 
    ros::Rate naptime(1.0); 
   
    vec_msg.dbl_vec.resize(3);   // 修改消息长度
    // 赋值
    vec_msg.dbl_vec[0]=1.414;    
    vec_msg.dbl_vec[1]=2.71828;    
    vec_msg.dbl_vec[2]=3.1416;
    
    vec_msg.dbl_vec.push_back(counter);    // 插入新数据 
    while(ros::ok())  {
    counter+=1.0;
    vec_msg.dbl_vec.push_back(counter);    // 插入新数据 
    my_publisher_object.publish(vec_msg);  // 发布数据
    naptime.sleep(); 
    }
}   

  为了能够在example_ros_msg中编译新节点,需要对该包中的package.xmlCMakeLists.txt进行编辑:

# package.xml文件插入以下行:
<build_depend>custom_msgs</build_depend>
<run_depend>custom_msgs</run_depend>
# CMakeLists.txt文件插入以下行:
cs_add_executable(vector_publisher src/vector_publisher.cpp)

  编译节点后运行节点:

catkin_make
rosrun example_ros_msg vector_publisher

  观察结果:

rostopic echo vec_topic

显示结果如下:

dbl_vec: [1.414, 2.71828, 3.1416, 0.0, 1.0, 2.0]
---
dbl_vec: [1.414, 2.71828, 3.1416, 0.0, 1.0, 2.0, 3.0]
---
dbl_vec: [1.414, 2.71828, 3.1416, 0.0, 1.0, 2.0, 3.0, 4.0]
---

  在~/ros_ws/src/example_ros_msg目录下(注意此处目录)创建一个使用该变长消息类型的订阅器程序vector_subscriber.cpp

// 使用该变长消息类型的订阅器程序:vector_subscriber.cpp
#include<ros/ros.h> 
#include <custom_msgs/VecOfDoubles.h>  
void myCallback(const custom_msgs::VecOfDoubles& message_holder)  // 回调函数
{ 
  std::vector <double> vec_of_doubles = message_holder.dbl_vec;   // 将订阅数据存放到vector容器内
  int nvals = vec_of_doubles.size();   // 记录容器大小
  for (int i=0;i<nvals;i++) { 
    ROS_INFO("vec[%d] = %f",i,vec_of_doubles[i]); //print out all the values 
  }
  ROS_INFO("\n");
} 

int main(int argc, char **argv) 
{ 
  ros::init(argc,argv,"vector_subscriber");   // 节点名
  ros::NodeHandle n;   // 建立网络通信
  
  ros::Subscriber my_subscriber_object= n.subscribe("vec_topic",1,myCallback); 

  ros::spin(); 
  return 0; 
} 

  同样,为了编译节点,将以下语句插入CMakeLists.txt文件中:

cs_add_executable(vector_subscriber src/vector_subscriber.cpp)

  利用catkin_make编译。

  打开终端,运行ROS系统:

roscore

  打开另外一个终端,运行发布器:

rosrun example_ros_msg vector_publisher

  打开另外一个终端,运行订阅器:

rosrun example_ros_msg vector_subscriber

显示结果如下:

[ INFO] [1658384328.515807550]: vec[0] = 1.414000
[ INFO] [1658384328.515938266]: vec[1] = 2.718280
[ INFO] [1658384328.515975837]: vec[2] = 3.141600
[ INFO] [1658384328.516006054]: vec[3] = 0.000000
[ INFO] [1658384328.516034508]: vec[4] = 1.000000
[ INFO] [1658384328.516060837]: vec[5] = 2.000000
[ INFO] [1658384328.516087448]: 

[ INFO] [1658384329.515928234]: vec[0] = 1.414000
[ INFO] [1658384329.516032330]: vec[1] = 2.718280
[ INFO] [1658384329.516074560]: vec[2] = 3.141600
[ INFO] [1658384329.516103575]: vec[3] = 0.000000
[ INFO] [1658384329.516131998]: vec[4] = 1.000000
[ INFO] [1658384329.516158618]: vec[5] = 2.000000
[ INFO] [1658384329.516184537]: vec[6] = 3.000000
[ INFO] [1658384329.516210657]: 

注:使用这种消息长度可变的数据类型时需要注意以下几点:
(1)确保变长向量不会过大,否则可能会消耗所有内存和通信带宽。
(2)与数组一样,如果访问未分配的内存时,将会导致运行错误(但是编译器并不会报错)。

2.2.2 ROS服务

  目前,前面提到的通信模式中,发布器并不清楚谁是订阅器,而订阅器也不知道是哪个发布器向该话题发布数据。这种通信方式适用于重复的消息,如传感器数值的发布。传感器发布器不需要知道哪些订阅器节点订阅话题,发布器也不会接收用户的任何请求来改变自身发布的数据。

  有些情况下,我们需要建立双向、一对一、可靠的通信。客户端向服务器发送请求,服务器向客户端发送响应。

(1)定义服务消息

  定义一个自定义服务消息来描述请求和响应的数据类型和字段名称。(通过一个.srv文件实现)

  在~/ros_ws/src目录下创建一个程序包example_ros_service

cs_create_pkg example_ros_service roscpp std_msgs

  在~/ros_ws/src/example_ros_service目录下创建一个子目录srv,该子目录下创建一个ExampleServiceMsg.srv文件,编辑文件内容,文件内容如下:

string name
---
bool on_the_list
bool good_guy
int32 age
string nickname

  该srv文件中,“—”上面的行定义了请求结构,该请求有单个组件(name)组成,数据类型是一个字符串;“—”下面的行定义了响应结构,该响应有on_the_listgood_guyagenickname四个字段组成,对应的数据类型为boolboolint32string

  与发布器和订阅器自定义消息类型一样,我们需要通知编译器通过package.xml文件生成新的消息头,因此在编译前我们需要编辑修改package.xml文件:

<build_depend>message_generation</build_depend>
<run_depend>message_runtime</run_depend>

  编辑CMakeList.txt文件如下:

cmake_minimum_required(VERSION 2.8.3)
project(example_ros_service)

find_package(catkin_simple REQUIRED)
catkin_simple()

#cs_add_executable(example_ros_service src/example_ros_service.cpp)
#cs_add_executable(example_ros_client src/example_ros_client.cpp)

cs_install()
cs_export()

  在工作空间目录下,编译:

roscd
catkin_make

  编译完成后,目录~ros_ws/devel/include/example_ros_service下生成了一个ExampleServiceMsg.h头文件。同时,该目录下还生成了两个服务头文件:ExampleServiceMsgRequest.hExampleServiceMsgResponse.h

  如果想要在一个节点中使用该服务消息类型,则需要利用以下代码包含相关头文件:

#include <example_ros_service/ExampleServiceMsg.h>

注:如果在另外一个单独的包内调用该服务消息类型,则需要注意两点:
(a)同样,在C++代码中仍需要添加#include <example_ros_service/ExampleServiceMsg.h>导入相关头文件。
(b)该程序包的package.xml文件将不再依赖message_generationmessage_runtime,因为该服务消息类型只需要生成一次,已经在example_ros_service执行;其次该程序包的package.xml文件需要新增对example_ros_service的依赖,即需要加入行<build_depend>example_ros_service</build_depend>

(2)ROS服务节点

  在目录~ros_ws/src/example_ros_service下创建一个服务器程序example_ros_service.cpp,源代码如下:

// 服务器程序:example_ros_service.cpp
#include <ros/ros.h>
#include <example_ros_service/ExampleServiceMsg.h>   // 服务消息类型文件头
#include <iostream>
#include <string>
using namespace std;

bool callback(example_ros_service::ExampleServiceMsgRequest& request, example_ros_service::ExampleServiceMsgResponse& response)
{
    ROS_INFO("callback activated");
    string in_name(request.name); 
    response.on_the_list=false;
	
	// 简单的服务器响应
    if (in_name.compare("Bob")==0)
 {
        ROS_INFO("asked about Bob");
        response.age = 32;
        response.good_guy=false;
        response.on_the_list=true;
        response.nickname="BobTheTerrible";
    } 
     if (in_name.compare("Ted")==0)
 {
        ROS_INFO("asked about Ted");
        response.age = 21;
        response.good_guy=true;
        response.on_the_list=true;
        response.nickname="Ted the Benevolent";
    }    
  return true;
}

int main(int argc, char **argv)
{
	ros::init(argc, argv, "example_ros_service");  // 节点名
	ros::NodeHandle n;   // 建立通信
	
	// 建立一个服务器响应,等待交互
	ros::ServiceServer service = n.advertiseService("lookup_by_name", callback);
	ROS_INFO("Ready to look up names.");
	ros::spin();

  return 0;
}

(3)与ROS服务器手动交互

  编译服务器程序example_ros_service.cpp,运行该程序:

catkin_make
rosrun example_ros_service example_ros_service

  打开另外一个终端,手动与服务器交互:

rosservice call lookup_by_name 'Ted'

显示结果如下:

on_the_list: True
good_guy: True
age: 21
nickname: "Ted the Benevolent"

(4)ROS客户端

  与上面手动与服务器交互不同,一般可以采用客户端程序与服务器交互。

  在目录~ros_ws/src/example_ros_service下创建一个服务器程序example_ros_client.cpp,源代码如下:

// 客户机程序:example_ros_client.cpp
#include <ros/ros.h>
#include <example_ros_service/ExampleServiceMsg.h>  // 服务消息类型文件头
#include <iostream>
#include <string>
using namespace std;

int main(int argc, char **argv) {
    ros::init(argc, argv, "example_ros_client");  // 节点名
    ros::NodeHandle n;
	
	// 实例化客户client,希望按照example_ros_service::ExampleServiceMsg中定义的方式传达请求和响应,且期望通信对象是名为lookup_by_name的服务
    ros::ServiceClient client = n.serviceClient<example_ros_service::ExampleServiceMsg>("lookup_by_name");
    // 实例化了一个服务拥有请求和响应的对象,包含一个请求字段和一个响应字段。首先将会填充请求消息,见下面代码“srv.request.name = in_name; ”
	example_ros_service::ExampleServiceMsg srv;

    bool found_on_list = false;
    string in_name;
    while (ros::ok()) {
        cout<<endl;
        cout << "enter a name (x to quit): ";
        cin>>in_name;
        if (in_name.compare("x")==0)
            return 0;
        srv.request.name = in_name; 
        if (client.call(srv)) {   // client.call(srv)判断是否调用成功,如果调用成功,srv.response将会是服务器的响应,该响应有定义好的四个可用字段。
            if (srv.response.on_the_list) {
                cout << srv.request.name << " is known as " << srv.response.nickname << endl;
                cout << "He is " << srv.response.age << " years old" << endl;
                if (srv.response.good_guy)
                    cout << "He is reported to be a good guy" << endl;
                else
                    cout << "Avoid him; he is not a good guy" << endl;
            } else {
                cout << srv.request.name << " is not in my database" << endl;
            }
        } else {
            ROS_ERROR("Failed to call service lookup_by_name");
            return 1;
        }
    }
    return 0;
}

(4)运行服务器和客户端

  保证ROS系统运行中,在另外一个终端运行服务器:

rosrun example_ros_service example_ros_service

  打开另外一个终端运行客户端:

rosrun example_ros_service example_ros_client

显示结果如下:

enter a name (x to quit): Ted
Ted is known as Ted the Benevolent
He is 21 years old
He is reported to be a good guy

enter a name (x to quit): Hny
Hny is not in my database

enter a name (x to quit): Bob
Bob is known as BobTheTerrible
He is 32 years old
Avoid him; he is not a good guy

注:在运行前,要修改CMakeLists.txt文件并且编译。后续代码均是如此,写完代码后要保证编辑CMakeLists.txt并且编译。

2.2.3 ROS中使用C++

  C++类的用法可以使用到ROS中,以提高代码效率和代码复用。

  在目录~ros_ws/src下创建包example_ros_class

cs_create_pkg example_ros_class roscpp std_msgs

(1)在头文件中定义类

  类所有成员的原型、私有和公共数据成员、类构造函数的原型。

// 类的头文件:example_ros_class.h
#ifndef EXAMPLE_ROS_CLASS_H_
#define EXAMPLE_ROS_CLASS_H_

#include <math.h>
#include <stdlib.h>
#include <string>
#include <vector>

#include <ros/ros.h> 

// 调用使用到的消息数据类型的头文件
#include <std_msgs/Bool.h> 
#include <std_msgs/Float32.h>
#include <std_srvs/Trigger.h> 

// 定义类ExampleRosClass,包含成员函数和数据成员
class ExampleRosClass
{
public:
    ExampleRosClass(ros::NodeHandle* nodehandle);  // 构造函数
private:  // 私有变量后带下划线_,便于阅读代码
    ros::NodeHandle nh_;    // 联系主函数和构造函数
	// 订阅器、服务器和发布器
    ros::Subscriber minimal_subscriber_; 
    ros::ServiceServer minimal_service_;
    ros::Publisher  minimal_publisher_;
    
    double val_from_subscriber_; 
    double val_to_remember_; 

    void initializeSubscribers(); 
    void initializePublishers();
    void initializeServices();
    
    void subscriberCallback(const std_msgs::Float32& message_holder);
    bool serviceCallback(std_srvs::TriggerRequest& request, std_srvs::TriggerResponse& response);
}; 
#endif 

(2)编写一个单独的实现文件

  包含上面定义的头文件、包含已声明成员函数的工作代码、包含在构造函数中封装必要初始化的代码。

// 类的实现:example_ros_class.cpp:
#include "example_ros_class.h"  // 导入声明好的类头文件

// 类的构造函数
ExampleRosClass::ExampleRosClass(ros::NodeHandle* nodehandle):nh_(*nodehandle) //直接赋值,将传入的参数nodehandle复制到私有变量nh_中
{
    ROS_INFO("in class constructor of ExampleRosClass");
	// 初始化订阅器、发布器和服务
    initializeSubscribers(); 
    initializePublishers();
    initializeServices();
    // 初始化成员变量
    val_to_remember_=0.0;  
}

void ExampleRosClass::initializeSubscribers()
{
    ROS_INFO("Initializing Subscribers");
    minimal_subscriber_ = nh_.subscribe("example_class_input_topic", 1, &ExampleRosClass::subscriberCallback,this);   // this 关键字作为当前实例
}
void ExampleRosClass::initializeServices()
{
    ROS_INFO("Initializing Services");
    minimal_service_ = nh_.advertiseService("example_minimal_service",&ExampleRosClass::serviceCallback,this);  
}
void ExampleRosClass::initializePublishers()
{
    ROS_INFO("Initializing Publishers");
    minimal_publisher_ = nh_.advertise<std_msgs::Float32>("example_class_output_topic", 1, true); 
}


void ExampleRosClass::subscriberCallback(const std_msgs::Float32& message_holder) {
    val_from_subscriber_ = message_holder.data;   // 保存数据到私有变量上
    ROS_INFO("myCallback activated: received value %f",val_from_subscriber_);
    std_msgs::Float32 output_msg;
    val_to_remember_ += val_from_subscriber_; 
    output_msg.data= val_to_remember_;
    minimal_publisher_.publish(output_msg); // 发布数据
}

bool ExampleRosClass::serviceCallback(std_srvs::TriggerRequest& request, std_srvs::TriggerResponse& response) {
    ROS_INFO("service callback activated");
    response.success = true; 
    response.message = "here is a response string";
    return true;
}

int main(int argc, char** argv) 
{
    ros::init(argc, argv, "exampleRosClass");   // 节点名

    ros::NodeHandle nh; 

    ROS_INFO("main: instantiating an object of type ExampleRosClass");
    ExampleRosClass exampleRosClass(&nh);  // 通过nh联系
    ROS_INFO("main: going into spin; let the callbacks do all the work");
    ros::spin();
    return 0;
} 

  在订阅器的回调函数中,当前的订阅数据会保留在类中的私有变量val_from_subscriber_中,而val_to_remember_则会保存订阅话题的每一次数据,这些变量在类中相当于全局变量。

  在构造函数中,执行订阅器和服务器时,在提供回调函数的同时需要提供一个指针。该关键字this(指针)告诉编译器正在引用此类的当前示例。

  在与服务相关的回调函数使用了一个已经定义好的服务消息:

bool ExampleRosClass::serviceCallback(std_srvs::TriggerRequest& request, std_srvs::TriggerResponse& response) 

  该消息类型Triggerstd_srvs中定义。因此,在编译前,需要在package.xml文件中增加对std_srvs的依赖。

  保持ROS系统运行,运行example_ros_class

rosrun example_ros_class example_ros_class

运行结果如下:

[ INFO] [1658474040.360780286]: main: instantiating an object of type ExampleRosClass
[ INFO] [1658474040.360880034]: in class constructor of ExampleRosClass
[ INFO] [1658474040.360908378]: Initializing Subscribers
[ INFO] [1658474040.362983130]: Initializing Publishers
[ INFO] [1658474040.363427029]: Initializing Services
[ INFO] [1658474040.363910102]: main: going into spin; let the callbacks do all the work

  打开另外一个终端,检查服务对应话题状态:

rosservice call example_minimal_service

该终端显示结果:

success: True
message: "here is a response string"

运行example_ros_class的终端显示结果:

[ INFO] [1658474296.879997406]: service callback activated

  检查订阅器状态:

rostopic pub -r 2  example_class_input_topic std_msgs/Float32 10.0

运行example_ros_class的终端显示结果:

[ INFO] [1658474645.347071052]: myCallback activated: received value 10.000000
[ INFO] [1658474645.847343847]: myCallback activated: received value 10.000000
[ INFO] [1658474646.346482449]: myCallback activated: received value 10.000000
[ INFO] [1658474646.847432733]: myCallback activated: received value 10.000000
[ INFO] [1658474647.347156673]: myCallback activated: received value 10.000000
[ INFO] [1658474647.847809435]: myCallback activated: received value 10.000000

2.2.4 ROS中创建库模块

  源代码变长则需要我们将其分解为较小的模块。为了实现对模块的复用,可以创建一个库。

​  在目录~ros_ws/src下创建包creating_a_ros_library

cs_create_pkg creating_a_ros_library roscpp std_msgs std_srvs

  将example_ros_class.cpp文件拷贝到包creating_a_ros_library的子目录src下,并且删除main函数和修改头文件的引用:

#include <creating_a_ros_library/example_ros_class.h>  // 导入声明好的类头文件

  类原型的头文件example_ros_class.h并不是放在include目录下,而需要在include目录下新建一个与程序包(creating_a_ros_library)同名的子目录,类原型的头文件放在该目录下。

​  编辑CMakeLists.txt文件,增加以下行:

cs_add_library(example_ros_library src/example_ros_class.cpp) 

  在工作空间进行编译:

roscd
catkin_make

   编译完成后,目录~ros_ws/devel/lib下出现文件libexample_ros_library.so

​  在目录~ros_ws/src/creating_a_ros_library/src下创建一个测试程序,其内容如下:

#include <creating_a_ros_library/example_ros_class.h>

int main(int argc, char** argv) 
{
    ros::init(argc, argv, "example_lib_test_main"); // 节点名

    ros::NodeHandle nh; 

    ROS_INFO("main: instantiating an object of type ExampleRosClass");
    ExampleRosClass exampleRosClass(&nh);  

    ROS_INFO("main: going into spin; let the callbacks do all the work");
    ros::spin();
    return 0;
} 

​  编辑CMakeLists.txt文件,插入以下行:

cs_add_executable(ros_library_test_main src/example_ros_class_test_main.cpp)
target_link_library(ros_library_test_main example_ros_library)

​  测试节点:

rosrun creating_a_ros_library ros_library_test_main

显示结果如下:

[ INFO] [1658485905.853814086]: main: instantiating an object of type ExampleRosClass
[ INFO] [1658485905.853958655]: in class constructor of ExampleRosClass
[ INFO] [1658485905.853995754]: Initializing Subscribers
[ INFO] [1658485905.856053623]: Initializing Publishers
[ INFO] [1658485905.856536291]: Initializing Services
[ INFO] [1658485905.857084721]: main: going into spin; let the callbacks do all the work
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值