GKMLT通讯工具箱(WPF MVVM) - 06-OPCUA通讯

一、概述

OPC UA (Open Platform Communications Unified Architecture) 是工业自动化领域的设备通讯标准,提供安全、可靠、平台无关的数据交换机制。本模块实现了OPC UA客户端功能,支持连接、浏览、读取、写入、订阅等核心操作。

二、通讯报文原理

2.1 OPC UA协议栈

复制代码
应用层: OPC UA服务 (读取、写入、订阅、浏览)
传输层: TCP/IP协议
安全层: 安全策略(加密、签名)
编码层: 二进制编码/XML编码

2.2 通讯模式

OPC UA采用客户端-服务器架构:

复制代码
客户端 ←→ OPC UA会话 ←→ 服务器
         ↓
    安全通道
    (加密+签名)

2.3 连接建立流程

复制代码
1. 发现服务器端点
   客户端 → GetEndpoints → 服务器 → EndpointDescription[]

2. 创建安全通道
   客户端 → OpenSecureChannel → 服务器 → SecureChannel

3. 创建会话
   客户端 → CreateSession → 服务器 → SessionId

4. 激活会话
   客户端 → ActivateSession → 服务器 → 会话已激活

2.4 服务调用报文结构

请求报文:

复制代码
Type: Request Message
- RequestHeader: 请求头(认证、时间戳等)
- 具体服务参数:NodeId、AttributeId等

响应报文:

复制代码
Type: Response Message
- ResponseHeader: 响应头(状态码、时间戳)
- 具体服务结果:Value、StatusCode等

2.5 节点浏览机制

OPC UA使用节点(Node)和引用(Reference)构建地址空间:

复制代码
Root Folder (i=85)
├── Objects (i=85)
│   └── Server (i=2253)
│       └── Arrays (ns=2;s=Arrays)
├── Types (i=86)
└── Views (i=87)

节点标识符(NodeId)格式:

  • i=85:数字标识符
  • ns=2;s=MyVariable:命名空间索引2 + 字符串标识符
  • ns=2;g=xxxxxxxx:命名空间索引2 + GUID标识符

2.6 订阅机制

OPC UA订阅采用发布-订阅模式:

复制代码
1. 创建订阅 (Subscription)
   客户端 → CreateSubscription → 服务器

2. 添加监控项 (MonitoredItem)
   客户端 → CreateMonitoredItems → 服务器

3. 数据变化通知
   服务器 → DataChangeNotification → 客户端

4. 保持连接
   客户端 ↔ 服务器 (Publish请求/响应)

订阅参数:

  • PublishingInterval:发布间隔(默认1000ms)
  • ClientHandle:客户端句柄
  • SamplingInterval:采样间隔
  • QueueSize:队列大小
  • DiscardOldest:队列满时是否丢弃最旧值

三、调用的库和方法

3.1 核心库

OPCFoundation.NetStandard.Opc.Ua
  • 版本:1.5.378.134
  • 来源:官方NuGet包
  • 作用:OPC UA协议实现

主要命名空间:

  • Opc.Ua:核心数据类型和定义
  • Opc.Ua.Client:客户端功能
  • Opc.Ua.Configuration:配置管理

3.2 核心类:OpcUaClient

3.2.1 BuildConfig 方法

功能:构建应用配置和证书管理

方法签名:

csharp 复制代码
public void BuildConfig()

实现原理:

csharp 复制代码
public void BuildConfig()
{
    // 1. 创建应用实例
    myAppInstance = new ApplicationInstance()
    {
        ApplicationType = ApplicationType.Client,
        ApplicationName = "GKMLT_OPCUAClient",
    };

    // 2. 创建应用配置
    myAppInstance.ApplicationConfiguration = new Opc.Ua.ApplicationConfiguration();

    // 3. 进行安全配置
    CreateClientConfiguration();

    // 4. 配置证书验证
    certificateValidator = new CertificateValidator();
    myAppInstance.ApplicationConfiguration.CertificateValidator = certificateValidator;
    certificateValidator.CertificateValidation += CertClient;
}

配置内容:

  • 应用名称和URI
  • 证书自动信任(开发环境)
  • 传输配额(超时、消息大小等)
  • 客户端会话超时
3.2.2 AnonmousConnect 方法

功能:匿名连接到OPC UA服务器

方法签名:

csharp 复制代码
public static async Task<Session> AnonmousConnect(string serverUrl)

