Utilizzare una Singola Callback per Molteplici Subscriber – ROS Italian Tutorial

Utilizzare una Singola Callback per Molteplici Subscriber – ROS Italian Tutorial

This tutorial is created by Rosbotics Ambassador 012 Alessandro 

Rosbotics Ambassador Program https://www.theconstruct.ai/rosbotics-ambassador/)

In questo tutorial verrà mostrata un’implementazione di un nodo subscriber in grado di leggere messaggi di diverso tipo e provenienti da molteplici topic, gestiti attraverso una singola funzione di callback.

Risorse

Come utilizzare i Rosject

Nel caso non si volesse installare ROS Noetic sul proprio sistema e fare il set up dell’ambiente di sviluppo, è possibile utilizzare i
Rosject offerti da The Construct per poter testare i contenuti del tutorial o per seguirlo passo passo.
Per fare ciò sarà sufficiente seguire il link indicato nelle risorse e cliccare su “Run”. In questo modo si avrà accesso a una copia del
Rosject utilizzato all’interno del tutorial.

Aprire terminal e IDE

Per aprire un nuovo terminale e avere accesso all’IDE forniti dal Rosject, sarà possibile utilizzare la console in basso a destra.

Prerequisiti

Per poter seguire il tutorial passo passo sarò necessario avere il nodo publisher, che pubblica su diversi topic, attivo e creare un package dove poter inserire il codice per il subscriber.
Per iniziare eseguiamo il nodo publisher. Sarà sufficiente aprire il terminale dalla console (vedi Risorse) e inserire i seguenti comandi nelle rispettive schede del terminale:

Terminale 1:
roscore
Terminale 2:
cd catkin_ws
rosrun publisher publisher.py

Quindi è possibile verificare il corretto funzionamento del nodo aprendo una seconda finestra al’interno del terminale e osservando l’output del comando rostopic list.
L’output previsto è il seguente:

Terminale 3:
rostopic list

/chatter0
/chatter1
/chatter2
/rosout
/rosout_agg

Procediamo quindi a creare il package per il subscriber:

Terminale 3:
cd src
catkin_create_pkg multiple_subscriber rospy std_msgs
mkdir multiple_subscriber/script

Situazione di partenza

Al momento abbiamo eseguito il nodo publisher che pubblica su 3 topic messaggi di vario tipo. In particolare:

  • /chatter0 pubblica messaggi di tipo String
  • /chatter1 pubblica messaggi di tipo Float64
  • /chatter2 pubblica messaggi di tipo Bool

Vediamo quindi come possiamo gestire tutti e tre i topic, creando un subscriber in grado di leggere i messaggi dai chatter e di gestirli con una singola funzione di callback, differenziando il risultato di quest’ultima sfruttando l’uso di parametri.

Creazione del file multiple_sub.py

Proseguiamo creando il file necessario a definire il nostro subscriber e tutte le sue funzioni.

Terminale 3:
touch multiple_sub/script/multiple_subscriber.py # Crea il file
chmod +x multiple_sub/script/multiple_subscriber.py # Garantisce i permessi di esecuzione
cd ..
catkin_make

Implementazione del subscriber

Partiamo quindi dallo specificare un interprete per il nostro codice in Python, dall’importare i moduli necessari (rospy e in questo caso std_msgs/String, std_msgs/Float64, std_msgs/Bool) e dallo scrivere un main che inizializzi il nodo e invochi rospy.spin() per poter eseguire le callback del nostro subscriber.

main

multiple_subscriber.py
import rospy
from std_msgs.msg import String, Float64, Bool

if __name__=="__main__":

rospy.init_node("multiple_subscriber")

topics = {"chatter0": [String, "Argomento passato da chatter0"], "chatter1": [Float64, "Argomento passato da chatter1"], "chatter2": [Bool, "Argomento passato da chatter2"]}

multiple_subscribe()

rospy.spin()

All’interno della funzione abbiamo definito anche topics, un dizionario contenente le specifiche dei topic che vogliamo andare a leggere, seguendo la seguente struttura:
{"nome_topic": [Tipo, Argomento]}

Per ogni chiave topic abbiamo quindi definito una lista contente il tipo del messaggio inviato dal topic e un argomento (in questo caso una stringa che esplicita di essere l’argomento passato per lo specifico topic) .

multiple_subscribe

La funzione multiple_subscribe() sarà definita perché il nodo possa seguire i 3 topic e perché i messaggi pubblicati possano innescare la callback.

multiple_subscriber.py
def multiple_subscribe():

for t in topics:

rospy.Subscriber(t, topics[t][0], callback, callback_args=topics[t][1])

Il nodo segue quindi ciascuno dei topic definiti nel dizionario, aspettandosi messaggi del relativo tipo, invocando una funzione callback e passando il relativo argomento tramite il parametro callback_args.

callback

Vediamo una semplice implementazione della callback per poter gestire i tre topic.

multiple_subscriber.py
def callback(data, args):

rospy.loginfo(f"Ricevuto: {data.data}")

print(args)

print('\n')

In questo caso andiamo a stampare come info di ROS il contenuto del messaggio arrivato alla callback e stampiamo l’argomento passato corrispondente. Entrambi dipenderanno da quale topic ha innescato la callback e perciò a questo livello il messaggio può essere gestito a seconda del parametro ad esso associato, potendo implementare in qualche modo un’esecuzione “su misura” della funzione di callback.

Test del subscriber

Siamo pronti per poter eseguire il nostro subscriber!
Ciò che ci aspettiamo è che vengano stampati allo stesso tempo il messaggio e l’argomento relativi alla callback innescata dall’arrivo di un messaggio dal singolo topic. L’output dovrebbe quindi essere di questo tipo:

[INFO] [***.***]: Ricevuto: Questo pubblica una stringa
Argomento passato da chatter0

[INFO] [***.***]: Ricevuto: 0.42
Argomento passato da chatter1

[INFO] [***.***]: Ricevuto: True
Argomento passato da chatter2

Per fare ciò dobbiamo eseguire il nostro nodo:

Terminale 3:
rosrun multiple_sub multiple_subscriber.py

Possiamo osservare come gli output si alternano ogni secondo a rotazione, ma questo è dovuto alla singola implementazione del publisher (che è possibile trovare all’interno del Rosject, vedi Risorse).

In ogni caso per ogni topic viene ricevuto, gestito e stampato un messaggio specifico relativo ad esso, e l’elaborazione del relativo parametro, con diverso risultato nonostante l’utilizzo della medesima funzione di callback.

Video Tutorial

Learn how to enable live parameter updates in ROS 2 (C++)

Learn how to enable live parameter updates in ROS 2 (C++)

What we are going to learn

  1. The limitations of having a minimal ROS2 parameter implementation
  2. How to add a parameter callback method

List of resources used in this post

  1. Use the rosject: https://app.theconstructsim.com/l/53e75e28/ 
  2. The Construct: https://app.theconstructsim.com/
  3. Original ROS2 Documentation: Monitoring for parameter changes (C++)
  4. ROS2 Courses –▸
    1. ROS2 Basics in 5 Days Humble (Python): https://app.theconstructsim.com/Course/132
    2. ROS2 Basics in 5 Days Humble (C++): https://app.theconstructsim.com/Course/133

Overview

ROS2 parameters are great for configurable nodes that you can adapt to your robot configuration simply by changing a configuration file or a launch file. However, if we just implemented the basics you will have to re-run your node each time you change a parameter. You can’t change the parameter on-the-fly and have it updated in the robot. But by adding a callback function that updates the variables in our code, it is possible to do a live parameter update while a program is running, removing the need for a tedious node restart. Learn how to do it in this post.

ROS Inside!

ROS Inside

ROS Inside

Before anything else, if you want to use the logo above on your own robot or computer, feel free to download it and attach it to your robot. It is really free. Find it in the link below:

ROS Inside logo

Opening the rosject

In order to follow this tutorial, we need to have ROS2 installed in our system, and ideally a ros2_ws (ROS2 Workspace). To make your life easier, we have already prepared a rosject with a simulation for that: https://app.theconstructsim.com/l/53e75e28/.

You can download the rosject on your own computer if you want to work locally, but just by copying the rosject (clicking the link), you will have a setup already prepared for you.

After the rosject has been successfully copied to your own area, you should see a Run button. Just click that button to launch the rosject (below you have a rosject example).

Learn ROS2 Parameters - Run rosject

Learn how to enable live parameter updates (C++) – Run rosject (example of the RUN button)

After pressing the Run button, you should have the rosject loaded. Now, let’s head to the next section to get some real practice.

Starting the simulation

After having opened the rosject, let’s start a simulation. For that, let’s open a terminal by clicking the Open a new terminal button.

Open a new Terminal

Open a new Terminal

Once inside the terminal, let’s run the commands below:

cd ~/ros2_ws
colcon build --packages-select rule_based_obstacle_avoidance

 

Now that our workspace is built. let’s source it so that ROS can find our packages:

source install/setup.bash

 

After the workspace has been sourced, we can launch our simulation:

ros2 launch rule_based_obstacle_avoidance simulation.launch.py

If everything went well, you should have a simulation loaded and opened automatically in a few seconds. The simulation will open from a top view, but you can use the mouse to move the simulation to a different perspective.

 How to enable live parameter updates (C++)

How to enable live parameter updates (C++)

 

Understanding the problem

Below is an example node in which we have only implemented the barebone basics of parameters in ROS2.

Let’s see it in action and see how it behaves, especially when we update its parameter values.

In a second terminal, let’s run the node for Obstacle Avoidance.

source ~/ros2_ws/install/setup.bash

ros2 run rule_based_obstacle_avoidance obstacle_avoidance

If you watch the simulation for a while, you will see that when the robot detects the wall, it rotates and moves forward until it detects another wall, and repeats the process.

The node name is obstacle_avoidance_node (you can check it in a third terminal by running: ros2 node list)

Now, let’s list the parameters of the node in a third terminal:

ros2 param list /obstacle_avoidance_node

We should see the following output:

  angular_z_velocity
  linear_x_velocity
  qos_overrides./parameter_events.publisher.depth
  qos_overrides./parameter_events.publisher.durability
  qos_overrides./parameter_events.publisher.history
  qos_overrides./parameter_events.publisher.reliability
  safety_distance
  use_sim_time

 

Now, still in the third terminal, let’s check the value of the safety_distance parameter:

ros2 param get /obstacle_avoidance_node safety_distance

The output we should have got should be the following:

Double value is: 1.5

 

Now, let’s set the parameter to a new value:

ros2 param set /obstacle_avoidance_node safety_distance 1.0

The expected output is:

Set parameter successful

 

Ok, so far so good. But with the new value, we expect the robot to get closer to the wall before turning around because now the safe distance was set from 1.5 meters to 1.0. The problem is that the robot is not considering the new value that we just set.

We can follow the same idea to try to make the robot move faster. Let’s check the current velocity of the robot:

ros2 param get /obstacle_avoidance_node linear_x_velocity

The output we should have got should be the following:

Double value is: 0.2

 

If we increase the speed:

ros2 param set /obstacle_avoidance_node linear_x_velocity 0.5

The expected output is:

Set parameter successful

The parameter was reported as successfully set, yet the robot does not move faster, because it still uses the value loaded when the node started.

In the current code, parameter values are fixed. As such, every time a parameter value changes, the parameter value in the code stays the same even though you may have expected it to update based on the latest value set.

In order to solve this, we must add a parameter callback function to your code so that the variable in the code gets the freshest data.

Before moving to the next section, please kill the simulation and all nodes running by pressing Ctrl+C on all terminals.

 

Solution: Add a parameter callback method

Alright, have you closed all programs by pressing CTRL+C on all terminals?

If so, let’s move forward, then.

We need to modify the following file:

  • ros2_ws/src/rule_based_obstacle_avoidance/src/obstacle_avoidance.cpp

Let’s open that file using the IDE (Code Editor):

Open the IDE - Code Editor

Open the IDE – Code Editor

 

Once the Code Editor is open, you should be able to see the ros2_ws folder (ROS2 workspace) and navigate to the file we mentioned above:

  • ros2_ws/src/rule_based_obstacle_avoidance/src/obstacle_avoidance.cpp

If you check around line 45 on that file, you will find the “private:” section, where we define the private variables of our class, something like the following:

private:
  rclcpp::TimerBase::SharedPtr timer_;

  double linear_x_velocity_;
  double angular_z_velocity_;
  
   // ...

 

Let’s modify that part, in order to add two variables below right after the definition of “timer_”, so that our code looks like:

private:
  rclcpp::TimerBase::SharedPtr timer_;

  std::shared_ptr<rclcpp::ParameterEventHandler> param_subscriber_;
  std::shared_ptr<rclcpp::ParameterCallbackHandle> cb_handle_; 

  double linear_x_velocity_;
  double angular_z_velocity_;
  
   // ...

 

Now, above that “private:” section, around line 38, let’s add the following code to instantiate a ParameterEventHandler, providing the current ROS node to use to create the required subscriptions:

param_subscriber_ = std::make_shared<rclcpp::ParameterEventHandler>(this);

Below the param_subscriber_ we have to set a callback method, in this case, a lambda function:

// Set a callback for this node's parameter, "linear_x_velocity"
    auto callback_linear_x = [this](const rclcpp::Parameter &p) {
      RCLCPP_INFO(this->get_logger(),
                  "callback_linear_x: Received an update to parameter \"%s\" "
                  "of type %s: \"%f\"",
                  p.get_name().c_str(), p.get_type_name().c_str(),
                  p.as_double());
      linear_x_velocity_ = p.as_double();
    };

 

Then we set “callback_linear_x” as the callback to invoke whenever linear_x_velocity is updated. We store the handle that is returned by “add_parameter_callback“; otherwise, the callback will not be properly registered.

cb_handle_ = param_subscriber_->add_parameter_callback("linear_x_velocity", callback_linear_x);

 

After those changes, the “public:” section of our class should look like this:

class ObstacleAvoidance : public rclcpp::Node {
public:
  ObstacleAvoidance() : Node("obstacle_avoidance_node") {

   auto default_qos = rclcpp::QoS(rclcpp::SystemDefaultsQoS());
    subscription_ = this->create_subscription<sensor_msgs::msg::LaserScan>(
        "scan", default_qos,
        std::bind(&ObstacleAvoidance::topic_callback, this, _1));
    vel_msg_publisher_ =
        this->create_publisher<geometry_msgs::msg::Twist>("cmd_vel", 10);

    // declare parameters and set default values
    this->declare_parameter("linear_x_velocity", 0.2);
    this->declare_parameter("angular_z_velocity", 0.2);
    this->declare_parameter("safety_distance", 1.5);
    // get parameters values
    this->get_parameter("linear_x_velocity", linear_x_velocity_);
    this->get_parameter("angular_z_velocity", angular_z_velocity_);
    this->get_parameter("safety_distance", d);

   param_subscriber_ = std::make_shared<rclcpp::ParameterEventHandler>(this);

    // Set a callback for this node's parameter, "linear_x_velocity"
    auto callback_linear_x = [this](const rclcpp::Parameter &p) {
      RCLCPP_INFO(this->get_logger(),
                  "callback_linear_x: Received an update to parameter \"%s\" "
                  "of type %s: \"%f\"",
                  p.get_name().c_str(), p.get_type_name().c_str(),
                  p.as_double());
      linear_x_velocity_ = p.as_double();
    };

    cb_handle_ = param_subscriber_->add_parameter_callback("linear_x_velocity",

    RCLCPP_INFO(this->get_logger(), "Obstacle avoidance running");

    timer_ = this->create_wall_timer(
        1000ms, std::bind(&ObstacleAvoidance::timerCallback, this));
  }

  ~ObstacleAvoidance() {}

private:
    // ...

 

Now that everything is in place, let’s build our package again and source it, using the first terminal:

cd ~/ros2_ws/
colcon build --packages-select rule_based_obstacle_avoidance
source install/setup.bash

The package should have been built with no problems:

user:~/ros2_ws$ colcon build --packages-select rule_based_obstacle_avoidance
Starting >>> rule_based_obstacle_avoidance
Finished <<< rule_based_obstacle_avoidance [27.3s]

Summary: 1 package finished [27.7s]

 

Now that our package has been rebuilt and sourced, let’s launch the simulation again:

ros2 launch neo_simulation2 simulation.launch.py

The simulation should have been opened just like before, but now we will see the parameters affecting the simulation in “real-time”.

Before changing the parameters, let’s also launch the Obstacle Avoidance node, just like before, using the second terminal:

ros2 run rule_based_obstacle_avoidance obstacle_avoidance

You should see the robot approaching the wall, and turning around when getting close to it.

Changing the x velocity using ROS 2 Parameters

Ok, now that the robot is moving, let’s retrieve again the current value of the linear x velocity using the third terminal:

ros2 param get /obstacle_avoidance_node linear_x_velocity

Just like before, the expected output is:

Double value is: 0.2

 

Now let’s change that value:

ros2 param set /obstacle_avoidance_node  linear_x_velocity  1.0

We expect a successful output:

Set parameter successful

 

If you look at the simulation now, you should see that when the robot is moving forward (not turning around), it moves really faster. So, as we can see, we are now able to make ROS2 Parameters be reflected “instantly”.

This opens up really many different possibilities.

We hope this post was really helpful to you. If you want a live version of this post with more details, please check the video in the next section.

Youtube video

So this is the post for today. Remember that we have the live version of this post on YouTube. If you liked the content, please consider subscribing to our youtube channel. We are publishing new content ~every day.

Keep pushing your ROS Learning.

Related Courses & Training

If you want to learn more about ROS and ROS2, we recommend the following courses:

Pin It on Pinterest