Do you really know how to correctly use the dynamic parameter adjustment mechanism in ROS?

Principle of dynamically adjusting parameters in ROS

We have a requirement in reality: we hope to change the parameters in the parameter server for debugging the robot, and we don't want to restart the relevant nodes every time we change the parameters, which is time-consuming, labor-consuming and troublesome. What we expect is that we can not only change the parameters in the parameter server for debugging, but also make the data in our refreshed parameter server work immediately without starting again.

In ROS, when the parameters in the parameter server are changed, the communication parties cannot know, because we can find from the working principle of the parameter server (take the topic communication architecture as an example):

Focusing on the data returned from the parameter server, we will find the following disadvantages of the communication mode of the parameter server:

1. The communication between parameter server and client / server is asynchronous without real-time interaction;

2. If the client / server wants to obtain the parameters in the parameter server, it must send a request to the parameter server;

3. The client / server will actively read / refresh the data in the parameter server only when the node is started;

The above three points make: in the process of C/S communication, the parameters in the parameter server have changed, and the communication parties will not be actively informed by the parameter server that "the parameters in the parameter server are refreshed, and each communication node will read it as soon as possible".

Since the parameter server cannot actively inform each node that the parameters have been refreshed, we use the characteristics of C/S Communication (because the refresh of the parameter server will not continue all the time) to add an additional communication node as the initiator of parameter refresh and the informer of "parameter refreshed" information. The specific communication logic is as follows:

The focus of dynamic reconfiguration is to provide a standard method to expose a subset of node parameters (the part of parameters you want to debug dynamically) to external reconfiguration. Using the client program, such as GUI, you can query a set of dynamically configurable parameters from the node, including their name, type and range, and provide a user-defined interface to the user.

Implementation of dynamic configuration C/S

File structure of the project:

① implementation of dynamic configuration client

In the architecture of dynamically configuring C/S service communication, the client is based on XXX Cfg file implementation, in fact, the essence of this file is in python py script file, the contents of which are as follows:

# import package  
from dynamic_reconfigure.parameter_generator_catkin import *  
# create parameter generator  
gen = ParameterGenerator()  
# add parameters into parameter generator  
gen.add("int_param",int_t,0,"int type",0,0,100)  
gen.add("double_param",double_t,1,"double type",1.57,0,3.14)  
gen.add("bool_param",bool_t,2,"bool type",True)  
measure = gen.enum([gen.const("small",int_t,0,"small"),  
        gen.const("medium",int_t,1,"medium"),  
        gen.const("big",int_t,2,"big")],"choice")  
gen.add("list_param",int_t,3,"alternative choices",0,0,2,edit_method=measure)  
  
# generate intermediate file  
exit(gen.generate("demo01","config","dr1"))  

If you create dr1.0 in my project file cfg file will write the above code without prompt, anyway cfg files and The syntax of the py script file is the same. We can first add DR1 cfg renamed DR1 py, so that there is a content prompt, and it will be much easier to write the program. After writing, we will DR1 py renamed DR1 cfg (if the. py suffix is not restored, the compilation will fail). The following steps are mainly followed in writing the client:

1. Import function library:

# import package  
from dynamic_reconfigure.parameter_generator_catkin import *

*The meaning of dynamic_ reconfigure. parameter_ generator_ All functions in the catkin library are imported into this file.

2. Create parameter generator:

# create parameter generator  
gen = ParameterGenerator()  

3. Add parameters to the parameter generator:

# add parameters into parameter generator  
gen.add("int_param",int_t,1,"int type",0,0,100)  // level=1
gen.add("double_param",double_t,2,"double type",1.57,0,3.14)  // level=2
gen.add("bool_param",bool_t,3,"bool type",True)  // level=3
measure = gen.enum([gen.const("small",int_t,0,"small"),  
        gen.const("medium",int_t,1,"medium"),  
        gen.const("big",int_t,2,"big")],"choice")  
gen.add("list_param",int_t,4,"alternative choices",0,0,2,edit_method=measure)  // level=4

The parameter generator can add int, double, bool, enum and string parameters.

1) The prototype of the add function parameter is as follows: add(name, paramtype, level, description, default=None, min=None, max=None, edit_method = ""), where

Name: parameter name;

Paramtype: four types of parameter types (int_t, str_t, bool_t, double_t);

Level: parameter mask is actually an integer constant, which indicates which parameter is modified or not, which is equivalent to the numerical number of the parameter;

Description: string type, used to describe the details of parameters;

Default: default value;

Min/max: maximum / minimum;

edit_method: used to receive the handle returned by enum function;

2) The prototype parameters of const function are as follows: const(name, type, value, description)

Name: parameter name;

Type: four types of parameter types (int_t, str_t, bool_t, double_t);