参数说明:

  • serverUrl:服务器Endpoint地址(如:opc.tcp://localhost:4840

返回值:

  • Session:已建立的会话对象

实现代码:

csharp 复制代码
public static async Task<Session> AnonmousConnect(string serverUrl)
{
    // 1. 获取服务器端点
    EndpointDescriptionCollection endpoints = GetEndpointDescription(serverUrl);

    // 2. 选择端点(优先选择无安全策略)
    EndpointDescription endpoint = SelectEndpoint(endpoints);

    // 3. 配置端点
    ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create());

    // 4. 创建匿名身份
    userIdentity = new UserIdentity(new AnonymousIdentityToken());

    // 5. 更新会话名称
    string sessionName = myAppInstance.ApplicationName + " Session";

    // 6. 创建并激活会话
    Session session = await Session.Create(
        myAppInstance.ApplicationConfiguration,
        configuredEndpoint,
        updateBeforeConnect: true,
        sessionName,
        60000,
        userIdentity,
        preferredLocales: null);

    // 7. 返回会话
    return session;
}

调用示例:

csharp 复制代码
Session session = await OpcUaClient.AnonmousConnect("opc.tcp://localhost:4840");
if (session != null && session.Connected)
{
    // 连接成功
}
3.2.3 UserNameConnect 方法

功能:使用用户名密码连接到OPC UA服务器

方法签名:

csharp 复制代码
public static async Task<Session> UserNameConnect(string serverUrl, string username, string password)

参数说明:

  • serverUrl:服务器Endpoint地址
  • username:用户名
  • password:密码

实现代码:

csharp 复制代码
public static async Task<Session> UserNameConnect(string serverUrl, string username, string password)
{
    // 1-4步同匿名连接

    // 5. 创建用户名密码身份
    userIdentity = new Opc.Ua.UserIdentity(username, System.Text.Encoding.UTF8.GetBytes(password));

    // 6-7步同匿名连接
    Session session = await Session.Create(/* 参数 */);
    return session;
}

调用示例:

csharp 复制代码
Session session = await OpcUaClient.UserNameConnect(
    "opc.tcp://localhost:4840",
    "admin",
    "password123");
3.2.4 BrowserNode 方法

功能:浏览指定节点的子节点

方法签名:

csharp 复制代码
public static ReferenceDescriptionCollection BrowserNode(Session session, string nodeIdStr, bool recursive = false)

