ROS Resources: Documentation | Support | Discussion Forum | Service Status | Q&A answers.ros.org

Proper way to write a driver "component compatible"

What is the correct way to write a sensor driver?

ROS2 provides a new way to write the nodes that allow composition and by extension zero-copy.

In ROS1, many drivers are written with a while(true) loop such as:

while(ros::ok()) {
   driver->poll(); // Returns once data available such file descriptor polling
   new_data = driver->read_data();
   if (new_data) publisher.publish(new_data);
   ros::spinOnce();
}

How do we implement properly such drivers in ROS2 in a Composable way? Do we create it in a thread? Do we have (mandatory) to use rclcpp::create_wall_timer?

Is the method that Velodyne uses for their Lidar the proper way?

VelodyneDriver::VelodyneDriver(const rclcpp::NodeOptions & options)
: rclcpp::Node("velodyne_driver_node", options) {
  // Initialize
  future_ = exit_signal_.get_future();
  poll_thread_ = std::thread(&VelodyneDriver::pollThread, this);
}

VelodyneDriver::~VelodyneDriver() {
  exit_signal_.set_value();
  poll_thread_.join();
}


void VelodyneDriver::poll() {
  // Poll sensor and publish data
}

void VelodyneDriver::pollThread()
{
  std::future_status status;

  do {
    poll();
    status = future_.wait_for(std::chrono::seconds(0));
  } while (status == std::future_status::timeout);
}

The basic thing for composability that you need is to have all actions and configuration done by the class itself; nothing can be external, since your “component container” won’t have any idea how to do any of that.

Thus, I’d say that there are two basic ways to achieve it, which you mentioned in your initial post.

  1. You use a thread to do the work, and then check if the thread needs to shutdown. This is how the Velodyne driver works. The benefit to this is that it is completely independent of the ROS 2 executor loop, so can block for arbitrary amounts of time without holding up other things like subscriptions. The downside is that it is another thread to manage, and you have to worry about locking between the data touched by the thread and the main thread.
  2. Register a callback with the event loop so that it executes on a fixes schedule. The benefit here is that you are using the same mechanism as the rest of ROS 2. The downside is that you have to be sure that your driver poll() method returns in a reasonable amount of time, otherwise you can end up starving the rest of the callbacks (this can be mitigated with callback groups).

Which you choose depends on your application. I wrote that Velodyne driver loop with the thread, so I obviously think that is a good way to do it. But your mileage may vary.

1 Like

Yeah, you can just have your own thread to do the polling, though I’ll note that in your original example, there’s no ros::spin*() calls made, so that’s not good :slight_smile:. If you wanted to spin (recommended) and not interfere with the original while loop, you’d probably use an async spinner, which just spins in a thread, so in both cases (ROS 1 and ROS 2) you either need to time share with the spin thread or create a separate one.

I will say that the use of a future in the Velodyne case may be overkill, so don’t let that scare you. You could instead continue to check rclcpp::ok() or use a (atomic) bool to signal shutdown. The future is fancier and maybe they’re using it for other reasons, but it isn’t necessary.


The other option, other than creating your own thread, is to have a callback that does the polling, sharing the executor’s thread (or threads depending on the type executor). One way to do that now is to have a timer which fires regularly.

But we’ve also talked about it in the past that it would be nice to have an “idle” callback which gets fired when there’s nothing else going on, but right now that doesn’t exist. Other event systems have this, e.g. wxWidgets has the IdleEvent (https://docs.wxwidgets.org/trunk/classwx_idle_event.html) and Qt recommends using a timer with a period of 0 (https://doc.qt.io/qt-5/qtimer.html#interval-prop).


Given how most drivers work, where you want to either poll very regularly, in order to reduce latency of acquisition, a dedicated thread for polling the sensor and publishing data seems like the best option.

1 Like

It would be nice to have an executor that can be externally-triggered. I’m not sure how the mechanism would or should work, but it would allow a driver developer to set up an executor that gets triggered when there is data available on a network socket (using select), for example.

Indeed, I now added the ros::spinOnce() as the example doesn’t make sense without it.

As you suggest I will remove the exit signal & future and use rclcpp::ok();
It will avoid someone to forget about the exit signal.