OPC UA 采数据,十个项目九个卡在三个地方:订阅参数配不对导致数据风暴或丢点、断线后订阅悄悄死掉没人知道、证书信任问题连都连不上。常州这条光伏电池片产线,800 多个点位(PECVD 炉温、真空度、气体流量、节拍计数),我们用 C# 官方栈 OPCFoundation.NetStandard.Opc.Ua 做采集,把整条链路从握手到断线重连抓包过了一遍。这篇就按抓包顺序讲,看完你照着配就能跑稳。

为什么这条产线弃用 Modbus TCP,转了 OPC UA?

老设备改造我们一般用 Modbus TCP,便宜直接。但这条线有一半是近两年的新设备(PECVD、烧结炉),原厂自带 OPC UA Server。对比下来差别很实在:Modbus 你得自己维护一张"40021 对应二区温度"的地址表,设备升级换个寄存器地址,采集端全瞎;OPC UA 的点位是带名字、带数据类型、带工程单位的节点,客户端能 Browse 浏览,温度就是 ns=2;s=PECVD.Zone2.Temp,单位 ℃ 跟着数据一起走。再加上它原生支持订阅上报和加密,MES 侧点名要可追溯的数据来源,最后定了 OPC UA。

抓包看全流程:从上电到数据进库的 6 次握手

很多人写 OPC UA 客户端只会调 Session.Create,出了问题完全不知道链路哪断了。我们用 Wireshark 挂在 4840 端口抓了一次完整建链,服务交互顺序是这样的(C=客户端上位机,S=设备端 Server):

#方向服务(Service)关键字段与作用
1C → SGetEndpoints问服务器支持哪些安全策略。返回 SecurityPolicyUri(None / Basic256Sha256 等)和证书。生产环境一定选 SignAndEncrypt,None 等于明文裸奔。
2C → SCreateSession服务器回 SessionIdAuthenticationTokenServerNonce。此时会话建了但还不能用
3C → SActivateSession客户端用自己证书的私钥对 Nonce 签名,证明"我真的是这张证书的持有者",再提交用户名密码。验过了,会话才激活。
4C → SCreateSubscription建订阅,提交 PublishingInterval=1000msMaxKeepAliveCount=10LifetimeCount=1000,服务器回 SubscriptionId
5C → SCreateMonitoredItems把要采的点位登记成监控项:NodeIdAttributeId=ValueSamplingIntervalQueueSize、死区过滤器。服务器会把实际生效值(revised)回给你。
6S → CPublish 响应数据来了:NotificationMessage 里装 MonitoredItemNotification,含 ClientHandle(你自己给点位编的号)和 DataValue(值 + 状态码 + SourceTimestamp + ServerTimestamp)。
最容易被误解的一点:Publish 是"客户端发请求、服务器攥着不回"。客户端提前把一堆 Publish 请求发过去排队,服务器没数据时不回,等到有数据变化、或 KeepAlive 到期了才用其中一个请求回你。所以抓包看到 Publish 请求发出去半天没响应,不是卡死,是正常的。这也是 OPC UA 订阅比轮询省流量的根本原因——没变化就不发数据。

采样间隔 ≠ 发布间隔:四个参数别配反了

新手最常犯的错,是把 SamplingInterval 当成"数据刷新周期",配一个 100ms 就以为 100ms 收到一次。根本不是。这两个参数管的是两层:

  • SamplingInterval(采样间隔):服务器多久去看一次底层数据源(PLC 寄存器)。这是服务器干活的节奏。
  • PublishingInterval(发布间隔):服务器多久把攒下的变化打包发一次给你。这是网络发包的节奏。
  • QueueSize + DiscardOldest:两个发布周期间数据变了好几次,先排队;队列满了丢最老的(true)还是最新的(false)。工艺曲线类要丢老保新,报警状态类建议丢新保老,别配反。
  • MaxKeepAliveCount / LifetimeCount:没数据时,多少个发布周期回一个空包保活(KeepAlive);客户端多久没露面、服务器就判订阅死亡(Lifetime)。

这条产线我们按点位性质分了三档参数,跑了一周稳定后的配置:

点位类型采样间隔发布间隔队列死区说明
炉温区温度(慢变)2000ms2000ms2绝对 0.5℃温度惯性大,2 秒足够,死区挡掉传感器末位抖动
气体流量 / 真空度500ms1000ms5百分比 1%流量调节过程要看趋势,队列留 5 防突发
开关量 / 报警状态500ms事件即发10Trigger 设 StatusValue,状态翻转必报,丢新保老
节拍 / 产量计数1000ms1000ms3计数只增不减,漏一个节拍都对不上 MES 产量

死区一开,模拟量上报量直接掉 90%

