ROS Spinning Up

11 minute read

Published:

drawing


Create Catkin Workspace and Package

Create a catkin workspace if necessary.

mkdir -p ~/example_ws/src && cd ~/example_ws

Export compile commands and specify the include path of the system ROS (melodic distribution).

catkin config --cmake-args -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCXXFLAGS="-isystem /opt/ros/melodic/include"

Create a catkin package.

cd src/
catkin_create_pkg node_example roscpp rospy
cd node_example/

Dynamic Reconfigure

Create cfg folder and cfg/NodeExample.cfg file which contains the following content.

#! /usr/bin/env python

PACKAGE = "node_example"

from dynamic_reconfigure.parameter_generator_catkin import *

gen = ParameterGenerator()

#       Name        Type      Level Description               Default   Min   Max
gen.add("enable",   bool_t,   0,    "Enable node operation",  True               )
gen.add("message",  str_t,    0,    "Message to be sent",     ""                 )
gen.add("a",        int_t,    0,    "First number",           0,        -100, 100)
gen.add("b",        int_t,    0,    "Second number",          0,        -100, 100)

exit(gen.generate(PACKAGE, "node_example", "NodeExample"))

Modify package.xml

Add depend on dynamic_reconfigure in package.xml.

......
<exec_depend>roscpp</exec_depend>
<exec_depend>rospy</exec_depend>

<!-- We have added this line -->
<depend>dynamic_reconfigure</depend>

<!-- The export tag contains other, unspecified, tags -->
<export>
......

Modify CMakeLists.txt

Add dynamic_reconfigure to find_package so that we can use generate_dynamic_reconfigure_options command to generate dynamic reconfigure header file.

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  dynamic_reconfigure
)

Search dynamic_reconfigure in CMakeLists.txt, uncomment the following block and add the NodeExample.cfg file.

# Generate dynamic reconfigure parameters in the 'cfg' folder
generate_dynamic_reconfigure_options(
 cfg/NodeExample.cfg
)

Add dynamic_reconfigure to catkin_package to inform other dependent projects

catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES node_example
  CATKIN_DEPENDS roscpp rospy dynamic_reconfigure
#  DEPENDS system_lib
)

Build

Compile the workspace to get the auto-generated configuration file.

# Press `Ctrl+z` to send `vim` to the background and go back to `~/example_ws`.
# Use `jobs` to check jobs running in the background and `fg` to bring the first job to foreground.
cd ~/example_ws

Install jq — a tool for processing JSON files — if you have not done so yet.

sudo apt install jq

Add the following alias to your ~/.bashrc. This provides a bash command called bui (build it) which first builds the catkin workspace by catkin build and then collects all compile commands from different packages into a single compile_commands.json file via the jq tool. Clangd is a Language Server Protocol which will parse the compile_commands.json file to provide language “smartness” features such as code completion, find references, etc.

# Edit bashrc
vim ~/.bashrc

# Add the following line to ~/.bashrc and save it by typing :wq
# Feel free to change `bui` to your preferred shortcut.
# I chose `bui` because it sounds like `build it`
alias bui="catkin build && jq -s 'map(.[])' build/**/compile_commands.json > compile_commands.json"

# Reload bashrc
source ~/.bashrc

Build catkin workspace and check the generated NodeExampleConfig.h file

ls devel/include/node_example/
# [Output] NodeExampleConfig.h

Custom Message

Create a msg directory and a NodeExampleData.msg file with the following content.

string message
int32 a
int32 b

Modify package.xml

Search message in package.xml and uncomment all related lines.

......
<!-- Use build_depend for packages you need at compile time: -->
  <build_depend>message_generation</build_depend>
<!-- Use build_export_depend for packages you need in order to build against this package: -->
  <build_export_depend>message_generation</build_export_depend>
<!-- Use buildtool_depend for build tool packages: -->
<!--   <buildtool_depend>catkin</buildtool_depend> -->
<!-- Use exec_depend for packages you need at runtime: -->
  <exec_depend>message_runtime</exec_depend>
<!-- Use test_depend for packages you need only for testing: -->
......

We built our custom message on top of std_msgs, so we should also add the corresponding depend tag.

......
<depend>dynamic_reconfigure</depend>
<depend>std_msgs</depend>
......

Modify CMakeLists.txt

Append message_generation and std_msgs to find_package.

find_package(catkin REQUIRED COMPONENTS
  roscpp
  rospy
  dynamic_reconfigure
  message_generation
  std_msgs
)

Uncomment the add_message_files block and specify the file name in msg/ folder.

# Generate messages in the 'msg' folder
add_message_files(
  FILES
  NodeExampleData.msg
)

Note that we must put NodeExampleData.msg rather than msg/NodeExampleData.msg, otherwise, we will get the following error during compilation.

Assertion failed: file
'~/example_ws/src/node_example/msg/msg/NodeExampleData.msg'
does not exist.  Message: message file not found

