在工业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之间的双向数据交互。

发布话题

发布话题

OPC UA 写入

OPC UA 订阅

发布话题

发布话题

ROS2 感知节点

ROS2 OPC UA 桥接节点

ROS2 导航节点

PLC OPC UA 服务器

ROS2 监控节点

ROS2 控制节点

PLC 逻辑程序

现场IO设备

架构说明

  • 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服务器,配置非常简单。以下是详细步骤:

  1. 创建PLC项目并设置IP地址

    • 打开TIA Portal V17/V18,新建项目
    • 添加S7-1200 CPU(如CPU 1214C DC/DC/DC,固件V4.5)
    • 在设备视图中,为CPU分配IP地址(如192.168.0.10)
  2. 激活OPC UA服务器

    • 选中CPU,在属性窗口中找到"OPC UA"→"服务器"
    • 勾选"激活OPC UA服务器"
    • 记录服务器地址:opc.tcp://192.168.0.10:4840
  3. 配置安全策略

    • 进入"OPC UA"→"安全"→"安全通道"
    • 勾选"Basic256Sha256-签名"和"Basic256Sha256-签名并加密"
    • 生成自签名证书或导入CA证书
  4. 创建数据块并暴露变量

    • 创建全局数据块DB1,命名为"RobotData"
    • 添加以下变量:
      • RobotSpeed:Int,机器人速度
      • RobotPositionX:Real,机器人X坐标
      • RobotPositionY:Real,机器人Y坐标
      • StartCommand:Bool,启动命令
      • EmergencyStop:Bool,急停信号
    • 勾选每个变量的"可从HMI/OPC UA访问"和"可从HMI/OPC UA写入"属性
  5. 下载程序并运行

    • 编译项目并下载到PLC
    • 将PLC切换到RUN模式
  6. 测试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 测试通信

  1. 查看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
    
  2. 向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 性能优化技巧

在工业应用中,通信的实时性和可靠性至关重要。以下是一些常用的优化技巧:

  1. 合理设置采样间隔和发布间隔

    • 关键控制数据:10-50ms采样间隔
    • 一般状态数据:100-200ms采样间隔
    • 非关键数据:500-1000ms采样间隔
  2. 使用订阅机制而非轮询

    • OPC UA的订阅机制只有在数据变化时才会推送数据,大大减少了网络流量和CPU占用
    • 避免使用周期性读取的方式获取数据
  3. 优化网络配置

    • 使用千兆以太网和工业交换机
    • 为控制数据分配独立的VLAN
    • 启用QoS(服务质量)机制,优先传输控制数据
  4. ROS2实时性优化

    • 使用PREEMPT_RT实时Linux内核
    • 配置CPU隔离,将ROS2节点绑定到特定CPU核心
    • 使用SCHED_FIFO调度策略
  5. OPC UA服务器优化

    • 合理设置最大会话数和订阅数
    • 优化缓冲区大小,避免数据丢失
    • 禁用不必要的安全策略,提高通信速度

5.2 常见问题排查

  1. 连接失败

    • 检查PLC和ROS2主机的IP地址是否在同一网段
    • 确认PLC的OPC UA服务器已激活并运行
    • 检查防火墙设置,确保4840端口已开放
    • 验证安全策略配置是否一致
  2. 变量读写失败

    • 检查节点路径是否正确(命名空间索引和标识符)
    • 确认变量已设置为"可从HMI/OPC UA访问"
    • 检查用户权限是否足够
    • 验证数据类型是否匹配
  3. 数据丢失或延迟过大

    • 检查网络带宽和延迟
    • 调整采样间隔和发布间隔
    • 优化PLC程序,减少CPU占用
    • 检查OPC UA服务器的负载情况
  4. 连接不稳定,频繁断开

    • 检查网络连接质量
    • 增加重连机制(本文代码已实现)
    • 调整会话超时时间
    • 检查PLC的OPC UA服务器日志

六、总结与展望

本文详细介绍了基于OPC UA的ROS2与西门子S7-1200 PLC通信的完整实现方案。通过封装open62541库,我们实现了一个功能完善的OPC UA客户端,并将其与ROS2系统集成,实现了双向数据交互。

该方案具有以下优点:

  • 标准化:采用工业标准OPC UA协议,兼容性好
  • 跨平台:支持多种操作系统和硬件平台
  • 安全性:内置完整的安全机制
  • 实时性:采用订阅机制,数据变化时主动推送
  • 可扩展性:可以轻松添加更多变量和功能

未来的工作可以包括:

  • 支持更多类型的PLC(如三菱、欧姆龙、AB等)
  • 实现复杂数据结构和方法的调用
  • 集成OPC UA PubSub机制,提高大规模系统的通信效率
  • 开发可视化配置工具,简化桥接节点的配置过程

希望本文能够帮助读者快速掌握ROS2与PLC通信的关键技术,为工业机器人系统的开发和落地提供参考。


👉 点击我的头像进入主页,关注专栏第一时间收到更新提醒,有问题评论区交流,看到都会回。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