Value: value of constant value parameter;

Description: string type, used to describe the details of parameters;

3) The prototype parameters of enum function are as follows: enum(constants, description)

Constants: array of constants [...];

Description: string type, used to describe the details of parameters;

How to write different types of enum s:

//Enumeration of string types
str_choice = gen.enum([gen.const("small",str_t,"A","small"),  
        gen.const("medium",str_t,"B","medium"),  
        gen.const("big",str_t,"C","big")],"str_choice")  
gen.add("list_param",str_t,0,"alternative choices","A",edit_method=str_choice)  
//Enumeration of integer types
int_choice = gen.enum([gen.const("small",int_t,0,"small"),  
        gen.const("medium",int_t,1,"medium"),  
        gen.const("big",int_t,2,"big")],"int_choice")  
gen.add("list_param",int_t,0,"alternative choices",0,0,2,edit_method=int_choice)  
//Enumeration of floating point types
double_choice = gen.enum([gen.const("small",double_t,0.0,"small"),  
        gen.const("medium",double_t,1.0,"medium"),  
        gen.const("big",double_t,2.0,"big")],"int_choice")  
gen.add("list_param",double_t,0,"alternative choices",0.0,0.0,2.0,edit_method=double_choice)  
//Enumeration of bool type
bool_choice = gen.enum([gen.const("small",bool_t,true,"small"),  
        gen.const("medium",bool_t,false,"medium")],"int_choice")  
gen.add("list_param",bool_t,0,"alternative choices",true,edit_method=bool_choice)  

Among them, the string/bool type does not need to specify min/max. When you're done After the cfg file, be sure to open the executable permissions for python script files in the following Linux systems. Only based on the script executable permissions can the compilation pass and the GUI be generated:

The chmod command is used to modify the executable permissions of files. The executable permissions are divided into the following three categories:

Let's right-click XXX Cfg file, click "open in integration terminal", then enter chmod +x XXX.cfg, and then use l -sl command to view the executable permissions of XXX.cfg file:

The first character represents the file type. d stands for directory, - stands for non directory. Next, every three characters are a group of permissions, which are divided into three groups, representing owner permissions, user permissions in the same group and other user permissions in turn. The three characters of each group of permissions in turn represent whether they are readable, writable and executable:

R: Indicates that you have read permission

W: Indicates that you have write permission

10: Indicates that you have executable permissions

-: indicates that you do not have this permission

Let's look at the second to fourth letters in the first line: rwx - the file is readable, writable and executable. For the meanings of all parameters of chmod command, see: Linux chmod command | rookie tutorial (runoob.com)https://www.runoob.com/linux/linux-comm-chmod.html

4) Exit and generate intermediate files

exit(gen.generate(package_name, "dynamic_server_name", "cfg_name"))

package_name indicates that the name is package_name's feature pack, dynamic_server_name indicates the node name of the dynamic parameter adjustment server, cfg_name indicates the cfg file name.

Then, the function package is built by importing the following dependencies: roscpp and std_msgs,rospy,dynamic_reconfigure. Package. After importing dependencies The XML code is as follows:

// Information related to dynamic configuration parameters
<buildtool_depend>catkin</buildtool_depend>  
<build_depend>dynamic_reconfigure</build_depend>  
<build_depend>roscpp</build_depend>  
<build_depend>rospy</build_depend>  
<build_depend>std_msgs</build_depend>  
<build_export_depend>dynamic_reconfigure</build_export_depend>  
<build_export_depend>roscpp</build_export_depend>  
<build_export_depend>rospy</build_export_depend>  
<build_export_depend>std_msgs</build_export_depend>  
<exec_depend>dynamic_reconfigure</exec_depend>  
<exec_depend>roscpp</exec_depend>  
<exec_depend>rospy</exec_depend>  
<exec_depend>std_msgs</exec_depend>  

After compilation, configure cmakelist Txt file:

// Configuration information related to dynamic configuration parameters
find_package(catkin REQUIRED COMPONENTS  
  dynamic_reconfigure  
  roscpp  
  rospy  
  std_msgs  
  message_generation  
)  
generate_dynamic_reconfigure_options(  
  cfg/dr1.cfg  
)  
catkin_package(  
#  INCLUDE_DIRS include  
#  LIBRARIES demo01  
 CATKIN_DEPENDS dynamic_reconfigure roscpp rospy std_msgs message_runtime  
#  DEPENDS system_lib  
)  
include_directories(  
# include  
  ${catkin_INCLUDE_DIRS}  
)  
add_executable(config src/config.cpp)  
add_dependencies(config ${PROJECT_NAME}_gencfg ${catkin_EXPORTED_TARGETS})  
target_link_libraries(config  
  ${catkin_LIBRARIES}  
) 

