西门子 S7 PLC 的通信是工业自动化中数据交互的核心,涉及通信协议、硬件接口、通信对象、编程实现等关键知识点,以下从核心协议、通信方式、硬件接口、关键参数、编程实现、常见问题 6 个维度,系统梳理核心知识点,帮助快速掌握 S7 PLC 通信逻辑:
一、核心通信协议(S7 PLC 的 "语言")
协议是 PLC 与其他设备(电脑、触摸屏、变频器、上位机)通信的规则,西门子 S7 PLC 主流协议如下:
|------------------------|------------------------------------------|-----------------------------------------|-----------------------|
| 协议名称 | 适用场景 | 核心特点 | 典型应用 |
| S7 协议(S7 Comm) | PLC 与编程软件、上位机(C#/Python)、其他 S7 PLC 通信 | 西门子私有协议,支持读写 DB 块、I/O、M 区,速度快、兼容性强 | TIA Portal 调试、上位机数据采集 |
| Profinet | 工业以太网现场总线,PLC 与分布式 I/O(如 ET200SP)、智能设备通信 | 实时性高(支持 RT/IRT 实时模式)、支持星形 / 环形拓扑,工业场景主流 | 生产线分布式控制、设备间高速通信 |
| Modbus TCP | PLC 与第三方设备(如传感器、变频器、触摸屏)跨品牌通信 | 开源通用协议,使用 TCP/IP 传输,适配性广 | 对接非西门子设备(如施耐德变频器) |
| MPI/PPI | 早期 PLC(如 S7-200/300)点对点通信 | 串口 / 小范围总线通信,速度较慢(MPI 最高 187.5kbps) | 老设备调试、简单点对点数据交互 |
| OPC UA | 工业级跨平台数据传输,PLC 与 SCADA/MES/ 云平台通信 | 标准化、安全(支持加密)、可扩展,工业 4.0 核心协议 | 工厂级数据汇总、云平台数据上传 |
二、S7 PLC 的通信方式("数据传输模式")
根据应用场景,S7 PLC 通信主要分为 3 类,覆盖从调试到系统集成的全场景:
1. 编程软件通信(调试 / 监控)
- 用途:工程师通过 TIA Portal、Step7 等软件连接 PLC,实现程序下载、DB 块监控、故障排查。
- 通信链路:电脑 ↔ 编程电缆 / 以太网 ↔ PLC。
- 核心要求:电脑与 PLC 在同一网段,或通过网关路由可达。
2. PLC 与 PLC 之间通信(设备间协同)
- 常见方式:
- S7 通信(单边通信) :一台 PLC 作为客户端主动读取 / 写入另一台 PLC 的 DB 块,无需在从站 PLC 编写程序,仅需配置主站通信逻辑。
- Profinet IO 通信 :一台 PLC 作为 IO 控制器(主站),另一台作为 IO 设备(从站),主站周期性读取从站数据,实时性更高。
- 全局数据通信(GD 通信) :适用于 S7-300/400,多台 PLC 共享数据块,无需编程,通过硬件配置实现。
3. PLC 与上位机 / 第三方设备通信(数据采集 / 控制)
- 上位机通信:通过 S7 协议、OPC UA 协议,实现 WinCC、LabVIEW、自定义 C#/Python 程序读取 PLC 的 DB 块、I/O 数据。
- 第三方设备通信:通过 Modbus TCP(最通用)、Profinet(智能设备)对接变频器、触摸屏、传感器等。
三、硬件接口与通信配置("物理连接 + 参数设置")
1. 常用硬件接口
- 以太网接口(RJ45):主流接口,支持 Profinet、S7 协议、Modbus TCP,传输速率 10/100Mbps,适用于大多数场景。
- 串口(RS485/RS232):早期接口,支持 Modbus RTU 协议,适用于短距离、低速通信(如对接老式传感器)。
- 专用通信模块:如 CP343-1(S7-300 以太网模块)、CP1243-1(S7-1200 以太网模块),用于扩展 PLC 通信接口或支持特殊协议。
2. 核心通信参数(必须配置正确)
- IP 地址:PLC 的以太网通信标识,需与通信设备(电脑、上位机)在同一网段(如电脑 IP 192.168.0.100,PLC IP 192.168.0.1)。
- 子网掩码 / 网关:默认子网掩码 255.255.255.0,跨网段通信需配置网关。
- 机架号 / 槽号:S7 协议通信时需指定,西门子 PLC 默认机架号 = 0,槽号 = 1(CPU 所在槽位)。
- DB 块属性:DB 块需设置为 "未优化的块"(仅 TIA Portal),否则上位机无法通过偏移量读取数据(优化块仅支持符号寻址)。
四、关键通信概念(避免踩坑)
1. 寻址方式
- 绝对寻址:直接使用物理地址(如 DB1.DBW0、I0.0),适用于上位机、跨设备通信。
- 符号寻址:使用变量名(如 "电机转速""温度值"),适用于编程调试,需确保通信双方变量名一致(如 OPC 通信)。
- 注意:TIA Portal 中新建 DB 块默认是 "优化的块",仅支持符号寻址,需手动改为 "未优化的块" 才能使用绝对寻址(DB 块属性→取消勾选 "优化的块访问")。
2. 数据类型对应
通信时需确保读取方与 PLC 的 DB 块数据类型一致,否则会出现数据解析错误,常见对应关系:
|------------------|--------------|-----------------|------------------|
| PLC 数据类型 | 字节长度 | 上位机对应类型 | 用途 |
| Bool(位) | 1 位(占 1 字节) | bool | 开关状态(运行 / 停止、故障) |
| Int(整数) | 2 字节 | short | 小范围整数(计数器值、转速) |
| DInt(双整数) | 4 字节 | int | 大范围整数(累计产量) |
| Real(浮点数) | 4 字节 | float | 连续数值(温度、压力) |
| String(字符串) | N 字节 | string | 文本信息(设备名称、故障描述) |
3. 通信权限
- PLC 需处于 "运行模式"(RUN),部分操作(如写入 DB 块)需确保 PLC 未设置读写保护。
- 若 PLC 启用了防火墙或访问控制列表(ACL),需允许通信设备的 IP 地址访问。
五、编程实现示例(S7 协议上位机读取 DB 块)
以 Python(snap7 库)和 C#(S7netplus 库)为例,展示核心代码逻辑(西门子 S7-1200/1500 通用):
1. Python(snap7 库,快速测试)
python
import snap7
# 1. 配置PLC参数
plc_ip = "192.168.0.1" # PLC IP地址
rack = 0 # 机架号(默认0)
slot = 1 # 槽号(默认1)
db_number = 1 # DB块号
start_byte = 0 # 起始字节地址
read_length = 4 # 读取字节长度(如DInt占4字节)
# 2. 建立连接
client = snap7.client.Client()
client.connect(plc_ip, rack, slot)
# 3. 读取DB块数据(DB块号需减1,snap7库约定)
data = client.db_read(db_number - 1, start_byte, read_length)
# 4. 解析数据(示例:读取DInt类型数据)
dint_value = snap7.util.get_dint(data, 0)print(f"DB{db_number}.DBD{start_byte} = {dint_value}")
# 5. 关闭连接
client.disconnect()
2. C#(S7netplus 库,工业软件集成)
cs
using S7.Net;using System;
class PlcCommunication{
static void Main()
{
// 1. 配置PLC参数
Plc plc = new Plc(CpuType.S71200, "192.168.0.1", 0, 1);
int dbNumber = 1; // DB块号
int startByte = 0; // 起始字节
int readLength = 4; // 读取长度
try
{
// 2. 连接PLC
plc.Open();
if (plc.IsConnected)
{
// 3. 读取DB块数据
byte[] data = plc.ReadBytes(DataType.DataBlock, dbNumber, startByte, readLength);
// 4. 解析DInt类型数据
int dintValue = S7.Net.Convert.ToDInt(data, 0);
Console.WriteLine($"DB{dbNumber}.DBD{startByte} = {dintValue}");
}
}
catch (Exception ex)
{
Console.WriteLine("通信错误:" + ex.Message);
}
finally
{
// 5. 关闭连接
plc.Close();
}
}}
六、常见通信问题与排查
|-------------------------|---------------------------------------------|-------------------------------------------------------|
| 问题现象 | 常见原因 | 排查方法 |
| 无法连接 PLC | 1. IP 地址不在同一网段;2. 物理链路断开;3. PLC 未运行 | 1. 检查电脑与 PLC IP 配置;2. 更换网线 / 接口;3. 确认 PLC 处于 RUN 模式 |
| 读取 DB 块返回空值 / 错误 | 1. DB 块是 "优化的块";2. 起始字节 / 长度设置错误;3. 数据类型不匹配 | 1. 改为 "未优化的块";2. 核对 DB 块变量偏移量;3. 确认数据类型字节长度一致 |
| 通信频繁断开 | 1. 网络干扰;2. PLC 负载过高;3. 超时时间设置过短 | 1. 使用屏蔽网线;2. 优化 PLC 程序;3. 延长上位机通信超时时间 |
| 第三方设备无法通过 Modbus TCP 连接 | 1. PLC 未启用 Modbus TCP 服务器;2. 端口号错误(默认 502) | 1. 在 TIA Portal 中配置 Modbus TCP 服务器;2. 确认第三方设备端口号为 502 |
总结
西门子 S7 PLC 通信的核心是 **"协议匹配 + 参数正确 + 链路通畅"**:
- 调试场景优先用 TIA Portal 的在线监控;
- 数据采集场景优先选 S7 协议(高效)或 Modbus TCP(通用);
- 跨平台 / 跨品牌场景优先 OPC UA(标准化)。掌握上述知识点后,可快速适配大多数工业通信需求,其他品牌(如施耐德、罗克韦尔)PLC 的通信逻辑一致,仅需替换对应协议和配置工具即可。
编辑分享
使用C#编写西门子S7PLC通信的相关知识点
使用 C# 实现西门子 S7 PLC 通信,核心是借助成熟的第三方库(如 S7NetPlus、Sharp7)简化协议交互,重点关注 库的选型、连接配置、数据读写、异常处理 四大模块。以下从 核心库对比、关键知识点、分步实现、进阶技巧、常见问题 五个维度,系统梳理 C# 与 S7 PLC 通信的核心内容:
一、核心通信库选型(C# 首选工具)
西门子 S7 PLC 通信依赖 S7 协议(私有),C# 无原生类库支持,需借助第三方库,主流选择对比:
|-----------------------------------|-------------------------------------------------------------------------|-----------------------------|--------------------|
| 库名称 | 优点 | 缺点 | 适用场景 |
| S7NetPlus | 开源活跃、API 简洁、支持 S7-1200/1500/300/400、支持 .NET Framework/.NET Core/.NET 5+ | 仅支持 S7 协议,不支持 Modbus/OPC UA | 上位机数据采集、工业软件集成(首选) |
| Sharp7 | 轻量、底层稳定、支持老款 S7 PLC(如 S7-200) | API 偏底层,需手动处理字节解析 | 简单数据读取、老设备兼容场景 |
| OPC UA 库(如 Opc.Ua.Client) | 标准化、跨平台、支持多品牌 PLC | 配置复杂、通信效率略低 | 跨品牌设备集成、工厂级数据汇总 |
推荐优先使用 S7NetPlus :兼顾易用性和功能性,覆盖绝大多数 S7 PLC 通信场景。
二、C# 通信核心知识点(必掌握)
1. 通信前提条件(缺一不可)
- 硬件连接 :电脑与 PLC 通过以太网(RJ45)连接,确保网线、交换机正常(推荐直连测试)。
- PLC 配置 :
- 已设置固定 IP 地址,且与电脑在同一网段(如 PLC:192.168.0.1,电脑:192.168.0.100);
- DB 块需设置为 "未优化的块访问" (TIA Portal 中 DB 块属性→取消勾选 "优化的块访问"),否则无法通过绝对地址(如 DB1.DBW0)读写;
- PLC 处于 RUN 模式 ,无读写保护(若有保护需输入密码)。
- 库依赖 :通过 NuGet 安装对应库(S7NetPlus 搜索 "S7.Net",Sharp7 搜索 "Sharp7")。
2. 关键参数说明
|------------|---------------------------|---------------------------------------------|
| 参数 | 含义 | 默认值 / 示例 |
| CPU 类型 | 目标 PLC 的 CPU 型号(需与库匹配) | CpuType.S71200、CpuType.S71500、CpuType.S7300 |
| IP 地址 | PLC 的以太网 IP(需提前在 TIA 中配置) | 192.168.0.1、192.168.1.200 |
| 机架号(Rack) | PLC 的物理机架号(软件逻辑标识) | 西门子 PLC 默认 0 |
| 槽号(Slot) | CPU 模块在机架中的槽位号 | S7-1200/1500 默认 1;S7-300 部分为 2 |
| DB 块号 | 目标数据块的编号(如 DB1、DB10) | 需与 PLC 中实际 DB 块号一致 |
| 起始字节 | 变量在 DB 块中的起始偏移字节(绝对地址) | 从 DB 块编辑器的 "偏移量" 列获取(如 0、2、4) |
| 数据类型映射 | C# 类型与 PLC 类型必须对应,否则解析错误 | 见下表 |
3. 数据类型映射表(核心避坑点)
|------------------|----------------|-----------------|------------------------------------------------|----------------|
| PLC 数据类型 | 字节长度 | C# 对应类型 | 读取方法(S7NetPlus) | 用途示例 |
| Bool(位) | 1 位(占 1 字节) | bool | ReadBit (DataType.DataBlock, dbNum, 字节地址,位索引) | 设备运行状态(0/1) |
| Int(整数) | 2 字节 | short | ReadInt16() / ReadBytes() + Convert.ToUInt16() | 转速、计数器值 |
| DInt(双整数) | 4 字节 | int | ReadInt32() | 累计产量、时间戳 |
| Real(浮点数) | 4 字节 | float | ReadFloat() | 温度、压力(如 25.5℃) |
| String(字符串) | N 字节(含 1 字节长度) | string | ReadString () / 手动解析字节数组 | 故障描述、设备名称 |
| Word(无符号字) | 2 字节 | ushort | ReadUInt16() | 无符号计数器值 |
注意:PLC 的 String 类型默认前 1 字节存储字符串长度,后续字节为字符(如 PLC 中 String [10] 实际占 11 字节),读取时需注意长度匹配。
三、S7NetPlus 库分步实现(核心场景代码)
以下以 S7-1200 PLC 为例,实现 DB 块的读写操作,包含完整流程(连接→读写→断开→异常处理)。
步骤 1:安装 S7NetPlus 库
- 打开 Visual Studio(2019+),创建 C# 项目(如 WinForm、Console App);
- 右键项目→管理 NuGet 程序包→搜索 "S7.Net"→安装最新版本(当前稳定版 5.0+)。
步骤 2:基础通信类封装(复用性强)
创建 PlcCommunication 类,封装连接、读写、断开方法,避免重复代码:
cs
using S7.Net;using System;
namespace PlcS7Communication{
public class PlcCommunication
{
// PLC 实例(核心对象)
private Plc _plc;
// 构造函数:初始化 PLC 参数
public PlcCommunication(CpuType cpuType, string ipAddress, int rack = 0, int slot = 1)
{
_plc = new Plc(cpuType, ipAddress, rack, slot);
}
/// <summary>
/// 连接 PLC
/// </summary>
/// <returns>连接成功返回 true,失败返回 false</returns>
public bool Connect()
{
try
{
if (!_plc.IsConnected)
{
_plc.Open(); // 建立连接
}
return _plc.IsConnected;
}
catch (Exception ex)
{
Console.WriteLine($"连接失败:{ex.Message}");
return false;
}
}
/// <summary>
/// 断开 PLC 连接
/// </summary>
public void Disconnect()
{
try
{
if (_plc.IsConnected)
{
_plc.Close(); // 关闭连接
}
}
catch (Exception ex)
{
Console.WriteLine($"断开连接失败:{ex.Message}");
}
}
/// <summary>
/// 读取 DB 块中的 Int 类型数据(16位整数)
/// </summary>
/// <param name="dbNumber">DB 块号</param>
/// <param name="startByte">起始字节地址</param>
/// <param name="value">读取到的数值(输出参数)</param>
/// <returns>读取成功返回 true</returns>
public bool ReadInt16(int dbNumber, int startByte, out short value)
{
value = 0;
try
{
if (!_plc.IsConnected)
{
Console.WriteLine("未连接 PLC,无法读取数据");
return false;
}
// 读取 DB 块:DataType.DataBlock 表示读取数据块,dbNumber 为块号,startByte 为起始字节
value = _plc.ReadInt16(DataType.DataBlock, dbNumber, startByte);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"读取 Int16 失败:{ex.Message}");
return false;
}
}
/// <summary>
/// 读取 DB 块中的 Float 类型数据(32位浮点数)
/// </summary>
public bool ReadFloat(int dbNumber, int startByte, out float value)
{
value = 0;
try
{
if (!_plc.IsConnected)
{
Console.WriteLine("未连接 PLC,无法读取数据");
return false;
}
value = _plc.ReadFloat(DataType.DataBlock, dbNumber, startByte);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"读取 Float 失败:{ex.Message}");
return false;
}
}
/// <summary>
/// 读取 DB 块中的 Bool 类型数据(位数据)
/// </summary>
/// <param name="dbNumber">DB 块号</param>
/// <param name="byteAddress">位所在的字节地址</param>
/// <param name="bitIndex">位索引(0-7)</param>
/// <param name="value">读取到的布尔值</param>
public bool ReadBit(int dbNumber, int byteAddress, int bitIndex, out bool value)
{
value = false;
try
{
if (!_plc.IsConnected)
{
Console.WriteLine("未连接 PLC,无法读取数据");
return false;
}
// 读取位数据:byteAddress 是位所在的字节,bitIndex 是字节中的第几位(0-7)
value = _plc.ReadBit(DataType.DataBlock, dbNumber, byteAddress, bitIndex);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"读取 Bit 失败:{ex.Message}");
return false;
}
}
/// <summary>
/// 写入 Int 类型数据到 DB 块
/// </summary>
public bool WriteInt16(int dbNumber, int startByte, short value)
{
try
{
if (!_plc.IsConnected)
{
Console.WriteLine("未连接 PLC,无法写入数据");
return false;
}
_plc.WriteInt16(DataType.DataBlock, dbNumber, startByte, value);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"写入 Int16 失败:{ex.Message}");
return false;
}
}
}}
步骤 3:调用通信类实现读写(控制台示例)
cs
using System;
namespace PlcS7Communication{
class Program
{
static void Main(string[] args)
{
// 1. 初始化 PLC 通信对象(S7-1200,IP:192.168.0.1,默认机架 0、槽号 1)
var plcComm = new PlcCommunication(CpuType.S71200, "192.168.0.1");
try
{
// 2. 连接 PLC
bool connectSuccess = plcComm.Connect();
if (!connectSuccess)
{
Console.WriteLine("PLC 连接失败,程序退出");
return;
}
Console.WriteLine("PLC 连接成功!");
// 3. 读取数据示例
// 读取 DB1.DBW0(Int 类型,起始字节 0)
if (plcComm.ReadInt16(1, 0, out short intValue))
{
Console.WriteLine($"DB1.DBW0 = {intValue}");
}
// 读取 DB1.DBD4(Float 类型,起始字节 4)
if (plcComm.ReadFloat(1, 4, out float floatValue))
{
Console.WriteLine($"DB1.DBD4 = {floatValue:F2}");
}
// 读取 DB1.DBX8.0(Bool 类型,字节 8,位索引 0)
if (plcComm.ReadBit(1, 8, 0, out bool bitValue))
{
Console.WriteLine($"DB1.DBX8.0 = {bitValue}");
}
// 4. 写入数据示例(写入 DB1.DBW0 = 100)
bool writeSuccess = plcComm.WriteInt16(1, 0, 100);
Console.WriteLine(writeSuccess ? "写入 DB1.DBW0 = 100 成功" : "写入失败");
}
finally
{
// 5. 无论成功与否,最终断开连接(释放资源)
plcComm.Disconnect();
Console.WriteLine("PLC 连接已断开");
}
Console.ReadKey();
}
}}
四、进阶技巧(工业场景必备)
1. 批量读取数据(提升效率)
若需读取多个连续变量,优先使用 ReadBytes 批量读取字节数组,再手动解析,避免多次通信:
cs
/// <summary>/// 批量读取 DB 块连续字节,解析为多个变量/// </summary>/// <param name="dbNumber">DB 块号</param>/// <param name="startByte">起始字节</param>/// <param name="readLength">读取总字节数</param>public void BatchReadData(int dbNumber, int startByte, int readLength){
try
{
if (!_plc.IsConnected) return;
// 批量读取字节数组(如从 DB1 起始字节 0 读取 10 字节)
byte[] data = _plc.ReadBytes(DataType.DataBlock, dbNumber, startByte, readLength);
// 解析:DB1.DBW0(Int16)、DB1.DBD2(Float)、DB1.DBX6.0(Bool)
short var1 = S7.Net.Convert.ToUInt16(data, 0); // 字节 0-1
float var2 = S7.Net.Convert.ToFloat(data, 2); // 字节 2-5
bool var3 = S7.Net.Convert.ToBit(data, 6, 0); // 字节 6,位 0
Console.WriteLine($"批量读取:var1={var1}, var2={var2}, var3={var3}");
}
catch (Exception ex)
{
Console.WriteLine($"批量读取失败:{ex.Message}");
}}
2. 异步通信(避免 UI 卡顿)
WinForm/WPF 等带 UI 的项目,需使用异步方法(async/await),防止通信阻塞 UI 线程:
cs
/// <summary>/// 异步读取 Int16 数据/// </summary>public async Task<bool> ReadInt16Async(int dbNumber, int startByte, out short value){
value = 0;
try
{
if (!_plc.IsConnected) return false;
// S7NetPlus 5.0+ 支持异步方法
value = await _plc.ReadInt16Async(DataType.DataBlock, dbNumber, startByte);
return true;
}
catch (Exception ex)
{
Console.WriteLine($"异步读取失败:{ex.Message}");
return false;
}}
3. 通信状态监控(工业级稳定性)
添加心跳检测机制,定期验证 PLC 连接状态,避免断连后未察觉:
cs
/// <summary>/// 心跳检测(每 5 秒检测一次连接)/// </summary>public async Task HeartbeatCheckAsync(CancellationToken cancellationToken){
while (!cancellationToken.IsCancellationRequested)
{
bool isConnected = _plc.IsConnected;
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] PLC 连接状态:{isConnected}");
// 若断开,尝试重连
if (!isConnected)
{
Console.WriteLine("尝试重连 PLC...");
await Task.Run(() => Connect());
}
await Task.Delay(5000, cancellationToken); // 5 秒间隔
}}
五、常见问题与排查(避坑指南)
|-------------------------|---------------------------------------|----------------------------------------------------------------------------|
| 问题现象 | 核心原因 | 排查方法 |
| 连接失败:"No route to host" | 1. IP 地址错误;2. 电脑与 PLC 不在同一网段;3. 防火墙拦截 | 1. 核对 TIA 中 PLC 的 IP;2. 手动设置电脑 IP 与 PLC 同网段;3. 关闭电脑防火墙或放行程序 |
| 读取失败:"Invalid address" | 1. DB 块是 "优化的块";2. 起始字节 / 数据类型不匹配 | 1. 在 TIA 中取消 DB 块 "优化的块访问";2. 核对 DB 块偏移量和字节长度 |
| 数据解析错误(如浮点数为 0.0) | C# 类型与 PLC 类型不匹配(如用 ReadInt32 读 Real) | 对照 "数据类型映射表",确保读取方法与 PLC 类型一致 |
| 通信频繁断开 | 1. 网络干扰;2. PLC 负载过高;3. 超时时间过短 | 1. 使用屏蔽网线;2. 优化 PLC 程序;3. 延长 S7NetPlus 超时时间(_plc.ConnectionTimeout = 5000) |
| 写入失败:"Access denied" | PLC 启用了写保护或处于 STOP 模式 | 1. 解除 PLC 写保护;2. 切换 PLC 到 RUN 模式 |
总结
C# 与西门子 S7 PLC 通信的核心逻辑是:选对库(S7NetPlus)→ 配对参数(IP / 机架 / 槽号)→ 选对读写方法(匹配数据类型)→ 做好异常处理和稳定性保障 。上述代码可直接用于 S7-1200/1500,若适配 S7-300/400,仅需修改 CpuType(如 CpuType.S7300)和槽号(部分 S7-300 槽号为 2)即可。
若需对接 Modbus TCP 或 OPC UA 协议,可基于上述逻辑扩展(如 Modbus 用 NModbus 库,OPC UA 用 Opc.Ua.Client 库),核心思路一致。