3、工业通信协议(下):OPC UA 协议基础、地址空间模型、安全机制、实战:使用 opcua-asyncio 库连接PLC并读写变量

3.1 OPC UA 到底是什么?

说实话,很多刚入行的朋友会把 OPC UA 和 OPC DA 搞混。我刚开始接触时也犯过这个错。

OPC UA 的全称是“统一架构”(Unified Architecture)。它跟老一代的 OPC DA 最大的区别是什么?

跨平台。 老 OPC 依赖 Windows 的 COM/DCOM 技术,换个 Linux 环境就抓瞎了。OPC UA 从底层就重新设计了,不依赖任何操作系统。

我在一个项目中遇到过,客户现场既有 Windows 上位机,又有 Linux 的边缘网关。如果用老 OPC,两边通信得折腾死。换成 OPC UA 后,一个库搞定所有平台。

核心特点:
  • 面向服务架构(SOA),不是简单的数据搬运
  • 内置安全机制,不是裸奔的 TCP
  • 自带地址空间,能描述数据的“含义”

3.2 地址空间模型——OPC UA 的灵魂

你想想看,传统工业通信里,我们读一个寄存器地址 40001,你知道它代表什么吗?不知道。你只能靠文档去查。

OPC UA 的地址空间模型,说白了就是给每个数据点都配了“身份证”。

它用 节点(Node)引用(Reference) 来组织信息。每个节点都有属性,比如:

  • NodeId:唯一标识,类似身份证号
  • BrowseName:人类可读的名字
  • Value:实际的数据值
  • DataType:数据类型(Int32、Float、String 等)

节点之间通过引用连接,形成一张巨大的信息网。你可以从“设备”节点,找到它的“温度传感器”,再找到“当前温度值”。

我的习惯: 在设计地址空间时,我通常会先画一张树状图。把设备、区域、变量层级理清楚,再动手编码。这样后期维护会省很多事。

3.3 安全机制——别让你的数据裸奔

工业网络一旦被攻击,后果很严重。我见过一个工厂,因为 OPC 服务暴露在公网上,被勒索软件加密了所有 PLC 程序……

OPC UA 的安全机制分三层:

安全层 作用 我的建议
传输层安全 加密通信通道,防止窃听 生产环境必须开启
应用层安全 用户认证、权限控制 至少用用户名+密码
消息层安全 签名和加密单个消息 高安全场景推荐

嗯,这里要注意:很多人在测试时为了方便,把安全模式设为 None。但上线前一定要改回来。我曾经吃过这个亏,测试环境跑得好好的,一上生产就被安全审计卡住了。

避坑指南: 我曾经在项目中直接复制了测试环境的配置文件到生产环境,结果安全证书没换,导致所有客户端连接失败。记住:生产环境一定要用正式的证书,不要用自签名的测试证书。

3.4 实战:用 opcua-asyncio 连接 PLC

理论说完了,咱们直接上手。我用的是 Python 的 opcua-asyncio 库,异步非阻塞,适合做高并发的工业网关。

3.4.1 安装库

pip install opcua-asyncio

就这么简单。没有复杂的依赖,跨平台兼容。

3.4.2 连接 PLC 并读取变量

假设你的 PLC 上有一个变量叫 ns=2;s=Temperature,我们把它读出来:

import asyncio
from opcua import Client

async def read_plc():
    # 创建客户端
    client = Client("opc.tcp://192.168.1.100:4840")
    
    try:
        # 连接
        await client.connect()
        print("连接成功!")
        
        # 获取节点
        node = client.get_node("ns=2;s=Temperature")
        
        # 读取值
        value = await node.read_value()
        print(f"当前温度: {value} °C")
        
    except Exception as e:
        print(f"连接失败: {e}")
    finally:
        await client.disconnect()

asyncio.run(read_plc())

你看,核心代码就几行。但实际项目中,我建议加上重连机制。工业网络不稳定,断线重连是基本功。

3.4.3 写入变量

写入和读取类似,只是把 read_value 换成 write_value

# 写入设定值
setpoint_node = client.get_node("ns=2;s=Setpoint")
await setpoint_node.write_value(100.0)  # 写入浮点数
print("写入成功!")
个人经验: 写入操作一定要做类型检查。我遇到过有人把字符串写进了 Float 类型的变量,结果 PLC 直接报错停机。建议写入前先读一下 DataType 属性,确认类型匹配。

3.4.4 订阅变量变化

轮询读取效率低,工业场景更推荐订阅模式:

class MySubHandler:
    def datachange_notification(self, node, val, data):
        print(f"节点 {node} 的值变为: {val}")

async def subscribe():
    client = Client("opc.tcp://192.168.1.100:4840")
    await client.connect()
    
    handler = MySubHandler()
    node = client.get_node("ns=2;s=Temperature")
    
    # 创建订阅,采样间隔100ms
    sub = await client.create_subscription(100, handler)
    await sub.subscribe_data_change(node)
    
    # 保持运行
    await asyncio.sleep(3600)
    
    await sub.delete()
    await client.disconnect()

为什么用订阅?说白了,就是让 PLC 主动推数据给你,而不是你每秒去问一次。这样网络压力小,实时性还高。

3.5 常见问题与排错

实战中你可能会遇到这些问题:

  • 连接超时:检查 IP 和端口,确认防火墙没拦截 4840 端口
  • 证书错误:生产环境用正式证书,测试环境可以暂时禁用安全验证
  • 节点不存在:用 UA Expert 工具浏览一下地址空间,确认 NodeId 写对了
重要提醒: 千万不要在生产环境直接修改 PLC 的变量值,除非你完全清楚后果。我建议先在测试环境验证所有写入逻辑。

好了,这一章的内容就到这里。OPC UA 其实还有很多高级特性,比如历史数据访问、报警与事件、方法调用等。但掌握了地址空间模型、安全机制和基本的读写操作,你已经能应对 80% 的工业自动化场景了。

下一章,我们会聊一聊 MQTT 在工业物联网中的应用。到时候见。