参数说明:

  • session:OPC UA会话
  • nodeIdStr:节点ID字符串(如:i=85
  • recursive:是否递归浏览(暂未使用)

返回值:

  • ReferenceDescriptionCollection:子节点引用集合

实现代码:

csharp 复制代码
public static ReferenceDescriptionCollection BrowserNode(Session session, string nodeIdStr, bool recursive = false)
{
    // 1. 解析节点ID
    NodeId nodeId = NodeId.Parse(nodeIdStr);

    // 2. 构建浏览描述
    BrowseDescription browseTo = new BrowseDescription();
    browseTo.NodeId = nodeId;
    browseTo.BrowseDirection = BrowseDirection.Forward;
    browseTo.ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences;
    browseTo.IncludeSubtypes = true;
    browseTo.NodeClassMask = 0;
    browseTo.ResultMask = (uint)(BrowseResultMask.All | BrowseResultMask.ReferenceTypeId | BrowseResultMask.BrowseName);

    // 3. 创建浏览请求
    BrowseDescriptionCollection nodesToBrowse = new BrowseDescriptionCollection();
    nodesToBrowse.Add(browseTo);

    // 4. 执行浏览
    BrowseResultCollection results = session.Browse(null, null, nodesToBrowse, out BrowseResultCollection diagnosticInfos);

    // 5. 提取引用集合
    ReferenceDescriptionCollection references = new ReferenceDescriptionCollection();
    if (results.Count > 0)
    {
        foreach (ReferenceDescription reference in results[0].References)
        {
            references.Add(reference);
        }
    }

    return references;
}

调用示例:

csharp 复制代码
var references = OpcUaClient.BrowserNode(session, "i=85", false);
foreach (var reference in references)
{
    Console.WriteLine($"Node: {reference.DisplayName}, NodeId: {reference.NodeId}");
}
3.2.5 SessionSyncRead 方法

功能:同步读取节点值

方法签名:

csharp 复制代码
public static OpcUaDataItem SessionSyncRead(Session session, NodeId nodeId)

参数说明:

  • session:OPC UA会话
  • nodeId:要读取的节点ID

返回值:

  • OpcUaDataItem:包含节点值、类型、状态码、时间戳等

实现代码:

csharp 复制代码
public static OpcUaDataItem SessionSyncRead(Session session, NodeId nodeId)
{
    // 1. 创建读取值ID集合
    ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
    ReadValueId nodeToRead = new ReadValueId();
    nodeToRead.NodeId = nodeId;
    nodeToRead.AttributeId = Attributes.Value;
    nodesToRead.Add(nodeToRead);

    // 2. 执行读取
    DataValueCollection results = null;
    DiagnosticInfoCollection diagnosticInfos = null;
    session.Read(
        default,
        0,
        TimestampsToReturn.Both,
        nodesToRead,
        out results,
        out diagnosticInfos);

    // 3. 提取结果
    OpcUaDataItem dataItem = new OpcUaDataItem();
    if (results.Count > 0)
    {
        DataValue value = results[0];
        dataItem.NodeId = nodeId.ToString();
        dataItem.Value = value.WrappedValue?.ToString() ?? "null";
        dataItem.TypeInfo = value.WrappedValue?.Type?.ToString() ?? "Unknown";
        dataItem.StatusCode = value.StatusCode?.ToString() ?? "Unknown";
        dataItem.Timestamp = value.SourceTimestamp?.ToString("yyyy-MM-dd HH:mm:ss.fff") ?? "";
    }

    return dataItem;
}

调用示例:

csharp 复制代码
var dataItem = OpcUaClient.SessionSyncRead(session, NodeId.Parse("ns=2;s=MyVariable"));
Console.WriteLine($"Value: {dataItem.Value}, Status: {dataItem.StatusCode}");
3.2.6 SessionSyncWrite 方法

功能:同步写入节点值

方法签名:

csharp 复制代码
public static int SessionSyncWrite(Session session, NodeId nodeId, object value, string dataType, bool isArray = false)

参数说明:

  • session:OPC UA会话
  • nodeId:要写入的节点ID
  • value:要写入的值
  • dataType:数据类型(String、Int32、Boolean等)
  • isArray:是否为数组(暂未完全实现)

返回值:

  • int:状态码(0表示成功)

实现代码:

csharp 复制代码
public static int SessionSyncWrite(Session session, NodeId nodeId, object value, string dataType, bool isArray = false)
{
    // 1. 创建写入值集合
    WriteValueCollection nodesToWrite = new WriteValueCollection();

    // 2. 根据数据类型构造DataValue
    DataValue dataValue = new DataValue();
    Variant variant = new Variant();
    System.Type type = null;

    switch (dataType)
    {
        case "Boolean":
            type = typeof(bool);
            variant = new Variant(Convert.ToBoolean(value));
            break;
        case "Int32":
            type = typeof(int);
            variant = new Variant(Convert.ToInt32(value));
            break;
        case "Double":
            type = typeof(double);
            variant = new Variant(Convert.ToDouble(value));
            break;
        case "String":
            type = typeof(string);
            variant = new Variant(value.ToString());
            break;
        // 其他类型...
    }

    dataValue.WrappedValue = variant;
    dataValue.Value = variant;

    // 3. 创建写入值
    WriteValue writeValue = new WriteValue();
    writeValue.NodeId = nodeId;
    writeValue.AttributeId = Attributes.Value;
    writeValue.Value = dataValue;
    nodesToWrite.Add(writeValue);

    // 4. 执行写入
    StatusCodeCollection results = null;
    DiagnosticInfoCollection diagnosticInfos = null;
    session.Write(
        default,
        nodesToWrite,
        out results,
        out diagnosticInfos);

    // 5. 返回状态码
    if (results.Count > 0)
    {
        return results[0].Code; // 0表示成功
    }

    return -1;
}

调用示例:

csharp 复制代码
int result = OpcUaClient.SessionSyncWrite(
    session,
    NodeId.Parse("ns=2;s=MyVariable"),
    123.45,
    "Double");

if (result == 0)
{
    Console.WriteLine("写入成功");
}
3.2.7 SessionSubscription 方法

功能:订阅节点数据变化

方法签名:

csharp 复制代码
public static void SessionSubscription(Session session, NodeId nodeId, bool isSubscribe, SubscriptionCallback callback)

参数说明:

  • session:OPC UA会话
  • nodeId:要订阅的节点ID
  • isSubscribe:true=订阅,false=取消订阅
  • callback:数据变化回调函数

实现代码:

csharp 复制代码
public static void SessionSubscription(Session session, NodeId nodeId, bool isSubscribe, SubscriptionCallback callback)
{
    // 1. 创建订阅
    Subscription subscription = new Subscription(session.DefaultSubscription);
    subscription.PublishingEnabled = true;
    subscription.PublishingInterval = 1000; // 1秒
    subscription.Priority = 0;
    subscription.KeepAliveCount = 10;
    subscription.LifetimeCount = 100;
    subscription.MaxNotificationsPerPublish = 100;
    subscription.MaxQueueSize = 100;

    session.AddSubscription(subscription);
    subscription.Create();

    // 2. 创建监控项
    MonitoredItem monitoredItem = new MonitoredItem(subscription.DefaultItem);
    monitoredItem.StartNodeId = nodeId;
    monitoredItem.AttributeId = Attributes.Value;
    monitoredItem.SamplingInterval = 1000; // 1秒采样
    monitoredItem.QueueSize = 1;
    monitoredItem.DiscardOldest = true;

    // 3. 添加通知事件
    monitoredItem.Notification += new MonitoredItemNotificationEventHandler(MonitoredItem_Notification);

    subscription.AddItem(monitoredItem);
    subscription.ApplyChanges();

    mi = monitoredItem;
    subNodeId = nodeId;
    subOpen = true;
}

// 数据变化通知处理
private static void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs args)
{
    MonitoredItemNotification notification = args.NotificationValue;
    DataValue value = notification.Value.Value;

    // 触发数据变更事件
    OnDataChanged?.Invoke(
        monitoredItem.StartNodeId.ToString(),
        value.WrappedValue?.ToString() ?? "null",
        value.StatusCode?.ToString() ?? "Unknown");
}