Uncomment the generate_messages block.

# Generate added messages and services with any dependencies listed here
generate_messages(
  DEPENDENCIES
  std_msgs  # Or other packages containing msgs
)

Add message_runtime and std_msgs to catkin_package.

catkin_package(
#  INCLUDE_DIRS include
#  LIBRARIES node_example
 CATKIN_DEPENDS roscpp rospy dynamic_reconfigure message_runtime std_msgs
#  DEPENDS system_lib
)

Build

Now we are ready to build our custom message. Use bui in ~/example_ws to build the workspace and verify whether NodeExampleData.h is in ./devel/include/node_example.

ls ./devel/include/node_example/
# [Output] NodeExampleConfig.h  NodeExampleData.h

Talker Node

Node File

Create file src/nodes/talker_node.cpp and expand the ros_main snippet in it. We specify the node name talker in ros::init.

drawing

Fo now, clangd has no idea where is the <ros/ros.h> header file because there is no corresponding compile command for the current file talker_node.cpp. Let’s add this file to CMakeLists.txt. Search for add_executable and change add_executable, add_dependencies, and target_link_libraries as follows.

......
# Declare a C++ executable
# With catkin_make all packages are built within a single CMake context
# The recommended prefix ensures that target names across packages don't collide
add_executable(talker src/nodes/talker_node.cpp)

## Rename C++ executable without prefix
## The above recommended prefix causes long target names, the following renames the
## target back to the shorter version for ease of user use
## e.g. "rosrun someones_pkg node" instead of "rosrun someones_pkg someones_pkg_node"
# set_target_properties(${PROJECT_NAME}_node PROPERTIES OUTPUT_NAME node PREFIX "")

# Add cmake target dependencies of the executable
# same as for the library above
add_dependencies(talker ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})

# Specify libraries to link a library or executable target against
target_link_libraries(talker
  ${catkin_LIBRARIES}
)
......

Build the workspace again using bui and reopen src/nodes/talker_node.cpp. We should see that all the error messages disappeared and documentations are available by pressing ctrl+k over function names.

drawing

Remark

If you are curious about what is going on under the hood, search for talker_node in the compile_commands.json file. We can see that the following entry was added after building the workspace via bui. Clangd will search for header files in the directories specified by the -I command and parse the documentations.

{
  "directory": "/home/username/example_ws/build/node_example",
  "command": "/usr/bin/c++  -DROSCONSOLE_BACKEND_LOG4CXX -DROS_BUILD_SHARED_LIBS=1 -DROS_PACKAGE_NAME=\\\"node_example\\\" -I/home/username/example_ws/devel/.private/node_example/include -I/opt/ros/melodic/include -I/opt/ros/melodic/share/xmlrpcpp/cmake/../../../include/xmlrpcpp    -o CMakeFiles/talker.dir/src/nodes/talker_node.cpp.o -c /home/username/example_ws/src/node_example/src/nodes/talker_node.cpp",
  "file": "/home/user/example_ws/src/node_example/src/nodes/talker_node.cpp"
},

Header File

Now we need to construct a Talker object that publishes the custom message periodically in the talker node. Edit include/node_example/talker.hpp.

Expand the once header guard snippet.

#ifndef NODE_EXAMPLE_TALKER_H
#define NODE_EXAMPLE_TALKER_H



#endif /* end of include guard: NODE_EXAMPLE_TALKER_H */

Include header files.

#ifndef NODE_EXAMPLE_TALKER_H
#define NODE_EXAMPLE_TALKER_H

#include <string>

#include <ros/ros.h>
#include <ros/time.h>
#include <dynamic_reconfigure/server.h>

#include <node_example/NodeExampleData.h>
#include <node_example/NodeExampleConfig.h>
......

Expand the ns namespace snippet.

......
#include <node_example/NodeExampleConfig.h>

namespace node_example
{

}
......

Expand the cl class snippet. The Talker object has a NodeHandle which will be initialized by the non-default constructor. Since we will not use the default copy constructor and default assignment constructor that come along with the non-default constructor, we delete these two functions. The default de-constructor is good enough in this example.

......
namespace node_example
{
  class Talker
  {
  public:
    Talker(const ros::NodeHandle& nh);


    ~Talker() = default;


    Talker(const Talker&) = delete;

    Talker &operator=(const Talker&) = delete;

  private:
    ros::NodeHandle nh_;

  };
} /* node_example */
......

Talker will publish our custom message periodically, so it needs a Publisher and a Timer.

......
  private:
    ros::NodeHandle nh_;

    ros::Timer timer_;

    ros::Publisher pub_;
......

Also, we would like to change the custom message on-the-fly, so we need a dynamic reconfigure server.

......
  ros::Publisher pub_;

  dynamic_reconfigure::Server<node_example::NodeExampleConfig> config_server_;
};
}  // namespace node_example
......

