工业机器人与PLC无缝对接:ROS2+OPC UA通信完整实战
本文提出了一种基于OPC UA的ROS2与西门子PLC通信方案,解决了工业机器人系统中异构设备互联的关键问题。通过搭建三层架构(ROS2节点-OPC UA桥接-PLC服务器),实现了双向数据交互。详细介绍了环境配置、核心代码实现和性能优化方法,包括ROS2 Humble安装、open62541库编译、S7-1200 PLC的OPC UA服务器设置等关键技术环节。该方案具有跨平台、安全可靠、实时性高
在工业4.
0与智能制造的浪潮下,机器人系统与传统PLC控制系统的深度融合已成为必然趋势。ROS2作为新一代机器人操作系统,凭借其分布式架构、硬实时支持和多语言特性,正迅速成为工业机器人开发的首选平台。然而,ROS2与PLC之间的异构通信一直是工业落地的关键难点。
传统的通信方案如Modbus TCP、Profinet等,要么存在协议不统一、跨平台性差的问题,要么需要昂贵的专用网关。OPC UA(统一架构)作为工业自动化领域的事实标准,提供了安全、跨平台、语义丰富的通信机制,完美解决了ROS2与PLC之间的互联互通问题。
本文将从原理到落地,详细讲解基于OPC UA的ROS2与西门子S7-1200 PLC通信的完整实现方案,包括环境搭建、核心代码编写、性能优化和常见问题排查,帮助读者快速掌握这一关键技术。
一、技术选型与整体架构
1.1 为什么选择OPC UA?
在众多工业通信协议中,OPC UA脱颖而出,主要得益于以下核心优势:
- 跨平台性:不依赖于特定操作系统或硬件平台,支持Windows、Linux、VxWorks等多种系统
- 安全性:内置完整的安全模型,支持身份认证、数据加密和访问控制
- 语义丰富:通过信息模型可以表达复杂的数据结构、方法和事件
- 标准化:由OPC基金会制定,得到了几乎所有工业自动化厂商的支持
- 可扩展性:支持从简单的传感器数据采集到复杂的云边协同应用
与其他常见协议的对比:
| 协议 | 跨平台 | 安全性 | 语义表达 | 实时性 | 厂商支持 |
|---|---|---|---|---|---|
| OPC UA | ✅ 优秀 | ✅ 优秀 | ✅ 优秀 | ⭐⭐⭐⭐ | ✅ 广泛 |
| Modbus TCP | ✅ 良好 | ❌ 无 | ❌ 差 | ⭐⭐⭐ | ✅ 广泛 |
| Profinet | ❌ 差 | ⭐⭐ | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
| EtherCAT | ❌ 差 | ❌ 无 | ⭐⭐ | ⭐⭐⭐⭐⭐ | ⭐⭐⭐ |
1.2 整体架构设计
本文采用"ROS2节点+OPC UA客户端+PLC OPC UA服务器"的三层架构,实现ROS2与PLC之间的双向数据交互。
架构说明:
- PLC端:启用内置的OPC UA服务器,将需要交互的变量暴露到地址空间
- ROS2端:开发一个桥接节点,集成OPC UA客户端功能
- 数据流向:
- PLC→ROS2:OPC UA订阅机制实时获取PLC数据,转换为ROS2话题发布
- ROS2→PLC:订阅ROS2控制话题,通过OPC UA写入PLC变量
这种架构的优势在于:
- 解耦性好:ROS2系统与PLC系统通过标准协议通信,互不影响
- 实时性高:采用OPC UA订阅机制,数据变化时主动推送
- 可扩展性强:可以轻松添加多个ROS2节点或PLC设备
- 安全性高:利用OPC UA的安全机制保障通信安全
二、环境准备
2.1 ROS2环境搭建
本文以ROS2 Humble Hawksbill版本为例,运行在Ubuntu 22.04 LTS系统上。如果还没有安装ROS2,可以按照官方文档进行安装:
# 设置locale
sudo locale-gen en_US en_US.UTF-8
sudo update-locale LC_ALL=en_US.UTF-8 LANG=en_US.UTF-8
export LANG=en_US.UTF-8
# 添加ROS2源
sudo apt update && sudo apt install curl gnupg lsb-release
sudo curl -sSL https://raw.githubusercontent.com/ros/rosdistro/master/ros.key -o /usr/share/keyrings/ros-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/ros-archive-keyring.gpg] http://packages.ros.org/ros2/ubuntu $(source /etc/os-release && echo $UBUNTU_CODENAME) main" | sudo tee /etc/apt/sources.list.d/ros2.list > /dev/null
# 安装ROS2 Humble
sudo apt update
sudo apt install ros-humble-desktop
sudo apt install ros-humble-rclcpp ros-humble-std-msgs
# 环境配置
echo "source /opt/ros/humble/setup.bash" >> ~/.bashrc
source ~/.bashrc
2.2 open62541库安装
open62541是目前最活跃、功能最完备的开源OPC UA协议栈,采用C99编写,支持跨平台部署。我们将使用它来实现OPC UA客户端功能。
# 安装依赖
sudo apt install git cmake build-essential libssl-dev
# 克隆open62541源码(推荐使用v1.4.0稳定版)
git clone https://github.com/open62541/open62541.git
cd open62541
git checkout v1.4.0
# 创建构建目录
mkdir build && cd build
# 编译安装
cmake -DCMAKE_BUILD_TYPE=Release -DUA_BUILD_EXAMPLES=OFF -DUA_BUILD_TOOLS=OFF ..
make -j$(nproc)
sudo make install
2.3 西门子S7-1200 PLC端配置
S7-1200从固件V4.4版本开始内置OPC UA服务器,配置非常简单。以下是详细步骤:
-
创建PLC项目并设置IP地址
- 打开TIA Portal V17/V18,新建项目
- 添加S7-1200 CPU(如CPU 1214C DC/DC/DC,固件V4.5)
- 在设备视图中,为CPU分配IP地址(如192.168.0.10)
-
激活OPC UA服务器
- 选中CPU,在属性窗口中找到"OPC UA"→"服务器"
- 勾选"激活OPC UA服务器"
- 记录服务器地址:
opc.tcp://192.168.0.10:4840
-
配置安全策略
- 进入"OPC UA"→"安全"→"安全通道"
- 勾选"Basic256Sha256-签名"和"Basic256Sha256-签名并加密"
- 生成自签名证书或导入CA证书
-
创建数据块并暴露变量
- 创建全局数据块DB1,命名为"RobotData"
- 添加以下变量:
RobotSpeed:Int,机器人速度RobotPositionX:Real,机器人X坐标RobotPositionY:Real,机器人Y坐标StartCommand:Bool,启动命令EmergencyStop:Bool,急停信号
- 勾选每个变量的"可从HMI/OPC UA访问"和"可从HMI/OPC UA写入"属性
-
下载程序并运行
- 编译项目并下载到PLC
- 将PLC切换到RUN模式
-
测试OPC UA服务器
- 使用UaExpert客户端连接PLC的OPC UA服务器
- 浏览地址空间,确认可以看到"RobotData"数据块中的变量
- 尝试读写变量,验证通信正常
三、核心代码实现
3.1 OPC UA客户端封装
首先,我们封装一个OPC UA客户端类,提供连接、断开、读写变量和订阅数据变化的功能。
创建文件include/ros2_opcua_bridge/opcua_client.h:
#ifndef ROS2_OPCUA_BRIDGE__OPCUA_CLIENT_H_
#define ROS2_OPCUA_BRIDGE__OPCUA_CLIENT_H_
#include <open62541/client_config_default.h>
#include <open62541/client_highlevel.h>
#include <open62541/plugin/log_stdout.h>
#include <string>
#include <functional>
#include <map>
#include <mutex>
class OpcUaClient {
public:
OpcUaClient();
~OpcUaClient();
// 连接到OPC UA服务器
bool connect(const std::string& endpoint_url,
const std::string& username = "",
const std::string& password = "");
// 断开连接
void disconnect();
// 检查是否已连接
bool isConnected() const;
// 读取变量值
template<typename T>
bool readValue(const std::string& node_path, T& value);
// 写入变量值
template<typename T>
bool writeValue(const std::string& node_path, const T& value);
// 订阅变量变化
typedef std::function<void(const UA_Variant*)> DataChangeCallback;
bool subscribeToDataChange(const std::string& node_path, DataChangeCallback callback);
private:
UA_Client* client_;
bool connected_;
std::mutex client_mutex_;
std::map<std::string, UA_NodeId> node_id_cache_;
// 根据节点路径获取NodeId
UA_NodeId getNodeIdFromPath(const std::string& node_path);
// 数据变化回调函数
static void dataChangeCallback(UA_Client* client, UA_UInt32 subId, void* subContext,
UA_UInt32 monId, void* monContext, UA_DataValue* value);
};
#endif // ROS2_OPCUA_BRIDGE__OPCUA_CLIENT_H_
实现文件src/opcua_client.cpp:
#include "ros2_opcua_bridge/opcua_client.h"
#include <sstream>
#include <vector>
OpcUaClient::OpcUaClient() : client_(nullptr), connected_(false) {
client_ = UA_Client_new();
UA_ClientConfig* config = UA_Client_getConfig(client_);
UA_ClientConfig_setDefault(config);
config->timeout = 5000; // 5秒超时
}
OpcUaClient::~OpcUaClient() {
if (connected_) {
disconnect();
}
UA_Client_delete(client_);
}
bool OpcUaClient::connect(const std::string& endpoint_url,
const std::string& username,
const std::string& password) {
std::lock_guard<std::mutex> lock(client_mutex_);
if (connected_) {
return true;
}
UA_StatusCode status;
if (username.empty() && password.empty()) {
// 匿名登录
status = UA_Client_connect(client_, endpoint_url.c_str());
} else {
// 用户名密码登录
status = UA_Client_connectUsername(client_, endpoint_url.c_str(),
username.c_str(), password.c_str());
}
if (status != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"连接OPC UA服务器失败: %s", UA_StatusCode_name(status));
return false;
}
connected_ = true;
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "成功连接到OPC UA服务器");
return true;
}
void OpcUaClient::disconnect() {
std::lock_guard<std::mutex> lock(client_mutex_);
if (!connected_) {
return;
}
UA_Client_disconnect(client_);
connected_ = false;
node_id_cache_.clear();
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "已断开与OPC UA服务器的连接");
}
bool OpcUaClient::isConnected() const {
return connected_;
}
UA_NodeId OpcUaClient::getNodeIdFromPath(const std::string& node_path) {
// 检查缓存
auto it = node_id_cache_.find(node_path);
if (it != node_id_cache_.end()) {
return it->second;
}
// 解析节点路径(格式:ns=2;s=PLC1.DB1.RobotSpeed)
std::vector<std::string> parts;
std::stringstream ss(node_path);
std::string part;
while (std::getline(ss, part, ';')) {
parts.push_back(part);
}
if (parts.size() != 2) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "无效的节点路径格式: %s", node_path.c_str());
return UA_NODEID_NULL;
}
// 解析命名空间索引
int ns_index;
if (sscanf(parts[0].c_str(), "ns=%d", &ns_index) != 1) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "无效的命名空间格式: %s", parts[0].c_str());
return UA_NODEID_NULL;
}
// 解析标识符
std::string identifier = parts[1].substr(2); // 去掉"s="前缀
UA_NodeId node_id = UA_NODEID_STRING(ns_index, identifier.c_str());
node_id_cache_[node_path] = node_id;
return node_id;
}
// 模板特化实现
template<>
bool OpcUaClient::readValue<bool>(const std::string& node_path, bool& value) {
std::lock_guard<std::mutex> lock(client_mutex_);
if (!connected_) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "未连接到OPC UA服务器");
return false;
}
UA_NodeId node_id = getNodeIdFromPath(node_path);
if (UA_NodeId_isNull(&node_id)) {
return false;
}
UA_Variant* var = UA_Variant_new();
UA_StatusCode status = UA_Client_readValueAttribute(client_, node_id, var);
if (status != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"读取变量失败: %s, 错误: %s", node_path.c_str(), UA_StatusCode_name(status));
UA_Variant_delete(var);
return false;
}
if (!UA_Variant_isScalar(var) || var->type != &UA_TYPES[UA_TYPES_BOOLEAN]) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "变量类型不匹配");
UA_Variant_delete(var);
return false;
}
value = *(UA_Boolean*)var->data;
UA_Variant_delete(var);
return true;
}
template<>
bool OpcUaClient::readValue<int>(const std::string& node_path, int& value) {
std::lock_guard<std::mutex> lock(client_mutex_);
if (!connected_) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "未连接到OPC UA服务器");
return false;
}
UA_NodeId node_id = getNodeIdFromPath(node_path);
if (UA_NodeId_isNull(&node_id)) {
return false;
}
UA_Variant* var = UA_Variant_new();
UA_StatusCode status = UA_Client_readValueAttribute(client_, node_id, var);
if (status != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"读取变量失败: %s, 错误: %s", node_path.c_str(), UA_StatusCode_name(status));
UA_Variant_delete(var);
return false;
}
if (!UA_Variant_isScalar(var) || var->type != &UA_TYPES[UA_TYPES_INT32]) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "变量类型不匹配");
UA_Variant_delete(var);
return false;
}
value = *(UA_Int32*)var->data;
UA_Variant_delete(var);
return true;
}
template<>
bool OpcUaClient::readValue<float>(const std::string& node_path, float& value) {
std::lock_guard<std::mutex> lock(client_mutex_);
if (!connected_) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "未连接到OPC UA服务器");
return false;
}
UA_NodeId node_id = getNodeIdFromPath(node_path);
if (UA_NodeId_isNull(&node_id)) {
return false;
}
UA_Variant* var = UA_Variant_new();
UA_StatusCode status = UA_Client_readValueAttribute(client_, node_id, var);
if (status != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"读取变量失败: %s, 错误: %s", node_path.c_str(), UA_StatusCode_name(status));
UA_Variant_delete(var);
return false;
}
if (!UA_Variant_isScalar(var) || var->type != &UA_TYPES[UA_TYPES_FLOAT]) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "变量类型不匹配");
UA_Variant_delete(var);
return false;
}
value = *(UA_Float*)var->data;
UA_Variant_delete(var);
return true;
}
template<>
bool OpcUaClient::writeValue<bool>(const std::string& node_path, const bool& value) {
std::lock_guard<std::mutex> lock(client_mutex_);
if (!connected_) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "未连接到OPC UA服务器");
return false;
}
UA_NodeId node_id = getNodeIdFromPath(node_path);
if (UA_NodeId_isNull(&node_id)) {
return false;
}
UA_Variant var;
UA_Variant_init(&var);
UA_Boolean ua_value = value ? UA_TRUE : UA_FALSE;
UA_Variant_setScalar(&var, &ua_value, &UA_TYPES[UA_TYPES_BOOLEAN]);
UA_StatusCode status = UA_Client_writeValueAttribute(client_, node_id, &var);
if (status != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"写入变量失败: %s, 错误: %s", node_path.c_str(), UA_StatusCode_name(status));
return false;
}
return true;
}
template<>
bool OpcUaClient::writeValue<int>(const std::string& node_path, const int& value) {
std::lock_guard<std::mutex> lock(client_mutex_);
if (!connected_) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "未连接到OPC UA服务器");
return false;
}
UA_NodeId node_id = getNodeIdFromPath(node_path);
if (UA_NodeId_isNull(&node_id)) {
return false;
}
UA_Variant var;
UA_Variant_init(&var);
UA_Int32 ua_value = value;
UA_Variant_setScalar(&var, &ua_value, &UA_TYPES[UA_TYPES_INT32]);
UA_StatusCode status = UA_Client_writeValueAttribute(client_, node_id, &var);
if (status != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"写入变量失败: %s, 错误: %s", node_path.c_str(), UA_StatusCode_name(status));
return false;
}
return true;
}
template<>
bool OpcUaClient::writeValue<float>(const std::string& node_path, const float& value) {
std::lock_guard<std::mutex> lock(client_mutex_);
if (!connected_) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "未连接到OPC UA服务器");
return false;
}
UA_NodeId node_id = getNodeIdFromPath(node_path);
if (UA_NodeId_isNull(&node_id)) {
return false;
}
UA_Variant var;
UA_Variant_init(&var);
UA_Float ua_value = value;
UA_Variant_setScalar(&var, &ua_value, &UA_TYPES[UA_TYPES_FLOAT]);
UA_StatusCode status = UA_Client_writeValueAttribute(client_, node_id, &var);
if (status != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"写入变量失败: %s, 错误: %s", node_path.c_str(), UA_StatusCode_name(status));
return false;
}
return true;
}
void OpcUaClient::dataChangeCallback(UA_Client* client, UA_UInt32 subId, void* subContext,
UA_UInt32 monId, void* monContext, UA_DataValue* value) {
auto callback = static_cast<DataChangeCallback*>(monContext);
if (callback && UA_DataValue_hasValue(value)) {
(*callback)(&value->value);
}
}
bool OpcUaClient::subscribeToDataChange(const std::string& node_path, DataChangeCallback callback) {
std::lock_guard<std::mutex> lock(client_mutex_);
if (!connected_) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "未连接到OPC UA服务器");
return false;
}
UA_NodeId node_id = getNodeIdFromPath(node_path);
if (UA_NodeId_isNull(&node_id)) {
return false;
}
// 创建订阅
UA_CreateSubscriptionRequest request = UA_CreateSubscriptionRequest_default();
request.requestedPublishingInterval = 100.0; // 100ms发布间隔
request.requestedMaxKeepAliveCount = 10;
request.requestedLifetimeCount = 30;
UA_CreateSubscriptionResponse response =
UA_Client_Subscriptions_create(client_, request, nullptr, nullptr, nullptr);
if (response.responseHeader.serviceResult != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"创建订阅失败: %s", UA_StatusCode_name(response.responseHeader.serviceResult));
return false;
}
UA_UInt32 subscriptionId = response.subscriptionId;
// 创建监控项
UA_MonitoredItemCreateRequest monRequest = UA_MonitoredItemCreateRequest_default(node_id);
monRequest.requestedParameters.samplingInterval = 100.0; // 100ms采样间隔
monRequest.requestedParameters.queueSize = 1;
monRequest.requestedParameters.discardOldest = UA_TRUE;
// 保存回调函数
DataChangeCallback* callbackPtr = new DataChangeCallback(callback);
UA_MonitoredItemCreateResult monResponse =
UA_Client_MonitoredItems_createDataChange(client_, subscriptionId,
UA_TIMESTAMPSTORETURN_BOTH,
monRequest, callbackPtr,
dataChangeCallback, nullptr);
if (monResponse.statusCode != UA_STATUSCODE_GOOD) {
UA_LOG_ERROR(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT,
"创建监控项失败: %s", UA_StatusCode_name(monResponse.statusCode));
delete callbackPtr;
UA_Client_Subscriptions_delete(client_, 1, &subscriptionId);
return false;
}
UA_LOG_INFO(UA_Log_Stdout, UA_LOGCATEGORY_CLIENT, "成功订阅变量: %s", node_path.c_str());
return true;
}
3.2 ROS2桥接节点实现
接下来,我们实现ROS2桥接节点,将OPC UA客户端与ROS2系统集成起来。
创建文件src/ros2_opcua_bridge_node.cpp:
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/bool.hpp"
#include "std_msgs/msg/int32.hpp"
#include "std_msgs/msg/float32.hpp"
#include "ros2_opcua_bridge/opcua_client.h"
#include <thread>
#include <chrono>
class Ros2OpcUaBridgeNode : public rclcpp::Node {
public:
Ros2OpcUaBridgeNode() : Node("ros2_opcua_bridge_node") {
// 声明参数
this->declare_parameter<std::string>("opcua_endpoint", "opc.tcp://192.168.0.10:4840");
this->declare_parameter<std::string>("opcua_username", "");
this->declare_parameter<std::string>("opcua_password", "");
this->declare_parameter<int>("reconnect_interval", 5000); // 重连间隔,单位ms
// 获取参数
opcua_endpoint_ = this->get_parameter("opcua_endpoint").as_string();
opcua_username_ = this->get_parameter("opcua_username").as_string();
opcua_password_ = this->get_parameter("opcua_password").as_string();
reconnect_interval_ = this->get_parameter("reconnect_interval").as_int();
// 创建发布者
robot_speed_pub_ = this->create_publisher<std_msgs::msg::Int32>("plc/robot_speed", 10);
robot_position_x_pub_ = this->create_publisher<std_msgs::msg::Float32>("plc/robot_position_x", 10);
robot_position_y_pub_ = this->create_publisher<std_msgs::msg::Float32>("plc/robot_position_y", 10);
emergency_stop_pub_ = this->create_publisher<std_msgs::msg::Bool>("plc/emergency_stop", 10);
// 创建订阅者
start_command_sub_ = this->create_subscription<std_msgs::msg::Bool>(
"ros2/start_command", 10,
std::bind(&Ros2OpcUaBridgeNode::startCommandCallback, this, std::placeholders::_1));
// 初始化OPC UA客户端
opcua_client_ = std::make_shared<OpcUaClient>();
// 启动连接线程
connection_thread_ = std::thread(&Ros2OpcUaBridgeNode::connectionLoop, this);
RCLCPP_INFO(this->get_logger(), "ROS2 OPC UA桥接节点已启动");
}
~Ros2OpcUaBridgeNode() {
running_ = false;
if (connection_thread_.joinable()) {
connection_thread_.join();
}
opcua_client_->disconnect();
}
private:
void connectionLoop() {
while (running_) {
if (!opcua_client_->isConnected()) {
RCLCPP_INFO(this->get_logger(), "尝试连接到OPC UA服务器: %s", opcua_endpoint_.c_str());
if (opcua_client_->connect(opcua_endpoint_, opcua_username_, opcua_password_)) {
// 连接成功,订阅PLC变量
setupSubscriptions();
} else {
RCLCPP_ERROR(this->get_logger(), "连接失败,%d秒后重试...", reconnect_interval_ / 1000);
}
}
// 处理OPC UA异步事件
if (opcua_client_->isConnected()) {
UA_Client_run_iterate(opcua_client_->getClient(), 100);
} else {
std::this_thread::sleep_for(std::chrono::milliseconds(reconnect_interval_));
}
}
}
void setupSubscriptions() {
// 订阅机器人速度
opcua_client_->subscribeToDataChange("ns=2;s=PLC1.DB1.RobotSpeed",
[this](const UA_Variant* value) {
if (value->type == &UA_TYPES[UA_TYPES_INT32]) {
auto msg = std_msgs::msg::Int32();
msg.data = *(UA_Int32*)value->data;
robot_speed_pub_->publish(msg);
RCLCPP_DEBUG(this->get_logger(), "收到机器人速度: %d", msg.data);
}
});
// 订阅机器人X坐标
opcua_client_->subscribeToDataChange("ns=2;s=PLC1.DB1.RobotPositionX",
[this](const UA_Variant* value) {
if (value->type == &UA_TYPES[UA_TYPES_FLOAT]) {
auto msg = std_msgs::msg::Float32();
msg.data = *(UA_Float*)value->data;
robot_position_x_pub_->publish(msg);
RCLCPP_DEBUG(this->get_logger(), "收到机器人X坐标: %.2f", msg.data);
}
});
// 订阅机器人Y坐标
opcua_client_->subscribeToDataChange("ns=2;s=PLC1.DB1.RobotPositionY",
[this](const UA_Variant* value) {
if (value->type == &UA_TYPES[UA_TYPES_FLOAT]) {
auto msg = std_msgs::msg::Float32();
msg.data = *(UA_Float*)value->data;
robot_position_y_pub_->publish(msg);
RCLCPP_DEBUG(this->get_logger(), "收到机器人Y坐标: %.2f", msg.data);
}
});
// 订阅急停信号
opcua_client_->subscribeToDataChange("ns=2;s=PLC1.DB1.EmergencyStop",
[this](const UA_Variant* value) {
if (value->type == &UA_TYPES[UA_TYPES_BOOLEAN]) {
auto msg = std_msgs::msg::Bool();
msg.data = *(UA_Boolean*)value->data ? true : false;
emergency_stop_pub_->publish(msg);
if (msg.data) {
RCLCPP_WARN(this->get_logger(), "收到急停信号!");
}
}
});
RCLCPP_INFO(this->get_logger(), "所有变量订阅成功");
}
void startCommandCallback(const std_msgs::msg::Bool::SharedPtr msg) {
if (opcua_client_->isConnected()) {
bool success = opcua_client_->writeValue<bool>("ns=2;s=PLC1.DB1.StartCommand", msg->data);
if (success) {
RCLCPP_INFO(this->get_logger(), "发送启动命令: %s", msg->data ? "true" : "false");
} else {
RCLCPP_ERROR(this->get_logger(), "发送启动命令失败");
}
} else {
RCLCPP_ERROR(this->get_logger(), "未连接到OPC UA服务器,无法发送命令");
}
}
std::shared_ptr<OpcUaClient> opcua_client_;
std::string opcua_endpoint_;
std::string opcua_username_;
std::string opcua_password_;
int reconnect_interval_;
std::thread connection_thread_;
bool running_ = true;
// 发布者
rclcpp::Publisher<std_msgs::msg::Int32>::SharedPtr robot_speed_pub_;
rclcpp::Publisher<std_msgs::msg::Float32>::SharedPtr robot_position_x_pub_;
rclcpp::Publisher<std_msgs::msg::Float32>::SharedPtr robot_position_y_pub_;
rclcpp::Publisher<std_msgs::msg::Bool>::SharedPtr emergency_stop_pub_;
// 订阅者
rclcpp::Subscription<std_msgs::msg::Bool>::SharedPtr start_command_sub_;
};
int main(int argc, char* argv[]) {
rclcpp::init(argc, argv);
auto node = std::make_shared<Ros2OpcUaBridgeNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
3.3 构建配置
创建CMakeLists.txt:
cmake_minimum_required(VERSION 3.8)
project(ros2_opcua_bridge)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic)
endif()
# 查找依赖
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
find_package(open62541 REQUIRED)
# 包含头文件
include_directories(include)
# 编译库
add_library(opcua_client SHARED src/opcua_client.cpp)
ament_target_dependencies(opcua_client rclcpp std_msgs)
target_link_libraries(opcua_client open62541)
# 编译节点
add_executable(ros2_opcua_bridge_node src/ros2_opcua_bridge_node.cpp)
ament_target_dependencies(ros2_opcua_bridge_node rclcpp std_msgs)
target_link_libraries(ros2_opcua_bridge_node opcua_client open62541)
# 安装
install(TARGETS
opcua_client
ros2_opcua_bridge_node
DESTINATION lib/${PROJECT_NAME})
install(DIRECTORY include/
DESTINATION include)
install(DIRECTORY launch
DESTINATION share/${PROJECT_NAME})
ament_package()
创建package.xml:
<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format3.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="3">
<name>ros2_opcua_bridge</name>
<version>0.0.0</version>
<description>ROS2与PLC通信的OPC UA桥接节点</description>
<maintainer email="your_email@example.com">Your Name</maintainer>
<license>Apache-2.0</license>
<buildtool_depend>ament_cmake</buildtool_depend>
<depend>rclcpp</depend>
<depend>std_msgs</depend>
<depend>open62541</depend>
<test_depend>ament_lint_auto</test_depend>
<test_depend>ament_lint_common</test_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
四、编译与运行
4.1 编译工作空间
# 创建工作空间
mkdir -p ~/ros2_opcua_ws/src
cd ~/ros2_opcua_ws/src
# 将上述代码复制到ros2_opcua_bridge包中
# 然后回到工作空间根目录
cd ~/ros2_opcua_ws
# 编译
colcon build --packages-select ros2_opcua_bridge
# 环境配置
source install/setup.bash
4.2 运行桥接节点
# 运行节点,指定OPC UA服务器地址
ros2 run ros2_opcua_bridge ros2_opcua_bridge_node --ros-args \
-p opcua_endpoint:="opc.tcp://192.168.0.10:4840"
4.3 测试通信
-
查看PLC发布的话题:
# 查看机器人速度 ros2 topic echo /plc/robot_speed # 查看机器人位置 ros2 topic echo /plc/robot_position_x ros2 topic echo /plc/robot_position_y # 查看急停信号 ros2 topic echo /plc/emergency_stop -
向PLC发送启动命令:
# 发送启动命令 ros2 topic pub /ros2/start_command std_msgs/msg/Bool "{data: true}" # 发送停止命令 ros2 topic pub /ros2/start_command std_msgs/msg/Bool "{data: false}"
五、性能优化与问题排查
5.1 性能优化技巧
在工业应用中,通信的实时性和可靠性至关重要。以下是一些常用的优化技巧:
-
合理设置采样间隔和发布间隔
- 关键控制数据:10-50ms采样间隔
- 一般状态数据:100-200ms采样间隔
- 非关键数据:500-1000ms采样间隔
-
使用订阅机制而非轮询
- OPC UA的订阅机制只有在数据变化时才会推送数据,大大减少了网络流量和CPU占用
- 避免使用周期性读取的方式获取数据
-
优化网络配置
- 使用千兆以太网和工业交换机
- 为控制数据分配独立的VLAN
- 启用QoS(服务质量)机制,优先传输控制数据
-
ROS2实时性优化
- 使用PREEMPT_RT实时Linux内核
- 配置CPU隔离,将ROS2节点绑定到特定CPU核心
- 使用SCHED_FIFO调度策略
-
OPC UA服务器优化
- 合理设置最大会话数和订阅数
- 优化缓冲区大小,避免数据丢失
- 禁用不必要的安全策略,提高通信速度
5.2 常见问题排查
-
连接失败
- 检查PLC和ROS2主机的IP地址是否在同一网段
- 确认PLC的OPC UA服务器已激活并运行
- 检查防火墙设置,确保4840端口已开放
- 验证安全策略配置是否一致
-
变量读写失败
- 检查节点路径是否正确(命名空间索引和标识符)
- 确认变量已设置为"可从HMI/OPC UA访问"
- 检查用户权限是否足够
- 验证数据类型是否匹配
-
数据丢失或延迟过大
- 检查网络带宽和延迟
- 调整采样间隔和发布间隔
- 优化PLC程序,减少CPU占用
- 检查OPC UA服务器的负载情况
-
连接不稳定,频繁断开
- 检查网络连接质量
- 增加重连机制(本文代码已实现)
- 调整会话超时时间
- 检查PLC的OPC UA服务器日志
六、总结与展望
本文详细介绍了基于OPC UA的ROS2与西门子S7-1200 PLC通信的完整实现方案。通过封装open62541库,我们实现了一个功能完善的OPC UA客户端,并将其与ROS2系统集成,实现了双向数据交互。
该方案具有以下优点:
- 标准化:采用工业标准OPC UA协议,兼容性好
- 跨平台:支持多种操作系统和硬件平台
- 安全性:内置完整的安全机制
- 实时性:采用订阅机制,数据变化时主动推送
- 可扩展性:可以轻松添加更多变量和功能
未来的工作可以包括:
- 支持更多类型的PLC(如三菱、欧姆龙、AB等)
- 实现复杂数据结构和方法的调用
- 集成OPC UA PubSub机制,提高大规模系统的通信效率
- 开发可视化配置工具,简化桥接节点的配置过程
希望本文能够帮助读者快速掌握ROS2与PLC通信的关键技术,为工业机器人系统的开发和落地提供参考。
👉 点击我的头像进入主页,关注专栏第一时间收到更新提醒,有问题评论区交流,看到都会回。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐


所有评论(0)