调用示例:

csharp 复制代码
OpcUaClient.SessionSubscription(session, NodeId.Parse("ns=2;s=MyVariable"), true, (nodeId, value, status) =>
{
    Console.WriteLine($"Node: {nodeId}, Value: {value}, Status: {status}");
});

3.3 数据模型

OpcUaDataItem 类

功能:封装OPC UA节点数据

csharp 复制代码
public class OpcUaDataItem
{
    public string NodeId { get; set; }        // 节点ID
    public string Value { get; set; }         // 节点值
    public string TypeInfo { get; set; }      // 数据类型
    public string StatusCode { get; set; }    // 状态码
    public string Timestamp { get; set; }     // 时间戳
}
OpcUaNodeInfo 类

功能:封装节点浏览信息

csharp 复制代码
public class OpcUaNodeInfo
{
    public string NodeId { get; set; }        // 节点ID
    public string BrowseName { get; set; }    // 浏览名称
    public string DisplayName { get; set; }   // 显示名称
    public string NodeClass { get; set; }     // 节点类型(Object、Variable等)
}

3.4 事件机制

OnDataChanged 事件

功能:订阅节点数据变化时触发

事件定义:

csharp 复制代码
public static event Action<string, string, string> OnDataChanged;

事件参数:

  • string nodeId:节点ID
  • string value:节点值
  • string statusCode:状态码

使用示例:

csharp 复制代码
OpcUaClient.OnDataChanged += (nodeId, value, statusCode) =>
{
    // 在UI线程更新显示
    Dispatcher.Invoke(() =>
    {
        AddLogMessage($"[{DateTime.Now}] {nodeId} = {value}");
    });
};

四、证书管理

4.1 证书配置

csharp 复制代码
// 自动信任不受信任的证书(开发环境)
configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates = true;

// 拒绝SHA1签名证书(可选)
configuration.SecurityConfiguration.RejectSHA1SignedCertificates = false;

// 证书存储位置
configuration.SecurityConfiguration.ApplicationCertificate.StoreType = CertificateStoreType.X509Store;
configuration.SecurityConfiguration.ApplicationCertificate.StorePath = "CurrentUser\\My";

4.2 证书验证回调

csharp 复制代码
private static void CertClient(CertificateValidator sender, CertificateValidationEventArgs e)
{
    if (e.Error.StatusCode == StatusCodes.BadCertificateUntrusted)
    {
        e.Accept = true; // 自动接受不受信任的证书
    }
}

4.3 自签名证书创建

csharp 复制代码
private static void CreateCertificateAndAddToStore(string applicationUri, string applicationName,
    string storeType, string storePath)
{
    List<string> localIps = GetLocalIpAddressAndDns();

    var certificateBuilder = CertificateFactory.CreateCertificate(
        applicationUri,
        applicationName,
        null,
        localIps);

    X509Certificate2 clientCertificate2 = certificateBuilder
        .SetNotBefore(startTime)
        .SetNotAfter(startTime.AddMonths(24))
        .SetPrivateKeyMinimumKeySize(2048)
        .CreateForSignature();

    // 添加到证书存储
    X509Utils.AddToStore(clientCertificate2, storeType, storePath);
}

五、安全策略

5.1 支持的安全策略

  • None:无安全(仅开发环境)
  • Basic128Rsa15:基础128位RSA加密
  • Basic256:基础256位加密
  • Basic256Sha256:256位加密+SHA256签名