Then we need to prepare data to be published. Instead of initializing a new NodeExampleData message every time before publication, we’d better have one message as a member variable and only change its attributes before publication. Finally, the enable variable will toggle on/off the talker node. That’s all the member variables for the Talker. The next step is to write some member functions to manipulate these variables.

......
  dynamic_reconfigure::Server<node_example::NodeExampleConfig> config_server_;

  NodeExampleData data_;

  bool enable_;
};
}  // namespace node_example
......

Let’s start from the constructor first because it delineates the functionality of the Talker class and it will be the only function exposed to the user. We use member initializer list to initialize the node handle nh_ using the given node handle nh, the message data_ using default constructor (i.e., empty string for message and $0$ for a and b), and enable_ to be true. Other member variables will be handled in the function body.

......
public:
  Talker(const ros::NodeHandle& nh) : nh_{ nh }, enable_{ true }, data_{}
  {
  }

  virtual ~Talker();
......

The setCallback function of the dynamic reconfigure server requires a callback function config_callback that gets called whenever we change the parameters in rqt_reconfigure. config_callback takes three parameters: a pointer to an object to be modified, a configuration object of the type automatically generated by the cfg file, e.g., NodeExampleConfig, and a level integer indicating which parameter has been changed. Since we are already in the scope of the object to be modified, we only need to write a callback function configCallback to take care of the last two parameters and use boost::bind to insert the pointer to the current object (this) to the front of config_callback.

......
public:
  Talker(const ros::NodeHandle& nh) : nh_{ nh }, enable_{ true }
  {
    // Set up a dynamic reconfigure server.
    // Note: do this before parameter server,
    // otherwise, some of the parameters might be over-written.
    dynamic_reconfigure::Server<node_example::NodeExampleConfig>::CallbackType config_callback;
    config_callback = boost::bind(&Talker::configCallback, this, _1, _2);
    config_server_.setCallback(config_callback);
  }
......

private:
  //////////////////////
  // Member Functions //
  //////////////////////
  void configCallback(node_example::NodeExampleConfig& config __attribute__((unused)),
                      uint32_t level __attribute__((unused)))
  {
    // We will postpone the implementation of this function after finishing all other functions
    // because we might rely on other functions to modify the member variables.
    return;
  }

  //////////////////////
  // Member Variables //
  //////////////////////
  ros::NodeHandle nh_;
......

The following part consists of parameters that can be modified by launch file and/or command line rather than dynamic reconfigure. The param function will try to store the parameter given by rosparam into the first input variable, if it fails, it will take the second input variable as the default parameter.

......
    config_server_.setCallback(config_callback);

    // Declare variables that can be modified by launch file rather than dynamic reconfigure.
    double rate = 1.0;  // This node will spin at 1Hz.
    nh_.param("enable", enable_, enable_);
    nh_.param("message", data_.message, data_.message);
    nh_.param("a", data_.a, data_.a);
    nh_.param("b", data_.b, data_.b);
    nh_.param("rate", rate, rate);
  }
......

Now we are ready to advertise on the “example” topic and publish periodically using the timerCallback function.

......
    nh_.param("rate", rate, rate);

    // Create a publisher and name the topic.
    start();

    // Create timer.
    timer_ = nh_.createTimer(ros::Duration(1.0 / rate), &Talker::timerCallback, this);
  }

......

private:

......

  void start()
  {
    pub_ = nh_.advertise<node_example::NodeExampleData>("example", 10);
  }

  void timerCallback(const ros::TimerEvent& event __attribute__((unused)))
  {
    if (!enable_)
      return;
    pub_.publish(data_);
  }
......

Finally, let’s complete the configCallback function.

......
private:
  //////////////////////
  // Member Functions //
  //////////////////////
  void configCallback(node_example::NodeExampleConfig& config,
                      uint32_t level __attribute__((unused)))
  {
    data_.message = config.message;
    data_.a = config.a;
    data_.b = config.b;
    enable_ = config.enable;
    if (enable_)
      start();
    else
      stop();
  }

  void stop()
  {
    pub_.shutdown();
  }
......

The talker.hpp file is done! Now we can create a Talker object in the talker_node.cpp.

#include <ros/ros.h>

#include <node_example/talker.hpp>

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

  // Construct an object here
  node_example::Talker talker(nh);

  ros::spin();
  return 0;
}

We can build and run the talker node now! Open a new tab in terminal. If you use the default GNOME terminal in Ubuntu, the shorcut is ctrl+shift+t.

# Terminal #1
cd ~/example_ws
catkin build  # or catkin build node_example
. devel/setup.bash  # equivalent to source devel/setup.bash
roscore

# Terminal #2
. devel/setup.bash
rosrun node_example talker

# Terminal #3
. devel/setup.bash
rostopic echo /example

# We shall see the following outputs.

# message: ''
# a: 0
# b: 0
# ---

Leave a Comment