刚开始调试时没配死区,800 个点里 300 多个模拟量,传感器末位一直在 ±0.1 抖动,每个发布周期几百条变化涌上来,上位机入库线程 CPU 直接拉到 60%。加上 DataChangeFilter 死区之后——变化幅度没超过死区就当没变——同样的点位,上报量掉到原来的十分之一。绝对死区给温度(0.5℃),百分比死区给流量(满量程 1%),各管各的量程,这是经验值。

// NuGet: OPCFoundation.NetStandard.Opc.Ua
using Opc.Ua;
using Opc.Ua.Client;

var endpoint = new ConfiguredEndpoint(null,
    new EndpointDescription(new Uri("opc.tcp://192.168.10.21:4840"),
        MessageSecurityMode.SignAndEncrypt,
        SecurityPolicy.Basic256Sha256SecurityPolicyUri));

using (var session = Session.Create(config, endpoint, false,
        "PVLineHMI", 60000, new UserIdentity("hmi_opc", "****"), null))
{
    var sub = new Subscription(session.DefaultSubscription) {
        PublishingInterval = 1000,   // 1秒打包发一次
        MaxKeepAliveCount  = 10,     // 10秒无数据回一个空包保活
        LifetimeCount      = 1000,   // 1000个周期没露面→订阅死亡
        PublishingEnabled  = true
    };

    var tempItem = new MonitoredItem(sub.DefaultItem) {
        StartNodeId       = new NodeId("PECVD.Zone1.Temp", 2),
        AttributeId       = Attributes.Value,
        SamplingInterval  = 2000,
        QueueSize         = 2,
        DiscardOldest     = true,
        Filter = new DataChangeFilter {
            Trigger       = DataChangeTrigger.StatusValue,
            DeadbandType  = (uint)DeadbandType.Absolute,
            DeadbandValue = 0.5      // 变化不到0.5℃不上报
        }
    };

    tempItem.Notification += (s, e) => {
        var n = (MonitoredItemNotification)e.NotificationValue;
        DataValue v = n.Value;
        if (StatusCode.IsBad(v.StatusCode)) return;   // 坏值不入库
        _buffer.Enqueue((n.ClientHandle, Convert.ToDouble(v.Value),
                         v.SourceTimestamp));  // 进队列,另起线程批量写库
    };

    sub.AddItem(tempItem);
    session.AddSubscription(sub);
    sub.Create();   // 这一句才真正把订阅和监控项发到服务器
}

▲ 注意数据别在 Notification 回调里直接写数据库——回调是通信线程,800 个点挤进来会堵发布。先丢内存队列(Channel/BlockingCollection),另起线程批量入库。

断线不丢数:订阅死了要能自己爬起来

光伏车间夏天电压波动大,交换机重启、设备 Server 进程重启都遇到过。OPC UA 断线有两层要盯:会话层订阅层。会话断了好说,Session.KeepAlive 事件里状态码变成 BadNoCommunication 就触发重连;坑的是会话还在、订阅却超时死了——网络中断时间超过 LifetimeCount(我们配 1000 秒),服务器单方面销毁订阅,重连后你以为还在采数据,其实一个点都不上来了。

我们的做法是重连分三步走:

  1. 先尝试 session.Reconnect():短时间闪断,会话和订阅都还在,直接恢复;
  2. 会话重建后调 TransferSubscriptions:把服务器上还活着的订阅迁移到新会话(SubscriptionId 不变,漏发的数据还能补);
  3. 迁移失败就重建订阅,并记录断线时刻——恢复后用 SourceTimestamp 向服务器或本地缓存补采断档区间,绝不能用 ServerTimestamp,那是服务器收到的时间,断网期间根本没有。
现场踩过的三个坑,说给你避一避:
证书信任:第一次连接必报 BadSecurityChecksFailed。客户端自签名证书要放到服务器的 Trusted 证书目录(设备厂商界面里通常有"信任客户端证书"列表),测试环境图省事可以自动接受,生产环境千万别。
CreateMonitoredItems 要分批:一次性把 800 个点塞进去,服务器处理超时直接 BadNothingToDo 或回包截断。每批 200~500 个,分几批建。
SamplingInterval 你申请的值不算数:服务器受性能限制会返回 revisedSamplingInterval,申请 100ms 可能给你 500ms。建完监控项一定要读回 revised 值,拿这个算数据周期,别拿自己的申请值算。

动手之前,先做这一件事

建议你拿手头的 OPC UA 设备先用 Wireshark 抓一次完整建链,对照上面那张六步表走一遍——把每个服务的请求和回包看明白,比看十篇文档都管用。参数配置从"发布 1000ms + 死区"这个保守值起步,再按点位性质分档调,上线后盯一周服务器 CPU 和上位机入库积压量。订阅机制用顺了,OPC UA 比 Modbus 轮询省心太多:数据自己推上来,断了有 KeepAlive 喊你,点名叫什么、什么单位,服务器全告诉你。