Robot Engineering Approach and ROS2 Integration
The previous sections discussed Boost.Asio itself. This section discusses how to organize code when you are actually working on a robot project, especially the relationships between serial port drivers, TCP/UDP communication modules, and ROS2 nodes.
Core Principles:
不要在 ROS2 回调里写长时间阻塞的串口 read / TCP read。
通信模块应该自己用 Asio 异步运行。
ROS2 节点只负责发布、订阅、参数、生命周期和业务逻辑。
This section still uses std::bind.
Example 1: Simulating robot protocol parsing in a common main()
program objective
Many serial protocols for lower-level devices are a single line of text, for example:
ODOM 1.0 2.0 0.5
BAT 24.3
IMU 0.1 0.2 9.8
In this example, we won't connect the serial port yet, but will simulate parsing these strings in a regular main().
Full code
#include <iostream>
#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::cout << "解析里程计:x = " << x
<< ",y = " << y
<< ",yaw = " << yaw << std::endl;
}
else if (type == "BAT")
{
double voltage = 0.0;
iss >> voltage;
std::cout << "解析电池电压:" << voltage << " V" << std::endl;
}
else if (type == "IMU")
{
double ax = 0.0;
double ay = 0.0;
double az = 0.0;
iss >> ax >> ay >> az;
std::cout << "解析 IMU 加速度:"
<< ax << ", " << ay << ", " << az << std::endl;
}
else
{
std::cout << "未知协议行:" << line << std::endl;
}
}
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
std::cout << "main:开始模拟协议解析" << std::endl;
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::cout << "main:结束" << std::endl;
// 返回 0 表示程序正常结束。
return 0;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
Compile and run
g++ demo1_protocol_parse.cpp -o demo1_protocol_parse -std=c++17
./demo1_protocol_parse
Run output
main:开始模拟协议解析
解析里程计:x = 1,y = 2,yaw = 0.5
解析电池电压:24.3 V
解析 IMU 加速度:0.1, 0.2, 9.8
未知协议行:HELLO STM32
main:结束
Points to note for 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:
串口读取
协议解析
ROS2 发布
控制逻辑
All mixed together in one function.
Example 2: Encapsulating Robot Protocol Parser in a Class
program objective
Change Example 1 to a class encapsulation, to prepare for connecting serial port callbacks later.
Full code
#include <iostream>
#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::cout << "未知协议行:" << line << std::endl;
}
}
private:
void parse_odom(std::istringstream& iss)
{
double x = 0.0;
double y = 0.0;
double yaw = 0.0;
iss >> x >> y >> yaw;
std::cout << "RobotProtocolParser:ODOM x = " << x
<< ",y = " << y
<< ",yaw = " << yaw << std::endl;
}
void parse_battery(std::istringstream& iss)
{
double voltage = 0.0;
iss >> voltage;
std::cout << "RobotProtocolParser:BAT voltage = "
<< voltage << " V" << std::endl;
}
};
int main()
{
// 程序从 main 函数开始执行,下面的语句会按顺序运行。
RobotProtocolParser parser;
std::cout << "main:开始解析" << std::endl;
parser.parse_line("ODOM 0.1 0.2 0.3");
parser.parse_line("BAT 23.8");
parser.parse_line("ERROR abc");
std::cout << "main:结束" << std::endl;
// 返回 0 表示程序正常结束。
return 0;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
Compile and run
g++ demo2_protocol_parser_class.cpp -o demo2_protocol_parser_class -std=c++17
./demo2_protocol_parser_class
Run output
main:开始解析
RobotProtocolParser:ODOM x = 0.1,y = 0.2,yaw = 0.3
RobotProtocolParser:BAT voltage = 23.8 V
未知协议行:ERROR abc
main:结束
Points to note for this example
The protocol parser should not depend on Boost.Asio, nor on ROS2.
It’s best if it is a pure C++ class. That way, you can directly verify it with ordinary main() or unit tests.
Example 3: Asio Serial Port Read + Protocol Parser Combination
program objective
Combine the previous asynchronous serial port reading and protocol parser:
串口收到一行
SerialRobotDriver::on_read() 被调用
取出字符串
交给 RobotProtocolParser 解析
继续注册下一次串口读取
Full code
#include <boost/asio.hpp>
#include <boost/system/error_code.hpp>
#include <functional>
#include <iostream>
#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::cout << "解析 ODOM:x = " << x
<< ",y = " << y
<< ",yaw = " << yaw << std::endl;
}
else if (type == "BAT")
{
double voltage = 0.0;
iss >> voltage;
std::cout << "解析 BAT:" << voltage << " V" << std::endl;
}
else
{
std::cout << "未知数据:" << line << std::endl;
}
}
};
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::cout << "打开串口失败:" << ec.message() << std::endl;
return;
}
serial_.set_option(boost::asio::serial_port_base::baud_rate(115200));
std::cout << "SerialRobotDriver:串口打开成功" << std::endl;
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::cout << "串口读取错误:" << ec.message() << std::endl;
return;
}
std::istream is(&buffer_);
std::string line;
std::getline(is, line);
std::cout << "收到串口行,bytes = " << bytes_transferred
<< ",line = " << line << std::endl;
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::cout << "main:开始 io.run()" << std::endl;
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
io.run();
std::cout << "main:io.run() 返回" << std::endl;
return 0;
}
运行结果:见下方“运行输出与时间顺序”;如果示例涉及定时器、线程、网络或外部设备,具体时间和顺序可能会随环境略有变化。
Compile and run
Terminal 1: Create 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++17 -lboost_system -pthread
./demo3_serial_robot_driver /dev/pts/3
Terminal 3: Send simulated lower-level device data.
echo "ODOM 1.0 2.0 0.5" > /dev/pts/4
echo "BAT 24.2" > /dev/pts/4
Execution output and chronological order
After the program starts, it outputs:
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
I understand you're giving an instruction, but it seems you might be asking me to translate a Chinese Markdown fragment. However, "发送第二行后输出:" is not a Markdown fragment itself. Could you please provide the actual Chinese Markdown text you'd like me to translate into natural American English? I'll then follow the guidelines you provided.
收到串口行,bytes = 9,line = BAT 24.2
解析 BAT:24.2 V
Points to note for this example
This is a minimal robot serial port driver prototype.
But it hasn't been done yet:
- Serial port disconnect and reconnect;
- Binary frame parsing;
- CRC check;
- Convert data to ROS2 topic
- Write back cmd_vel to the electrical control;
- Thread safety.
These can be added further onto this structure.
Example 4: Recommended structure for using Asio in a ROS2 node
program objective
This example shows the recommended structure; it doesn't require you to run it fully right now.
Recommended Approach:
ROS2 节点线程:负责 rclcpp::spin()
Asio 线程:负责 io_context.run()
串口回调:收到数据后更新成员变量或 post 给 ROS2 逻辑
ROS2 timer:定期发布 odom / battery / imu
code skeleton
#include <boost/asio.hpp>
#include <functional>
#include <iostream>
#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::cout << "RobotNode:启动完成" << std::endl;
}
~RobotNode()
{
guard_.reset();
io_.stop();
if (asio_thread_.joinable())
{
asio_thread_.join();
}
}
private:
void run_asio()
{
std::cout << "Asio 线程:io.run() 开始" << std::endl;
// 启动事件循环,前面注册的异步任务会在这里被调度执行。
io_.run();
std::cout << "Asio 线程:io.run() 返回" << std::endl;
}
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;
}
Typical Runtime Output and Time Sequence
After the node starts, output similar to:
RobotNode:启动完成
Asio 线程:io.run() 开始
Afterwards, the ROS2 timer outputs once every second:
[INFO] [robot_node]: publish: robot alive
[INFO] [robot_node]: publish: robot alive
[INFO] [robot_node]: publish: robot alive
After pressing Ctrl+C to exit:
Asio 线程:io.run() 返回
Points to note for this example
ROS2 also has timer:
create_wall_timer(...)
Boost.Asio also has a timer:
boost::asio::steady_timer
They are not the same thing.
General Advice:
ROS2 周期发布 topic -> 用 ROS2 timer
串口/TCP/UDP 超时、重连、心跳 -> 用 Asio timer
Why you should not block reading the serial port inside a ROS2 callback
Don't be like this:
订阅 cmd_vel 的回调里
直接 while 循环 read 串口
Because the ROS2 executor may be blocked by you, other topics, timers, and services will be affected.
A better way is:
Asio 负责异步收发
ROS2 回调只把命令投递给 Asio 模块
Example 5: Submitting tasks to Asio in a ROS2 callback
program objective
Show a common structure: after ROS2 receives a command, it does not directly write to the serial port, but instead submits the write operation to the Asio thread.
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::cout << "Asio 线程中发送命令:" << command;
// 这里调用 SerialWriter::async_send(command)
// 或者调用你自己的串口驱动发送队列
}
Typical Output and Time Sequence
When ROS2 receives a command:
cmd_vel 0.2 0.0
The ROS2 callback thread immediately dispatches the task. Then the Asio thread executes:
Asio 线程中发送命令:cmd_vel 0.2 0.0
Points to note for this example
boost::asio::post() can safely deliver tasks to the Asio event loop.
If your serial port object is only accessed within the Asio thread, then many locking issues can be reduced.
Recommended Layering in Robot Engineering
Recommended structure:
RobotNode ROS2 节点层
├── SerialRobotDriver 串口通信层
│ ├── async_read_until 异步读取
│ ├── write_queue 异步写队列
│ └── steady_timer 超时/心跳/重连
├── RobotProtocolParser 协议解析层
└── RobotState 数据缓存层
Do not write as:
一个 node.cpp 里面塞满:
串口打开
read
write
协议解析
odom 发布
cmd_vel 订阅
PID 控制
TF 发布
参数读取
This will be very difficult to maintain later on.
Common Pitfalls
Pitfall 1: io.run() early return
The reason is usually that there is no asynchronous task, and there is no work_guard.
Solution:
auto guard = boost::asio::make_work_guard(io);
Pitfall 2: Callback Not Executing
Common causes:
- Did not call
io.run(); io.run()has returned;- Asynchronous object destructed prematurely;
- buffer destructed prematurely;
- The serial port did not receive the delimiter, for example
async_read_until(..., '\n'), but the microcontroller did not send a newline.
Pitfall 3: Incorrectly writing a bound member function in a class.
Correct writing:
std::bind(&ClassName::callback,
this,
std::placeholders::_1,
std::placeholders::_2)
If it is shared_ptr managing the lifecycle:
std::bind(&Session::on_read,
shared_from_this(),
std::placeholders::_1,
std::placeholders::_2)
Pitfall 4: Buffer is a local variable when writing asynchronously
Wrong approach:
函数里创建 std::string
把它交给 async_write
函数返回后 string 销毁
异步写还没完成
Recommended:
用类成员变量
用 shared_ptr
用 write_queue
Pitfall 5: Treating TCP as Packets of Data
TCP is a byte stream with no message boundaries. Custom robot protocols must implement framing.
Pitfall 6: UDP believed to be reliable
UDP may experience packet loss, out-of-order delivery, and duplication. Do not blindly rely on a single UDP transmission for important commands.
How can you continue to expand further?
After completing this set, you can move on to three small projects:
- Serial Peripheral Protocol Driver Linux host computer reads odom sent from STM32 via Asio serial port and sends cmd_vel.
- TCP Debug Server Write a TCP server, where a mobile phone or computer connects over the network to send debug commands to control the car.
- UDP Heartbeat and Status Broadcast The car broadcasts its status every 1 second, and the debugging tool automatically discovers the device.
These three projects can all interface with ROS2, Nav2, ros2_control, and small car chassis control.