Skip to content

Wiki

ROS2 Serial Driver library

About 15 min read

Existing English translations were reused. 4 current source blocks remain in Simplified Chinese, without triggering paid translation.

1.ROS1 Serial Library 1.1

This is a ROS1 library, but this library can also be compiled from third-party source code for ROS2.

http://wjwwood.io/serial/doc/1.1.0/index.html

2.ROS2 Serial Library 1.2

As the name suggests, it is a serial port packet,

This library is applicable to ROS Humble and ROS Jazzy versions.

Official explanation:

https://docs.ros.org/en/humble/p/serial_driver/generated/index.html#

https://index.ros.org/p/serial_driver/

Below is a tutorial hand-crafted based on the official documentation (as of before this tutorial was created, there was not a single decent tutorial).

2.1.SerialDriver library features

This library is mainly based on the Boost.Asio library, so the principle is the characteristics of the Boost.Asio library:

Event-driven Boost.Asio Use the low-level I/O multiplexing mechanisms provided by the operating system (such as Linux's epoll and Windows' I/O Completion Ports) to monitor whether file descriptors (such as serial ports, sockets, etc.) have available data. When data arrives, the corresponding event is triggered.

Callback mechanismWhen a user calls an asynchronous method (such as async_read or async_receive), a callback function is passed in. When the specified event occurs (such as data reception completion), Boost.Asio automatically calls the callback function provided by the user.

I/O Service Object The core of Boost.Asio is a io_service or io_context object, which manages all asynchronous operations and the scheduling of callbacks. When receiving asynchronously:

  • The user registers an operation (such as async_receive) with io_context.

  • io_context suspends the asynchronous operation and waits for the I/O event to be triggered.

  • When the I/O event is triggered,io_context invokes the corresponding callback function.

Does not block the main thread Asynchronous operations do not block the calling thread. The main thread can continue executing other tasks, while the asynchronous operation completes in the background.

2.2.Advantages based on the Boost.Asio library

Boost.Asio库是靠系统IO进行事件触发的,并且单独开了一个线程去干这件事,所以这就使boost库拥有单片机的串口中断的特点,又拥有真多线程的特点。

Although the microcontroller's serial interrupt can also be triggered by events via IO, since the microcontroller has only one true thread, entering the interrupt will inevitably block the main thread, causing a loss of efficiency.

If receiving directly in the main thread and adding an infinite loop and delay, this approach is often the least efficient and carries significant safety risks, so it is strongly not recommended.

Efficiency: Boost.Asio receive >> MCU serial port interrupt >> multithreaded blocking receive >> single-threaded blocking receive (when the transmission speed is very fast, all are far greater than >>)

2.3.Install ROS2 serial port package

serial_driver is a library that encapsulates serial communication functionality, and it is divided into two parts:

  • ROS2 Dependency version: Used in the ROS2 environment, relies on the communication mechanism of ROS2 (such as rclcpp).

  • Standalone library version: Does not depend on ROS2 and can be used directly in ordinary C++ projects. (Untested)

2.3.1.Method 1: Install in ROS2
sudo apt install ros-<ros2-distro>-asio-cmake-module
sudo apt install ros-<ros2-distro>-serial-driver

#humble
sudo apt install ros-humble-asio-cmake-module
sudo apt install ros-humble-serial-driver
#jazzy
sudo apt install ros-jazzy-asio-cmake-module
sudo apt install ros-jazzy-serial-driver
2.3.2.Method 2: General (This method has not been tested; if you have tested it, please help delete this sentence)

If you are using it in ROS1 or elsewhere, and ROS2 is not installed, please choose this method.

Clone repository

首先,克隆 transport_drivers 仓库(包含 serial_driver):

https://github.com/ros-drivers/transport_drivers

git clone https://github.com/ros-drivers/transport_drivers.git

Install dependencies

serial_driver depends on the ASIO library (a cross-platform C++ networking library). Ensure that ASIO is installed on the system:

sudo apt install libasio-dev

Compile standalone library

Enter the serial_driver directory and compile:

cd transport_drivers/serial_driver
mkdir build
cd build
cmake ..
make
sudo make install

After installation is complete, simply add the package normally in CMake.

2.3.3.Other preparatory work

Serial Port Debugging Assistant Installation (Linux Version) (If you need the Windows version, ask the Control Group for it)

sudo apt-get install cutecom

Permissions


# 将用户权限提高
sudo usermod -aG dialout $USER
newgrp dialout

For more operations on USB devices in Linux, seeVinci Robot Team Linux Introductory Tutorial

2.4.Initialization explained

  1. Create a feature package
ros2 pkg create cpp01_serial --build-type ament_cmake --dependencies rclcpp serial_driver --node-name demo01_uart_send
  1. Include the header file:#include "serial_driver/serial_driver.hpp"

  2. First, initialize the serial port:

Serial port initialization functioninit_port()It has two parameters,

The first parameter device_name is the port name

The second parameter config is the serial port parameter setting

device_name是个字符串,是填/dev下的设备名

ls /dev | grep USB

For more operations on USB devices in Linux, seeVinci Robot Team Linux Introductory Tutorial

config is a class object, so this class object needs to be instantiated as follows:

This thing is a lightweight class, so there is no need to put it on the heap; just create it directly on the stack.

SerialPortConfig(uint32_t baud_rate, drivers::serial_driver::FlowControl flow_control , drivers::serial_driver::Parity parity, drivers::serial_driver::StopBits stop_bits)

From this overloaded function, it can be seen (ask the control group about parameters you do not understand).

第一个参数baud_rate 波特率

The second parameter flow_control flow control

The third parameter parity: parity check

The fourth parameter stop_bits stop bit

The specific parameters are inserial_port.hppof them

This way, the serial port is successfully initialized.

2.5.Detailed Explanation of Sending Code

  1. Main sending logic

Next, we will write the main sending logic. First, create a delay. Do not use a dead loop such as while(true) for the delay; ROS2 provides a timer.

First, create a timer (there is no need to explain timer creation in detail; if you do not understand it, review Teacher Zhao's video above).

One thing to note is that we use milliseconds , not microseconds .

The core sending function is this send function. As can be seen, the entry parameter is a vector of vector type,

#include "rclcpp/rclcpp.hpp"
#include "serial_driver/serial_driver.hpp"

class Serial_Node : public rclcpp::Node
{
public:
  Serial_Node() : Node("serial_node_cpp")
  {
    // 等设备准备好再初始化
    // std::this_thread::sleep_for(std::chrono::milliseconds(500));

    // 串口设备名(根据实际设备调整)
    const std::string device_name = "/dev/ttyUSB0";

    RCLCPP_INFO(this->get_logger(), "Serial port Node Open!");
    // 创建串口配置对象
    // 波特率115200;不开启流控制;无奇偶效验;停止位1。
    drivers::serial_driver::SerialPortConfig config(
        9600,
        drivers::serial_driver::FlowControl::NONE,
        drivers::serial_driver::Parity::NONE,
        drivers::serial_driver::StopBits::ONE);

    // 初始化串口
    try
    {
      io_context_ = std::make_shared<drivers::common::IoContext>(1);
      // 初始化 serial_driver_
      serial_driver_ = std::make_shared<drivers::serial_driver::SerialDriver>(*io_context_);
      serial_driver_->init_port(device_name, config);
      serial_driver_->port()->open();

      RCLCPP_INFO(this->get_logger(), "Serial port initialized successfully");
      RCLCPP_INFO(this->get_logger(), "Using device: %s", serial_driver_->port().get()->device_name().c_str());
      RCLCPP_INFO(this->get_logger(), "Baud_rate: %d", config.get_baud_rate());
    }
    catch (const std::exception &ex)
    {
      RCLCPP_ERROR(this->get_logger(), "Failed to initialize serial port: %s", ex.what());
      return;
    }

    // 设置发送定时器
    send_timer_ = this->create_wall_timer(
        std::chrono::milliseconds(500),
        std::bind(&Serial_Node::send_message_timer_callback, this));
  }

private:
  void send_message_timer_callback()
  {
    // 发送一串字符串
    const std::string message = "Hello from ROS 2!\n";
    std::vector<uint8_t> data(message.begin(), message.end());
    // std::vector<uint8_t> hex_data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello" in ASCII
    auto port = serial_driver_->port();

    try
    {
      size_t bytes_sent_size = port->send(data);
      RCLCPP_INFO(this->get_logger(), "Sent: %s (%ld bytes)", message.c_str(), bytes_sent_size);
    }
    catch(const std::exception &ex)
    {
      RCLCPP_ERROR(this->get_logger(), "Error Transmiting from serial port:%s",ex.what());
    }
  }

  std::shared_ptr<drivers::serial_driver::SerialDriver> serial_driver_;
  std::shared_ptr<drivers::common::IoContext> io_context_;
  rclcpp::TimerBase::SharedPtr send_timer_;
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  auto node = std::make_shared<Serial_Node>();

  rclcpp::spin(node);

  rclcpp::shutdown();
  return 0;
}

2.6.Detailed Explanation of the Receiver Code

2.6.1.Timer method (inefficient, not recommended)
#include "rclcpp/rclcpp.hpp"
#include "serial_driver/serial_driver.hpp"
#include <string>

class Serial_Node : public rclcpp::Node
{
public:
  Serial_Node() : Node("serial_node_cpp")
  {
    // 等设备准备好再初始化
    // std::this_thread::sleep_for(std::chrono::milliseconds(500));

    // 串口设备名(根据实际设备调整)
    const std::string device_name = "/dev/ttyUSB0";

    RCLCPP_INFO(this->get_logger(), "Serial port Node Open!");
    // 创建串口配置对象
    // 波特率115200;不开启流控制;无奇偶效验;停止位1。
    drivers::serial_driver::SerialPortConfig config(
        9600,
        drivers::serial_driver::FlowControl::NONE,
        drivers::serial_driver::Parity::NONE,
        drivers::serial_driver::StopBits::ONE);

    // 初始化串口
    try
    {
      io_context_ = std::make_shared<drivers::common::IoContext>(1);
      // 初始化 serial_driver_
      serial_driver_ = std::make_shared<drivers::serial_driver::SerialDriver>(*io_context_);
      serial_driver_->init_port(device_name, config);
      serial_driver_->port()->open();

      RCLCPP_INFO(this->get_logger(), "Serial port initialized successfully");
      RCLCPP_INFO(this->get_logger(), "Using device: %s", serial_driver_->port().get()->device_name().c_str());
      RCLCPP_INFO(this->get_logger(), "Baud_rate: %d", config.get_baud_rate());
    }
    catch (const std::exception &ex)
    {
      RCLCPP_ERROR(this->get_logger(), "Failed to initialize serial port: %s", ex.what());
      return;
    }

    // 设置发送定时器
    receive_timer_ = this->create_wall_timer(
        std::chrono::milliseconds(3),
        std::bind(&Serial_Node::receive_message_timer_callback, this));
  }

private:
  void receive_message_timer_callback()
  {
    std::vector<uint8_t> data(100); // "Hello" in ASCII
    auto port = serial_driver_->port();

    try
    {
      size_t bytes_receive_size = port->receive(data);
      if(bytes_receive_size > 0)
      {
        std::string message(data.begin(),data.begin() + bytes_receive_size);
        RCLCPP_INFO(this->get_logger(), "Receive: %s (%ld bytes)", message.c_str(), bytes_receive_size);
      }
    }
    catch(const std::exception &ex)
    {
      RCLCPP_ERROR(this->get_logger(), "Error Receiving from serial port:%s",ex.what());
    }
  }

  std::shared_ptr<drivers::serial_driver::SerialDriver> serial_driver_;
  std::shared_ptr<drivers::common::IoContext> io_context_;
  rclcpp::TimerBase::SharedPtr receive_timer_;
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  auto node = std::make_shared<Serial_Node>();

  rclcpp::spin(node);

  rclcpp::shutdown();
  return 0;
}

2.6.2.Asynchronous approach (efficient)
#include "rclcpp/rclcpp.hpp"
#include "serial_driver/serial_driver.hpp"
#include <string>

class Serial_Node : public rclcpp::Node
{
public:
  Serial_Node() : Node("serial_node_cpp")
  {
    // 等设备准备好再初始化
    // std::this_thread::sleep_for(std::chrono::milliseconds(500));

    // 串口设备名(根据实际设备调整)
    const std::string device_name = "/dev/ttyUSB0";

    RCLCPP_INFO(this->get_logger(), "Serial port Node Open!");
    // 创建串口配置对象
    // 波特率115200;不开启流控制;无奇偶效验;停止位1。
    drivers::serial_driver::SerialPortConfig config(
        9600,
        drivers::serial_driver::FlowControl::NONE,
        drivers::serial_driver::Parity::NONE,
        drivers::serial_driver::StopBits::ONE);

    // 初始化串口
    try
    {
      io_context_ = std::make_shared<drivers::common::IoContext>(1);
      // 初始化 serial_driver_
      serial_driver_ = std::make_shared<drivers::serial_driver::SerialDriver>(*io_context_);
      serial_driver_->init_port(device_name, config);
      serial_driver_->port()->open();

      RCLCPP_INFO(this->get_logger(), "Serial port initialized successfully");
      RCLCPP_INFO(this->get_logger(), "Using device: %s", serial_driver_->port().get()->device_name().c_str());
      RCLCPP_INFO(this->get_logger(), "Baud_rate: %d", config.get_baud_rate());
    }
    catch (const std::exception &ex)
    {
      RCLCPP_ERROR(this->get_logger(), "Failed to initialize serial port: %s", ex.what());
      return;
    }

    async_receive_message();
  }

private:
  void async_receive_message()  //创建一个函数更方便重新调用
  {
    auto port = serial_driver_->port();

    // 设置接收回调函数
    port->async_receive([this](const std::vector<uint8_t> &data,const size_t &size) 
    {
        if (size > 0)
        {
            // 处理接收到的数据
            std::string received_message(data.begin(), data.begin() + size);
            RCLCPP_INFO(this->get_logger(), "Received: %s (%ld bytes)", received_message.c_str(),size);
        }
        // 继续监听新的数据
        async_receive_message();
    }
    );
  }

  std::shared_ptr<drivers::serial_driver::SerialDriver> serial_driver_;
  std::shared_ptr<drivers::common::IoContext> io_context_;
  std::vector<uint8_t> receive_data_buffer = std::vector<uint8_t>(1024); // 接收缓冲区
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  auto node = std::make_shared<Serial_Node>();

  rclcpp::spin(node);

  rclcpp::shutdown();
  return 0;
}

2.7.Total code (both transmit and receive)

#include "rclcpp/rclcpp.hpp"
#include "serial_driver/serial_driver.hpp"
#include <string>

class Serial_Node : public rclcpp::Node
{
public:
  Serial_Node() : Node("serial_node_cpp")
  {
    // 等设备准备好再初始化
    // std::this_thread::sleep_for(std::chrono::milliseconds(500));

    // 串口设备名(根据实际设备调整)
    const std::string device_name = "/dev/ttyUSB0";

    RCLCPP_INFO(this->get_logger(), "Serial port Node Open!");
    // 创建串口配置对象
    // 波特率115200;不开启流控制;无奇偶效验;停止位1。
    drivers::serial_driver::SerialPortConfig config(
        9600,
        drivers::serial_driver::FlowControl::NONE,
        drivers::serial_driver::Parity::NONE,
        drivers::serial_driver::StopBits::ONE);

    // 初始化串口
    try
    {
      io_context_ = std::make_shared<drivers::common::IoContext>(1);
      // 初始化 serial_driver_
      serial_driver_ = std::make_shared<drivers::serial_driver::SerialDriver>(*io_context_);
      serial_driver_->init_port(device_name, config);
      serial_driver_->port()->open();

      RCLCPP_INFO(this->get_logger(), "Serial port initialized successfully");
      RCLCPP_INFO(this->get_logger(), "Using device: %s", serial_driver_->port().get()->device_name().c_str());
      RCLCPP_INFO(this->get_logger(), "Baud_rate: %d", config.get_baud_rate());
    }
    catch (const std::exception &ex)
    {
      RCLCPP_ERROR(this->get_logger(), "Failed to initialize serial port: %s", ex.what());
      return;
    }

        // 设置发送定时器
    transmit_timer_ = this->create_wall_timer(
        std::chrono::milliseconds(5000),
        std::bind(&Serial_Node::send_message_timer_callback, this));

    async_receive_message();   //进入异步接收
  }

private:

  void send_message_timer_callback()
  {
    // 发送一串字符串
    const std::string transmitted_message = "Hello from ROS 2!\n";
    transmit_data_buffer = std::vector<uint8_t>(transmitted_message.begin(), transmitted_message.end());
    // std::vector<uint8_t> hex_data = {0x48, 0x65, 0x6C, 0x6C, 0x6F}; // "Hello" in ASCII
    auto port = serial_driver_->port();

    try
    {
      size_t bytes_transmit_size = port->send(transmit_data_buffer);
      RCLCPP_INFO(this->get_logger(), "Transmitted: %s (%ld bytes)", transmitted_message.c_str(), bytes_transmit_size);
    }
    catch(const std::exception &ex)
    {
      RCLCPP_ERROR(this->get_logger(), "Error Transmiting from serial port:%s",ex.what());
    }
  }

  void async_receive_message()  //创建一个函数更方便重新调用
  {
    auto port = serial_driver_->port();

    // 设置接收回调函数
    port->async_receive([this](const std::vector<uint8_t> &data,const size_t &size) 
    {
        if (size > 0)
        {
            // 处理接收到的数据
            std::string received_message(data.begin(), data.begin() + size);
            RCLCPP_INFO(this->get_logger(), "Received: %s (%ld bytes)", received_message.c_str(),size);
        }
        // 继续监听新的数据
        async_receive_message();
    }
    );
  }

  std::shared_ptr<drivers::serial_driver::SerialDriver> serial_driver_;
  std::shared_ptr<drivers::common::IoContext> io_context_;
  rclcpp::TimerBase::SharedPtr transmit_timer_;
  std::vector<uint8_t> transmit_data_buffer = std::vector<uint8_t>(1024); // 发送缓冲区
  std::vector<uint8_t> receive_data_buffer = std::vector<uint8_t>(1024);  // 接收缓冲区
};

int main(int argc, char **argv)
{
  rclcpp::init(argc, argv);

  auto node = std::make_shared<Serial_Node>();

  rclcpp::spin(node);

  rclcpp::shutdown();
  return 0;
}

2.8.Precautions

  1. Prohibit referenceboost/asio.hpp:Since the principle of this library is the Boost library, and it is ROS2's built-in Boost library, if you use the generic version ofboost/asio.hpp,it will cause errors with many duplicate symbol issues.