After compilation, the results are as follows:

In fact, the parameter generator essentially packages the parameters of the client into a C + + custom class type, which is our topic type in C/S service communication.

② Implementation of dynamic configuration server

1. Add the required header file

#include "ros/ros.h"  
#include "demo01/dr1Config.h"  
#include "dynamic_reconfigure/server.h" 

demo01/dr1Config.h: . cfg generated user-defined class type file;

dynamic_reconfigure/server.h: Files related to server in dynamic configuration parameters;

2. Node initialization:

ros::init(argc,argv,"dr");  

3. server creation:

dynamic_reconfigure::Server<demo01::dr1Config> server;  

4. Creation of callback function:

// void(ConfigType &, uint32_t level)  
void CallbackFunc(demo01::dr1Config& ConfigType_obj, uint32_t level)  
{  
    ROS_INFO("param:%d,%f,%d,%d\n\t",ConfigType_obj.int_param,  
    ConfigType_obj.double_param,  
    ConfigType_obj.bool_param,  
    ConfigType_obj.list_param);  
    ROS_INFO("which parameter is changed::%d\n\t",level);  
}  

Where Demo01:: dr1config & configtype_ Obj is the topic type; level is the mask of the parameter (32-bit long integer variable), which indicates whether the parameter has been changed or not. It is just a numeric name for the parameter.

5. Setting of callback function:

dynamic_reconfigure::Server<demo01::dr1Config>::CallbackType Callback;  
// boost::bind return a function object  
Callback = boost::bind(&CallbackFunc,_1,_2);  
server.setCallback(Callback);  

The prototype of the callback function object is as follows:

boost::function<void(ConfigType &, uint32_t level)> CallbackType;  

This parameter must be a function object of type boost:: function <... >. In order to convert ordinary function pointers into function objects in the boost library, we should use the boost::bind binding function to convert:

Callback = boost::bind(&CallbackFunc,_1,_2);  

6. Use the roundabout function to callback the callback function:

ros::spin();  

Our running results are as follows:

1. Run the client:

2. Run rosrun rqt_reconfigure rqt_reconfigure:

 3. Dynamically changing parameters:

We can see that when we change different parameters, the level value will also be different, that is, different level values represent different parameters we change. Note: when compiling, we need to add. / vscode/tasks. Replace the JSON code with the following code:

{  
//About} tasks For a document in JSON} format, see
    // https://go.microsoft.com/fwlink/?LinkId=733558  
    "version": "2.0.0",  
    "tasks": [  
        {  
            "label": "catkin_make:debug", //Descriptive information representing the prompt
            "type": "shell",  //You can choose shell or process. If it's a shell, the code runs a command in the shell. If it's a process, it runs as a process
            "command": "catkin_make",//This is the command we need to run
            "args": [],//If you need to add some suffixes after the command, you can write it here, such as - DCATKIN_WHITELIST_PACKAGES=“pac1;pac2”  
            "group": {"kind":"build","isDefault":true},  
            "presentation": {  
                "reveal": "always"//Optional always or silence, which indicates whether to output information
            },  
            "problemMatcher": "$msCompile"  
        }  
    ]  
}  

And compiled cfg/. After the MSG file, in C_ cpp_ properties. In the includepath of the JSON file, add/ The path of devel/include directory is shown in the following figure after adding:

"includePath": [  
  "/opt/ros/noetic/include/**",  
  "/usr/include/**",  
  "/home/rosnetic/test01/devel/include/**" // Change the behavior and add the directory yourself
] 

After adding, you can call the header file transformed from the custom file in the file, and you will not report it wrong.

Topic communication and dynamic parameter configuration

The document structure is as follows:

The cfg/cpp folder is generated during compilation. We don't need to create this folder and its C + + header file.

From the above structure, we can see that node A plays two roles: the server of dynamic configuration parameters and the publisher of topic communication. Therefore, the program should also be divided into two parts. The program design flow chart is as follows:

① Common code part

setlocale(LC_ALL,"");  
ros::init(argc,argv,"dr");  

② Code of dynamic configuration parameters

dynamic_reconfigure::Server<demo01::dr1Config> server;  
dynamic_reconfigure::Server<demo01::dr1Config>::CallbackType Callback;  
// boost::bind return a function object  
Callback = boost::bind(&CallbackFunc,_1,_2);  
server.setCallback(Callback);  
//Callback function for dynamically configuring parameters
void CallbackFunc(demo01::dr1Config& ConfigType_obj, uint32_t level) 
{  
    ROS_INFO("param:%d,%f,%d,%d\n\t",ConfigType_obj.int_param,  
    ConfigType_obj.double_param,  
    ConfigType_obj.bool_param,  
    ConfigType_obj.list_param);  
    ROS_INFO("which parameter is changed::%d\n\t",level+1);  
} 

