Wiki
Robotics engineering conventions and ROS2 integration
Existing English translations were reused. 1 current source blocks remain in Simplified Chinese, without triggering paid translation.
The previous sections discussed the Boost.Asio itself.
This section discusses how you should organize code when actually doing robot projects, especially: the relationship between the serial port driver, the TCP/UDP communication module, and the ROS2 node.
Core principles:
不要在 ROS2 回调里写长时间阻塞的串口 read / TCP read。
通信模块应该自己用 Asio 异步运行。
ROS2 节点只负责发布、订阅、参数、生命周期和业务逻辑。
This section still uses std::bind.
1.示例 1:普通 main() 中模拟机器人协议解析
1.1.Program Objectives
Many lower-computer serial port protocols are line-based text, for example:
ODOM 1.0 2.0 0.5
BAT 24.3
IMU 0.1 0.2 9.8
In this example, we will not connect the serial port for now, and only simulate parsing these strings in a regular main().
1.2.Complete code
#include <print>
#include <sstream>
#include <string>
void parse_line(const std::string& line)
{
std::istringstream iss(line);
std::string type;
iss >> type;
if (type == "ODOM")
{
double x = 0.0;
double y = 0.0;
double yaw = 0.0;
iss >> x >> y >> yaw;
std::println("解析里程计:x = {},y = {},yaw = {}", x, y, yaw);
}
else if (type == "BAT")
{
double voltage = 0.0;
iss >> voltage;
std::println("解析电池电压:{} V", voltage);
}
else if (type == "IMU")
{
double ax = 0.0;
double ay = 0.0;
double az = 0.0;
iss >> ax >> ay >> az;
std::println("解析 IMU 加速度:{}, {}, {}", ax, ay, az);
}
else
{
std::println("未知协议行:{}", line);
}
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
std::println("main:开始模拟协议解析");
parse_line("ODOM 1.0 2.0 0.5");
parse_line("BAT 24.3");
parse_line("IMU 0.1 0.2 9.8");
parse_line("HELLO STM32");
std::println("main:结束");
// 返回 0 表示程序正常结束。
return 0;
}
Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.
1.3.Compile and run
g++ demo1_protocol_parse.cpp -o demo1_protocol_parse -std=c++23
./demo1_protocol_parse
1.4.Run output
main:开始模拟协议解析
解析里程计:x = 1,y = 2,yaw = 0.5
解析电池电压:24.3 V
解析 IMU 加速度:0.1, 0.2, 9.8
未知协议行:HELLO STM32
main:结束
1.5.Points to note in this example
Before connecting the serial port, first write and test the protocol parsing function separately. This makes debugging more efficient.
Don't start by immediately saying:
串口读取
协议解析
ROS2 发布
控制逻辑
All mixed together in one function.
2.Example 2: Encapsulating a robot protocol parser inside a class
2.1.Program Objectives
Refactor Example 1 into a class wrapper to prepare for connecting the serial port callback later.
2.2.Complete code
#include <print>
#include <sstream>
#include <string>
class RobotProtocolParser
{
public:
void parse_line(const std::string& line)
{
std::istringstream iss(line);
std::string type;
iss >> type;
if (type == "ODOM")
{
parse_odom(iss);
}
else if (type == "BAT")
{
parse_battery(iss);
}
else
{
std::println("未知协议行:{}", line);
}
}
private:
void parse_odom(std::istringstream& iss)
{
double x = 0.0;
double y = 0.0;
double yaw = 0.0;
iss >> x >> y >> yaw;
std::println("RobotProtocolParser:ODOM x = {},y = {},yaw = {}", x, y, yaw);
}
void parse_battery(std::istringstream& iss)
{
double voltage = 0.0;
iss >> voltage;
std::println("RobotProtocolParser:BAT voltage = {} V", voltage);
}
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
RobotProtocolParser parser;
std::println("main:开始解析");
parser.parse_line("ODOM 0.1 0.2 0.3");
parser.parse_line("BAT 23.8");
parser.parse_line("ERROR abc");
std::println("main:结束");
// 返回 0 表示程序正常结束。
return 0;
}
Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.
2.3.Compile and run
g++ demo2_protocol_parser_class.cpp -o demo2_protocol_parser_class -std=c++23
./demo2_protocol_parser_class
2.4.Run output
main:开始解析
RobotProtocolParser:ODOM x = 0.1,y = 0.2,yaw = 0.3
RobotProtocolParser:BAT voltage = 23.8 V
未知协议行:ERROR abc
main:结束
2.5.Points to note in this example
The protocol parser should not depend on Boost.Asio, nor should it depend on ROS2.
It is best if it is a pure C++ class. In this way, you can directly verify it with a normal main() or unit test.
3.Example 3: Asio serial port read + protocol parser combination
3.1.Program Objectives
Combine the preceding asynchronous serial port reading with the protocol parser:
串口收到一行
SerialRobotDriver::on_read() 被调用
取出字符串
交给 RobotProtocolParser 解析
继续注册下一次串口读取
3.2.Complete code
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <functional>
#include <print>
#include <istream>
#include <sstream>
#include <string>
class RobotProtocolParser
{
public:
void parse_line(const std::string& line)
{
std::istringstream iss(line);
std::string type;
iss >> type;
if (type == "ODOM")
{
double x = 0.0;
double y = 0.0;
double yaw = 0.0;
iss >> x >> y >> yaw;
std::println("解析 ODOM:x = {},y = {},yaw = {}", x, y, yaw);
}
else if (type == "BAT")
{
double voltage = 0.0;
iss >> voltage;
std::println("解析 BAT:{} V", voltage);
}
else
{
std::println("未知数据:{}", line);
}
}
};
class SerialRobotDriver
{
public:
SerialRobotDriver(boost::asio::io_context& io, const std::string& port_name)
: serial_(io)
{
boost::system::error_code ec;
serial_.open(port_name, ec);
if (ec)
{
std::println("打开串口失败:{}", ec.message());
return;
}
serial_.set_option(boost::asio::serial_port_base::baud_rate(115200));
std::println("SerialRobotDriver:串口打开成功");
start_read();
}
private:
void start_read()
{
boost::asio::async_read_until(serial_,
buffer_,
'\n',
std::bind(&SerialRobotDriver::on_read,
this,
std::placeholders::_1,
std::placeholders::_2));
}
void on_read(const boost::system::error_code& ec, std::size_t bytes_transferred)
{
if (ec)
{
std::println("串口读取错误:{}", ec.message());
return;
}
std::istream is(&buffer_);
std::string line;
std::getline(is, line);
std::println("收到串口行,bytes = {},line = {}", bytes_transferred, line);
parser_.parse_line(line);
start_read();
}
private:
// serial_port 表示串口设备,后续读写都通过它完成。
boost::asio::serial_port serial_;
boost::asio::streambuf buffer_;
RobotProtocolParser parser_;
};
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
std::string port_name = "/dev/pts/3";
if (argc >= 2)
{
port_name = argv[1];
}
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io;
SerialRobotDriver driver(io, port_name);
std::println("main:开始 io.run()");
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
io.run();
std::println("main:io.run() 返回");
return 0;
}
Running result : See “Running output and chronological order” below; if the example involves timers, threads, networks, or external devices, the specific timing and order may vary slightly depending on the environment.
3.3.Compile and run
Terminal 1: Create a virtual serial port.
socat -d -d pty,raw,echo=0 pty,raw,echo=0
Terminal 2: Run the program.
g++ demo3_serial_robot_driver.cpp -o demo3_serial_robot_driver -std=c++23 -lboost_system -pthread
./demo3_serial_robot_driver /dev/pts/3
Terminal 3: Send simulated lower computer data.
echo "ODOM 1.0 2.0 0.5" > /dev/pts/4
echo "BAT 24.2" > /dev/pts/4
3.4.Run output and chronological order
After the program starts, output:
SerialRobotDriver:串口打开成功
main:开始 io.run()
After sending the first line, output:
收到串口行,bytes = 17,line = ODOM 1.0 2.0 0.5
解析 ODOM:x = 1,y = 2,yaw = 0.5
Output after sending the second line:
收到串口行,bytes = 9,line = BAT 24.2
解析 BAT:24.2 V
3.5.Points to note in this example
This is a minimal prototype of a robot serial port driver.
But it has not done so yet:
- Serial port disconnect and reconnect;
- Binary frame parsing;
- CRC verification;
- Convert data to ROS2 topic;
- cmd_vel writes back to the lower computer;
- Multithread safety.
These can continue to be added on top of this structure.
4.Example 4: ROS2 Recommended structure for using Asio in a node
4.1.Program Objectives
This example demonstrates the recommended structure; you are not required to run it completely right now.
Recommended approach:
ROS2 节点线程:负责 rclcpp::spin()
Asio 线程:负责 io_context.run()
串口回调:收到数据后更新成员变量或 post 给 ROS2 逻辑
ROS2 timer:定期发布 odom / battery / imu
4.2.Code skeleton
#include <boost/asio.hpp>
#include <functional>
#include <print>
#include <memory>
#include <string>
#include <thread>
#include <rclcpp/rclcpp.hpp>
#include <std_msgs/msg/string.hpp>
class RobotNode : public rclcpp::Node
{
public:
RobotNode()
: Node("robot_node"),
guard_(boost::asio::make_work_guard(io_))
{
publisher_ = this->create_publisher<std_msgs::msg::String>("robot_status", 10);
ros_timer_ = this->create_wall_timer(
std::chrono::seconds(1),
std::bind(&RobotNode::on_ros_timer, this));
asio_thread_ = std::thread(std::bind(&RobotNode::run_asio, this));
std::println("RobotNode:启动完成");
}
~RobotNode()
{
guard_.reset();
io_.stop();
if (asio_thread_.joinable())
{
asio_thread_.join();
}
}
private:
void run_asio()
{
std::println("Asio 线程:io.run() 开始");
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
io_.run();
std::println("Asio 线程:io.run() 返回");
}
void on_ros_timer()
{
std_msgs::msg::String msg;
msg.data = "robot alive";
publisher_->publish(msg);
RCLCPP_INFO(this->get_logger(), "publish: %s", msg.data.c_str());
}
private:
// io_context 是 Asio 的事件循环对象,异步任务需要靠它调度。
boost::asio::io_context io_;
boost::asio::executor_work_guard<boost::asio::io_context::executor_type> guard_;
// 创建子线程,让这部分代码和 main 线程并发运行。
std::thread asio_thread_;
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr ros_timer_;
};
int main(int argc, char* argv[])
{
// 程序从 main 函数开始执行,argc/argv 用来接收命令行参数。
rclcpp::init(argc, argv);
std::shared_ptr<RobotNode> node = std::make_shared<RobotNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
4.3.Typical runtime output and chronological order
After the node starts, the output is similar to:
RobotNode:启动完成
Asio 线程:io.run() 开始
Afterwards ROS2 the timer outputs once every 1 second:
[INFO] [robot_node]: publish: robot alive
[INFO] [robot_node]: publish: robot alive
[INFO] [robot_node]: publish: robot alive
After exiting by pressing Ctrl+C:
Asio 线程:io.run() 返回
4.4.Points to note in this example
ROS2 There is also a timer:
create_wall_timer(...)
Boost.Asio also has a timer:
boost::asio::steady_timer
They are not the same thing.
General recommendations:
ROS2 周期发布 topic -> 用 ROS2 timer
串口/TCP/UDP 超时、重连、心跳 -> 用 Asio timer
4.5.Why should you not block on reading the serial port in the ROS2 callback?
Do not do this:
订阅 cmd_vel 的回调里
直接 while 循环 read 串口
Because the ROS2 executor may be blocked by you, other topics, timers, and services will all be affected.
A better approach is:
Asio 负责异步收发
ROS2 回调只把命令投递给 Asio 模块
5.Example 5: ROS2 deliver the task to Asio in the callback
5.1.Program Objectives
Show a common structure: ROS2 after receiving a command, it does not write directly to the serial port, but instead dispatches the write operation to the Asio thread.
5.2.Code snippet
void on_cmd_vel(const std_msgs::msg::String::SharedPtr msg)
{
std::string command = msg->data + "\n";
boost::asio::post(io_,
std::bind(&RobotNode::send_command_in_asio_thread,
this,
command));
}
void send_command_in_asio_thread(const std::string& command)
{
std::print("Asio 线程中发送命令:{}", command);
// 这里调用 SerialWriter::async_send(command)
// 或者调用你自己的串口驱动发送队列
}
5.3.Typical outputs and time sequence
When ROS2 receives a command:
cmd_vel 0.2 0.0
ROS2 The callback thread will immediately deliver the task. Then the Asio thread executes:
Asio 线程中发送命令:cmd_vel 0.2 0.0
5.4.Points to note in this example
boost::asio::post() Tasks can be safely delivered to the Asio event loop.
If your serial port object is only accessed within the Asio thread, then many locking issues can be reduced.
6.Recommended Hierarchical Layers in Robotic Engineering
Recommended structure:
RobotNode ROS2 节点层
├── SerialRobotDriver 串口通信层
│ ├── async_read_until 异步读取
│ ├── write_queue 异步写队列
│ └── steady_timer 超时/心跳/重连
├── RobotProtocolParser 协议解析层
└── RobotState 数据缓存层
Do not write:
一个 node.cpp 里面塞满:
串口打开
read
write
协议解析
odom 发布
cmd_vel 订阅
PID 控制
TF 发布
参数读取
This will make later maintenance extremely difficult.
7.Summary of Common Pitfalls
7.1.Pitfall 1:io.run() Early return
The reason is usually that there are no asynchronous tasks, and there is also no work_guard.
Solution:
auto guard = boost::asio::make_work_guard(io);
7.2.Pitfall 2: Callback Not Executed
Common causes:
- did not call
io.run(); io.run()has already returned;- the asynchronous object was destructed early;
- the buffer was destructed early;
- the serial port did not receive the delimiter, for example
async_read_until(..., '\n')but the lower-level device did not send a newline.
7.3.Pitfall 3: Incorrectly binding a member function in a class
Correct syntax:
std::bind(&ClassName::callback,
this,
std::placeholders::_1,
std::placeholders::_2)
If it is shared_ptr management lifecycle:
std::bind(&Session::on_read,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2)
7.4.Pitfall 4: During asynchronous writes, buffer is a local variable
Incorrect approach:
函数里创建 std::string
把它交给 async_write
函数返回后 string 销毁
异步写还没完成
Recommended:
用类成员变量
用 shared_ptr
用 write_queue
7.5.Pitfall 5: Treating TCP as packet-by-packet data
TCP is a byte stream and has no message boundaries. A robot's custom protocol must be designed with framing.
7.6.Pitfall 6: Assuming UDP is necessarily reliable
UDP may lose packets, reorder them, or duplicate them. Do not blindly rely on a single UDP transmission for important commands.
8.How can you continue to expand later?
After completing this set, you can continue with three small projects:
-
Serial lower-level protocol driver
The Linux host reads odom sent by STM32 via the Asio serial port and sends cmd_vel. -
TCP debug server
Write a TCP server so that a phone or computer can connect over the network and send debug commands to control the car. -
UDP heartbeat and status broadcast
The car broadcasts its own status once per second, and the debugging tool automatically discovers the device.
These three projects can all connect with ROS2, Nav2, ros2_control, and the robot chassis control.