5.2 用户认证方式

  1. 匿名认证

    csharp 复制代码
    userIdentity = new UserIdentity(new AnonymousIdentityToken());
  2. 用户名密码认证

    csharp 复制代码
    userIdentity = new UserIdentity(username, Encoding.UTF8.GetBytes(password));
  3. 证书认证

    csharp 复制代码
    userIdentity = new UserIdentity(certificate);

六、错误处理

6.1 常见状态码

状态码 含义 处理方式
Good (0x00000000) 成功 正常处理
BadNodeIdUnknown (0x80030000) 节点不存在 检查NodeId
BadAttributeInvalid (0x80050000) 属性无效 检查AttributeId
BadUserAccessDenied (0x801F0000) 访问拒绝 检查权限
BadTimeout (0x800A0000) 超时 增加超时时间
BadSessionClosed (0x80060000) 会话关闭 重新连接

6.2 异常处理示例

csharp 复制代码
try
{
    var session = await OpcUaClient.AnonmousConnect("opc.tcp://localhost:4840");
    if (session.Connected)
    {
        var dataItem = OpcUaClient.SessionSyncRead(session, nodeId);
        Console.WriteLine($"Value: {dataItem.Value}");
    }
}
catch (ServiceResultException sre)
{
    Console.WriteLine($"OPC UA错误: {sre.StatusCode}");
}
catch (Exception ex)
{
    Console.WriteLine($"系统异常: {ex.Message}");
}

七、性能优化

7.1 批量操作

使用批量读取/写入提高效率:

csharp 复制代码
// 批量读取
ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
nodesToRead.Add(new ReadValueId { NodeId = nodeId1, AttributeId = Attributes.Value });
nodesToRead.Add(new ReadValueId { NodeId = nodeId2, AttributeId = Attributes.Value });

session.Read(default, 0, TimestampsToReturn.Both, nodesToRead, out results, out _);

7.2 订阅优化

  • 调整发布间隔(PublishingInterval)
  • 优化采样间隔(SamplingInterval)
  • 设置合适的队列大小(QueueSize)
csharp 复制代码
subscription.PublishingInterval = 1000; // 1秒
monitoredItem.SamplingInterval = 500;   // 500毫秒

7.3 连接复用

避免频繁创建/销毁会话:

csharp 复制代码
// 全局会话
private static Session _currentSession;

// 使用时检查连接
if (_currentSession == null || !_currentSession.Connected)
{
    _currentSession = await OpcUaClient.AnonmousConnect(endpointUrl);
}

八、应用场景

8.1 工业自动化

  • PLC数据采集
  • SCADA系统集成
  • MES系统对接

8.2 物联网平台

  • 设备数据上报
  • 远程监控与控制
  • 数据分析平台

8.3 设备调试

  • 节点浏览工具
  • 数据读写测试
  • 通讯诊断工具

九、总结

OPC UA通讯模块提供了完整的工业通讯解决方案:

  1. 安全性:支持加密、签名、证书验证
  2. 可靠性:会话管理、重连机制、错误恢复
  3. 高效性:批量操作、订阅机制、连接复用
  4. 易用性:简洁的API接口、完善的事件机制
  5. 标准化:符合OPC UA国际标准

该模块可广泛应用于工业自动化、物联网、设备调试等场景,为数据采集和设备控制提供可靠的通讯保障。

相关推荐
雨浓YN5 小时前
GKMLT通讯工具箱(WPF MVVM) - 03-西门子S7通讯
wpf
雨浓YN7 小时前
GKMLT通讯工具箱(WPF MVVM) - 05-WebAPI通讯
wpf
软泡芙1 天前
【WPF 】MVVM 设计模式在 WPF 中的实战应用
设计模式·wpf
张小俊_1 天前
WPF 跨线程 UI 更新与硬编码赋值引发的 Bug 排查
c#·bug·wpf
七夜zippoe2 天前
DolphinDB在工业物联网中的优势
物联网·wpf·工业物联网·优势·dolphindb
heimeiyingwang2 天前
【架构实战】观察者模式在分布式系统中的应用
观察者模式·架构·wpf
bugcome_com2 天前
WPF + Microsoft.ToolKit.Mvvm 技术指南与实战项目
microsoft·wpf
武藤一雄3 天前
WPF中逻辑树(Logical Tree)与可视化树(Visual Tree)到底是什么
microsoft·c#·.net·wpf·.netcore
炸炸鱼.3 天前
ELK 企业级日志分析系统完整部署手册
elk·wpf