③ while polling gets the parameters in the parameter server and the subscriber publishing information associated with it

ros::NodeHandle nh;  
ros::Publisher pub = nh.advertise<demo01::sub_msg>("chatter",10,true);  
demo01::sub_msg msg_obj;  
      
ros::Rate rate(1);  
while(ros::ok())  
{  
    // Polling for information from parameter server
    msg_obj.bool_param = nh.param("/dr/bool_param",false);  
    msg_obj.double_param = nh.param("/dr/double_param",0.0);  
    msg_obj.int_param = nh.param("/dr/int_param",0);  
    msg_obj.list_param = nh.param("/dr/list_param",0);  
    pub.publish(msg_obj);  
  
    ros::spinOnce();
    rate.sleep();  
}  

In this way, the publisher can read the information in the parameter server every time before publishing. We should pay special attention to the parameter name in the parameter server. If we are not sure, we can write the code of "dynamic parameter adjustment" and run it. Use rosparam list to check the name of the parameter reported by "dynamic parameter adjustment client" to the parameter server:

We add "/ dr /..." to each parameter, It's too troublesome. We can add a namespace to the node handle to achieve this purpose:

#include "ros/ros.h"
#include "demo01/dr1Config.h"
#include "demo01/sub_msg.h"
#include "dynamic_reconfigure/server.h"

// void(ConfigType &, uint32_t level)
void CallbackFunc(demo01::dr1Config& ConfigType_obj, uint32_t level)
{
    ROS_INFO("param:%d,%f,%d,%d\n\t",ConfigType_obj.int_param,
    ConfigType_obj.double_param,
    ConfigType_obj.bool_param,
    ConfigType_obj.list_param);
    ROS_INFO("which parameter is changed::%d\n\t",level+1);
}

int main(int argc,char* argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"dr");
    dynamic_reconfigure::Server<demo01::dr1Config> server;
    dynamic_reconfigure::Server<demo01::dr1Config>::CallbackType Callback;
    // boost::bind return a function object
    Callback = boost::bind(&CallbackFunc,_1,_2);
    server.setCallback(Callback);

    ros::NodeHandle nh("dr"); // Add dr namespace
    ros::Publisher pub = nh.advertise<demo01::sub_msg>("chatter",10,true);
    demo01::sub_msg msg_obj;
        
    ros::Rate rate(1);
    while(ros::ok())
    {
        msg_obj.bool_param = nh.param("/dr/bool_param",false);
        msg_obj.double_param = nh.param("/dr/double_param",0.0);
        msg_obj.int_param = nh.param("/dr/int_param",0);
        msg_obj.list_param = nh.param("/dr/list_param",0);
        pub.publish(msg_obj);

        ros::spinOnce();
        rate.sleep();
    }
    return 0;
}

However, at this time, the publisher's topic changes to / dr/chatter, while the subscriber's topic is still / chatter. In this way, add the same namespace as the publisher to the subscriber's node handle, so that the topic topic names of the two can remain the same again:

#include "demo01/sub_msg.h"
#include "ros/ros.h"

// void (*fp)(const boost::shared_ptr<const M> &)
void CallbackFunc(const demo01::sub_msg::ConstPtr& ptr)
{
    ROS_INFO("sub_param:%d,%d,%f,%d\n\t",ptr->int_param,ptr->list_param,ptr->double_param,ptr->bool_param);
}

int main(int argc,char* argv[])
{
    setlocale(LC_ALL,"");
    ros::init(argc,argv,"sub");
    ros::NodeHandle nh("dr"); // Add dr namespace
    ros::Subscriber sub = nh.subscribe<demo01::sub_msg>("chatter",10,CallbackFunc);
    ros::spin();
    return 0;
}

CMakelist.txt file to add configuration information:

// Configuration related to topic communication
add_message_files(  
  FILES  
  sub_msg.msg  
)  
generate_messages(  
  DEPENDENCIES  
  std_msgs  
)  
add_executable(sub src/sub.cpp)  
add_dependencies(sub ${PROJECT_NAME}_gencpp ${catkin_EXPORTED_TARGETS})  
target_link_libraries(sub  
  ${catkin_LIBRARIES}  
)  

Package. Add configuration information to XML file:

// Configuration information related to topic communication
<build_depend>message_generation</build_depend>  
<exec_depend>message_runtime</exec_depend>  

The operation results are as follows:

At this point, we can change the information subscribed by subscribers at any time through the GUI.

Keywords: C++ ROS

Added by rgermain on Wed, 26 Jan 2022 16:09:51 +0200