一、为什么工业上位机要选“OPC UA + .NET 8”?

我们团队去年接了个新能源电池生产线的上位机项目,光PLC就涉及三个品牌:西门子S7-1200、罗克韦尔Micro850、施耐德M262。一开始按传统思路,针对每个品牌写专属通信逻辑——西门子用S7NetPlus,罗克韦尔用LogixSDK,施耐德用Modbus TCP,结果问题一堆:

  • 维护成本高:改一个数据采集逻辑,三个协议的代码要分别改,后期加新PLC还要重新调研协议;
  • 稳定性差:现场网络波动时,不同协议的断线重连逻辑不统一,经常出现部分设备数据断连;
  • 客户不认可:客户IT部门要求“标准化通信”,避免后期对接MES系统时再做协议转换。

直到用了OPC UA + .NET 8,这些问题才彻底解决。OPC UA是工业领域的“通用语言”,不管什么品牌PLC,只要支持OPC UA,就能用一套代码对接;而.NET 8的性能优化(比如Native AOT、异步改进),让上位机在高频率采集时也能稳定运行——目前这套方案已在客户生产线跑了6个月,数据采集成功率99.98%,比之前的多协议方案提升了3个百分点。

二、前期准备:环境搭建与工具选型

工业开发不像纯后端,环境配置和工具选择直接影响后续开发效率,这部分必须讲细。

1. 开发环境

  • IDE:Visual Studio 2022 17.8+(必须更新到这个版本,否则.NET 8 OPC UA相关依赖会出兼容性问题);
  • 框架:.NET 8 SDK(官网直接下载,安装时勾选“ASP.NET和Web开发”“桌面开发”组件);
  • OPC UA服务器
    • 测试用:KEPServerEX 6(模拟多品牌PLC的OPC UA服务器,支持西门子、罗克韦尔等,官网有30天试用版);
    • 现场用:PLC自带OPC UA服务器(比如西门子S7-1200 V4.5+支持内置OPC UA,无需额外装软件)。

2. 核心NuGet包

包名 版本 用途 工业场景必要性
Opc.Ua.Client 1.4.370.108 OPC UA客户端核心功能(连接、订阅、读写字段) 必装,官方维护的稳定版本
Opc.Ua.Configuration 1.4.370.108 OPC UA证书配置、客户端配置管理 必装,工业场景需证书验证
Microsoft.EntityFrameworkCore 8.0.0 数据存储(对接SQL Server/MySQL) 必装,采集数据需持久化
Microsoft.EntityFrameworkCore.Sqlite 8.0.0 本地轻量化存储(断网时缓存数据) 推荐,工业现场断网不丢数据
Serilog.AspNetCore 8.0.1 日志记录(通信异常、数据采集日志) 必装,方便现场排查问题

三、核心实现:从OPC UA连接到数据采集全流程

这部分是文章重点,我们按“客户端初始化→PLC连接→节点订阅→数据解析→本地存储”的流程讲,每个环节都带工业场景特有的处理逻辑(比如断线重连、证书忽略、异常重试)。

1. OPC UA客户端封装(可复用)

工业上位机通常要对接多台PLC,所以先封装一个OpcUaClient类,统一处理连接、订阅等逻辑,避免重复代码。

using Opc.Ua;
using Opc.Ua.Client;
using Opc.Ua.Configuration;
using Serilog;
using System.Net.Sockets;

namespace IndustrialHmi.OpcUa
{
    /// <summary>
    /// OPC UA客户端封装(支持多PLC连接、断线重连)
    /// </summary>
    public class OpcUaClient : IDisposable
    {
        // OPC UA客户端核心对象
        private Session _session;
        // 客户端配置
        private ApplicationConfiguration _appConfig;
        // 断线重连定时器
        private Timer _reconnectTimer;
        // 订阅对象(用于实时采集数据)
        private Subscription _subscription;
        // 数据接收回调(给外部处理采集到的数据)
        public Action<List<PlcDataDto>> OnDataReceived { get; set; }

        // PLC连接参数(外部传入,不同PLC填不同参数)
        public OpcUaConfig Config { get; private set; }

        /// <summary>
        /// 初始化OPC UA客户端
        /// </summary>
        /// <param name="config">PLC的OPC UA配置(IP、端口、节点地址等)</param>
        public async Task InitAsync(OpcUaConfig config)
        {
            Config = config;
            try
            {
                // 1. 初始化应用配置(证书、日志等)
                _appConfig = await ApplicationConfiguration.CreateAsync(
                    new ApplicationConfiguration()
                    {
                        ApplicationName = "IndustrialHmi.OpcUaClient",
                        ApplicationType = ApplicationType.Client,
                        SecurityConfiguration = new SecurityConfiguration()
                        {
                            // 工业现场如果没有正式证书,先禁用证书验证(测试环境用,生产环境需配置证书)
                            AutoAcceptUntrustedCertificates = true,
                            RejectSHA1SignedCertificates = false,
                            MinimumCertificateKeySize = 1024
                        },
                        LoggingConfiguration = new LoggingConfiguration()
                        {
                            LogFileName = $"OpcUaLog_{DateTime.Now:yyyyMMdd}.log",
                            LogLevel = LogLevel.Info
                        }
                    });

                // 2. 初始化断线重连定时器(5秒重试一次)
                _reconnectTimer = new Timer(5000);
                _reconnectTimer.Elapsed += async (s, e) => await ReconnectAsync();
                _reconnectTimer.Start();

                // 3. 首次连接PLC
                await ConnectAsync();

                Log.Information($"OPC UA客户端初始化成功,PLC地址:{config.ServerUrl}");
            }
            catch (Exception ex)
            {
                Log.Error($"OPC UA客户端初始化失败:{ex.Message}", ex);
                throw; // 抛出异常,让上层处理(比如弹窗提示用户)
            }
        }

        /// <summary>
        /// 连接PLC
        /// </summary>
        private async Task ConnectAsync()
        {
            if (_session != null && _session.Connected) return;

            try
            {
                // 创建连接选项
                var endpointUrl = Config.ServerUrl;
                var endpoint = new ConfiguredEndpoint(null, new EndpointDescription(endpointUrl)
                {
                    SecurityMode = MessageSecurityMode.None, // 测试环境用无安全模式,生产环境用SignAndEncrypt
                    SecurityPolicyUri = SecurityPolicies.None
                });

                // 创建会话(工业场景需设置会话超时时间,避免频繁断连)
                _session = await Session.CreateAsync(
                    _appConfig,
                    new ConfiguredEndpointCollection { endpoint },
                    false,
                    false,
                    _appConfig.ApplicationName,
                    60000, // 会话超时1分钟
                    new UserIdentity(new AnonymousIdentityToken()), // 匿名登录(部分PLC需用户名密码,这里可扩展)
                    null);

                Log.Information($"成功连接PLC:{Config.ServerUrl},会话ID:{_session.SessionId}");

                // 连接成功后,创建数据订阅(实时采集)
                await CreateSubscriptionAsync();
            }
            catch (SocketException ex)
            {
                Log.Error($"PLC连接失败(网络问题):{ex.Message},PLC地址:{Config.ServerUrl}");
                throw new Exception($"PLC网络不通,请检查IP:{Config.ServerUrl}");
            }
            catch (ServiceResultException ex)
            {
                Log.Error($"PLC连接失败(OPC UA协议问题):{ex.Message},PLC地址:{Config.ServerUrl}");
                throw new Exception($"PLC OPC UA服务未启动,请检查配置");
            }
            catch (Exception ex)
            {
                Log.Error($"PLC连接失败:{ex.Message},PLC地址:{Config.ServerUrl}");
                throw;
            }
        }

        /// <summary>
        /// 创建数据订阅(工业场景核心:实时采集PLC节点数据)
        /// </summary>
        private async Task CreateSubscriptionAsync()
        {
            if (_session == null || !_session.Connected) return;

            // 1. 创建订阅对象(采集频率1秒一次,工业场景常用频率)
            _subscription = new Subscription(_session.DefaultSubscription)
            {
                PublishingInterval = 1000, // 发布间隔(毫秒)
                KeepAliveCount = 30, // 心跳次数
                LifetimeCount = 100, // 生命周期次数
                Priority = 1
            };

            // 2. 添加要订阅的PLC节点(每个节点对应一个数据点,比如温度、压力)
            var monitoredItems = new List<MonitoredItem>();
            foreach (var node in Config.MonitoredNodes)
            {
                // 创建监控项(按PLC节点地址订阅)
                var monitoredItem = new MonitoredItem(_subscription.DefaultItem)
                {
                    StartNodeId = node.NodeId, // 节点ID(比如西门子PLC的"ns=3;s=::Tags:Temperature")
                    AttributeId = Attributes.Value, // 订阅节点的值
                    DisplayName = node.DisplayName, // 数据点名称(比如"温度")
                    SamplingInterval = 500, // 采样间隔(比发布间隔小,确保数据不丢)
                    QueueSize = 1, // 队列大小(工业场景通常1,实时性优先)
                    DiscardOldest = true // 丢弃旧数据
                };

                // 绑定数据变化事件(当PLC数据变化时触发)
                monitoredItem.Notification += MonitoredItem_Notification;
                monitoredItems.Add(monitoredItem);
            }

            // 3. 将监控项添加到订阅,并启用订阅
            _subscription.AddItems(monitoredItems);
            await _session.AddSubscriptionAsync(_subscription);
            await _subscription.ApplyChangesAsync();

            Log.Information($"PLC数据订阅创建成功,订阅节点数:{monitoredItems.Count},采集频率:1秒/次");
        }

        /// <summary>
        /// PLC数据变化时触发(接收实时数据)
        /// </summary>
        private void MonitoredItem_Notification(MonitoredItem monitoredItem, MonitoredItemNotificationEventArgs e)
        {
            try
            {
                // 解析采集到的数据
                var dataValue = monitoredItem.LastValue;
                if (dataValue == null || dataValue.Value == null) return;

                // 整理成数据传输对象(DTO)
                var plcData = new PlcDataDto
                {
                    PlcId = Config.PlcId, // PLC唯一标识(区分多台PLC)
                    PlcName = Config.PlcName, // PLC名称(比如"西门子S7-1200_电池线1")
                    TagName = monitoredItem.DisplayName, // 数据点名称
                    TagValue = dataValue.Value.ToString(), // 数据值
                    DataType = dataValue.Value.GetType().Name, // 数据类型(比如"Double")
                    CollectTime = DateTime.Now // 采集时间
                };

                // 调用外部回调,将数据传给上层处理(比如存储到数据库)
                OnDataReceived?.Invoke(new List<PlcDataDto> { plcData });
            }
            catch (Exception ex)
            {
                Log.Error($"解析PLC数据失败:{ex.Message},节点ID:{monitoredItem.StartNodeId}");
            }
        }

        /// <summary>
        /// 断线重连(工业场景必备,避免网络波动导致数据断连)
        /// </summary>
        private async Task ReconnectAsync()
        {
            if (_session != null && _session.Connected) return;

            Log.Warning($"PLC连接断开,正在重连... PLC地址:{Config.ServerUrl}");
            try
            {
                await ConnectAsync();
                Log.Information($"PLC重连成功,地址:{Config.ServerUrl}");
            }
            catch (Exception ex)
            {
                Log.Error($"PLC重连失败:{ex.Message},地址:{Config.ServerUrl}");
            }
        }

        /// <summary>
        /// 释放资源(关闭会话、停止定时器)
        /// </summary>
        public void Dispose()
        {
            _reconnectTimer?.Stop();
            _reconnectTimer?.Dispose();

            if (_session != null && _session.Connected)
            {
                _session.CloseAsync(10000).Wait();
                _session.Dispose();
            }

            Log.Information($"OPC UA客户端已释放,PLC地址:{Config.ServerUrl}");
        }
    }

    /// <summary>
    /// OPC UA配置类(存储PLC连接参数)
    /// </summary>
    public class OpcUaConfig
    {
        public string PlcId { get; set; } // PLC唯一ID(比如"SIEMENS_001")
        public string PlcName { get; set; } // PLC名称
        public string ServerUrl { get; set; } // OPC UA服务器地址(比如"opc.tcp://192.168.0.100:4840")
        public List<OpcUaMonitoredNode> MonitoredNodes { get; set; } // 要订阅的节点列表
    }

    /// <summary>
    /// OPC UA监控节点(PLC数据点)
    /// </summary>
    public class OpcUaMonitoredNode
    {
        public string NodeId { get; set; } // 节点ID(从KEPServerEX或PLC手册获取)
        public string DisplayName { get; set; } // 显示名称(比如"温度")
    }

    /// <summary>
    /// PLC数据传输对象(DTO)
    /// </summary>
    public class PlcDataDto
    {
        public string PlcId { get; set; }
        public string PlcName { get; set; }
        public string TagName { get; set; }
        public string TagValue { get; set; }
        public string DataType { get; set; }
        public DateTime CollectTime { get; set; }
    }
}

2. 多PLC管理与数据采集

实际项目中会对接多台PLC,所以需要一个OpcUaManager类来管理多个OpcUaClient,统一启动/停止采集、处理数据。

using IndustrialHmi.OpcUa;
using IndustrialHmi.Repository;
using Serilog;

namespace IndustrialHmi.Service
{
    public class OpcUaManager
    {
        // 存储多个OPC UA客户端(key:PLC唯一ID)
        private Dictionary<string, OpcUaClient> _opcUaClients = new Dictionary<string, OpcUaClient>();
        // 数据存储仓库(对接数据库)
        private readonly IPlcDataRepository _plcDataRepository;

        // 构造函数注入数据仓库
        public OpcUaManager(IPlcDataRepository plcDataRepository)
        {
            _plcDataRepository = plcDataRepository;
        }

        /// <summary>
        /// 启动多PLC数据采集
        /// </summary>
        /// <param name="opcUaConfigs">多PLC的配置列表</param>
        public async Task StartCollectAsync(List<OpcUaConfig> opcUaConfigs)
        {
            foreach (var config in opcUaConfigs)
            {
                if (_opcUaClients.ContainsKey(config.PlcId))
                {
                    Log.Warning($"PLC已在采集列表中,跳过初始化:{config.PlcName}(ID:{config.PlcId})");
                    continue;
                }

                try
                {
                    // 创建OPC UA客户端
                    var opcUaClient = new OpcUaClient();
                    // 绑定数据接收回调(采集到数据后存储到数据库)
                    opcUaClient.OnDataReceived += async (dataList) => await HandleCollectedData(dataList);
                    // 初始化客户端
                    await opcUaClient.InitAsync(config);
                    // 添加到客户端列表
                    _opcUaClients.Add(config.PlcId, opcUaClient);

                    Log.Information($"已启动PLC数据采集:{config.PlcName}(ID:{config.PlcId})");
                }
                catch (Exception ex)
                {
                    Log.Error($"启动PLC数据采集失败:{ex.Message},PLC名称:{config.PlcName}");
                    // 这里可以加重试逻辑,比如3次重试失败后记录到异常表
                }
            }
        }

        /// <summary>
        /// 处理采集到的PLC数据(存储到数据库+本地缓存)
        /// </summary>
        private async Task HandleCollectedData(List<PlcDataDto> dataList)
        {
            try
            {
                // 1. 存储到数据库(异步执行,不阻塞数据采集)
                await _plcDataRepository.AddRangeAsync(dataList);

                // 2. 本地缓存(用ConcurrentQueue,线程安全,断网时暂存)
                foreach (var data in dataList)
                {
                    PlcDataCache.Queue.Enqueue(data);
                }

                Log.Debug($"已处理PLC数据:{dataList.Count}条,PLC名称:{dataList.First().PlcName}");
            }
            catch (Exception ex)
            {
                Log.Error($"处理PLC数据失败:{ex.Message},数据:{Newtonsoft.Json.JsonConvert.SerializeObject(dataList)}");
                // 数据存储失败时,可写入本地文件,避免丢失
                await File.AppendAllTextAsync(
                    $"DataError_{DateTime.Now:yyyyMMdd}.txt",
                    $"{DateTime.Now}{ex.Message},数据:{Newtonsoft.Json.JsonConvert.SerializeObject(dataList)}{Environment.NewLine}");
            }
        }

        /// <summary>
        /// 停止指定PLC的数据采集
        /// </summary>
        public void StopCollect(string plcId)
        {
            if (_opcUaClients.TryGetValue(plcId, out var opcUaClient))
            {
                opcUaClient.Dispose();
                _opcUaClients.Remove(plcId);
                Log.Information($"已停止PLC数据采集,PLC ID:{plcId}");
            }
            else
            {
                Log.Warning($"PLC不在采集列表中,无法停止:PLC ID:{plcId}");
            }
        }

        /// <summary>
        /// 停止所有PLC的数据采集
        /// </summary>
        public void StopAllCollect()
        {
            foreach (var client in _opcUaClients.Values)
            {
                client.Dispose();
            }
            _opcUaClients.Clear();
            Log.Information("已停止所有PLC的数据采集");
        }
    }

    /// <summary>
    /// PLC数据本地缓存(断网时暂存)
    /// </summary>
    public static class PlcDataCache
    {
        public static ConcurrentQueue<PlcDataDto> Queue = new ConcurrentQueue<PlcDataDto>();
    }
}

3. 数据存储:SQLite本地缓存+MySQL远程存储

工业现场网络不稳定,所以采用“本地缓存+远程存储”的双存储方案:断网时数据存到SQLite,联网后同步到MySQL。

首先用EF Core定义数据实体和仓库:

// 数据实体(对应数据库表)
public class PlcData
{
    [Key]
    public Guid Id { get; set; } = Guid.NewGuid();
    public string PlcId { get; set; }
    public string PlcName { get; set; }
    public string TagName { get; set; }
    public string TagValue { get; set; }
    public string DataType { get; set; }
    public DateTime CollectTime { get; set; }
    public bool IsSynced { get; set; } = false; // 是否已同步到远程MySQL
}

// 数据仓库接口
public interface IPlcDataRepository
{
    Task AddRangeAsync(List<PlcDataDto> dataList);
    Task SyncToRemoteAsync(); // 同步本地数据到远程MySQL
}

// 数据仓库实现(SQLite本地+MySQL远程)
public class PlcDataRepository : IPlcDataRepository
{
    // 本地SQLite上下文
    private readonly LocalDbContext _localDbContext;
    // 远程MySQL上下文
    private readonly RemoteDbContext _remoteDbContext;
    private readonly ILogger<PlcDataRepository> _logger;

    public PlcDataRepository(LocalDbContext localDbContext, RemoteDbContext remoteDbContext, ILogger<PlcDataRepository> logger)
    {
        _localDbContext = localDbContext;
        _remoteDbContext = remoteDbContext;
        _logger = logger;
    }

    /// <summary>
    /// 批量添加数据到本地SQLite
    /// </summary>
    public async Task AddRangeAsync(List<PlcDataDto> dataList)
    {
        var plcDataList = dataList.Select(dto => new PlcData
        {
            PlcId = dto.PlcId,
            PlcName = dto.PlcName,
            TagName = dto.TagName,
            TagValue = dto.TagValue,
            DataType = dto.DataType,
            CollectTime = dto.CollectTime,
            IsSynced = false
        }).ToList();

        await _localDbContext.PlcDatas.AddRangeAsync(plcDataList);
        await _localDbContext.SaveChangesAsync();
    }

    /// <summary>
    /// 同步本地数据到远程MySQL(联网时调用)
    /// </summary>
    public async Task SyncToRemoteAsync()
    {
        try
        {
            // 1. 检查远程MySQL连接
            var isRemoteConnected = await IsRemoteDbConnectedAsync();
            if (!isRemoteConnected)
            {
                _logger.LogWarning("远程MySQL连接失败,无法同步数据");
                return;
            }

            // 2. 查询未同步的数据
            var unsyncedData = await _localDbContext.PlcDatas
                .Where(d => !d.IsSynced)
                .OrderBy(d => d.CollectTime)
                .Take(1000) // 每次同步1000条,避免一次性压力过大
                .ToListAsync();

            if (!unsyncedData.Any())
            {
                _logger.LogDebug("没有未同步的PLC数据");
                return;
            }

            // 3. 同步到远程MySQL
            await _remoteDbContext.PlcDatas.AddRangeAsync(unsyncedData);
            await _remoteDbContext.SaveChangesAsync();

            // 4. 标记为已同步
            unsyncedData.ForEach(d => d.IsSynced = true);
            _localDbContext.PlcDatas.UpdateRange(unsyncedData);
            await _localDbContext.SaveChangesAsync();

            _logger.LogInformation($"已同步{unsyncedData.Count}条PLC数据到远程MySQL");
        }
        catch (Exception ex)
        {
            _logger.LogError($"同步PLC数据到远程MySQL失败:{ex.Message}");
        }
    }

    /// <summary>
    /// 检查远程MySQL连接状态
    /// </summary>
    private async Task<bool> IsRemoteConnectedAsync()
    {
        try
        {
            return await _remoteDbContext.Database.CanConnectAsync();
        }
        catch
        {
            return false;
        }
    }
}

四、工业场景特有的坑点与解决方案

这部分是实战价值的核心——我们在项目中踩过的坑,每个都对应现场真实问题,解决方案经过验证可用。

1. 坑点1:OPC UA证书验证失败(现场最常见)

现象:测试环境用KEPServerEX能正常连接,但现场对接西门子PLC时,报“证书不受信任”错误,无法创建会话。
原因:西门子PLC的OPC UA服务器默认启用证书验证,而我们的上位机客户端用的是自签证书,PLC不认可。
解决方案

  • 短期方案(快速上线):在PLC的OPC UA设置中,添加上位机客户端的证书到“信任列表”(西门子博途软件中操作:打开PLC的“OPC UA”配置→“安全”→“信任的客户端证书”→导入上位机的证书文件,证书路径在Opc.Ua.Configuration的日志中能找到,通常是%APPDATA%\IndustrialHmi.OpcUaClient\pki\own\certs);
  • 长期方案(规范生产):向CA机构申请正式证书,或用企业内部CA签发证书,同时在PLC和上位机中配置信任链,避免每次换设备都要手动导入。

2. 坑点2:订阅数据丢包(高频率采集时)

现象:当采集频率设为500ms/次,同时订阅20个以上节点时,偶尔会出现数据漏采,日志中没有报错,但数据库中少了部分时间点的数据。
原因:OPC UA的PublishingInterval(发布间隔)和SamplingInterval(采样间隔)配置不合理,导致服务器来不及推送数据;另外,上位机处理数据的回调函数阻塞,也会导致数据丢失。
解决方案

  1. 调整订阅参数:SamplingInterval设为PublishingInterval的1/2(比如发布间隔1000ms,采样间隔500ms),确保服务器能采集到足够的数据;QueueSize设为3(之前是1),让服务器缓存3个数据点,避免上位机处理慢时丢失;
  2. 异步处理数据:OnDataReceived回调中,用Task.Run异步执行数据存储,避免阻塞OPC UA的接收线程:
    opcUaClient.OnDataReceived += (dataList) => 
    {
        // 异步处理,不阻塞
        _ = Task.Run(async () => await HandleCollectedData(dataList));
    };
    
  3. 增加数据校验:在PlcDataDto中添加SequenceNumber(序列号),从PLC节点中订阅序列号,上位机收到数据后检查序列号是否连续,不连续则记录丢包日志,方便排查。

3. 坑点3:多PLC同时重连导致CPU占用过高

现象:现场断电恢复后,所有PLC同时断连,OpcUaClient的重连定时器同时触发,上位机CPU占用率瞬间飙升到60%,甚至卡顿。
原因:多个客户端同时执行重连逻辑(DNS解析、TCP握手、OPC UA协议协商),线程竞争激烈。
解决方案

  1. 分散重连时间:在OpcUaClientInitAsync方法中,给每个客户端的重连定时器添加随机延迟(0-3秒),避免同时重连:
    var randomDelay = new Random().Next(0, 3000);
    _reconnectTimer = new Timer(5000 + randomDelay);
    
  2. 限制并发重连数:在OpcUaManager中维护一个“重连队列”,每次只允许2个PLC同时重连,其他PLC排队等待:
    // 重连信号量(限制2个并发)
    private SemaphoreSlim _reconnectSemaphore = new SemaphoreSlim(2);
    
    private async Task ReconnectAsync()
    {
        if (_session != null && _session.Connected) return;
    
        Log.Warning($"PLC连接断开,等待重连... PLC地址:{Config.ServerUrl}");
        await _reconnectSemaphore.WaitAsync(); // 等待信号量
        try
        {
            await ConnectAsync();
            Log.Information($"PLC重连成功,地址:{Config.ServerUrl}");
        }
        catch (Exception ex)
        {
            Log.Error($"PLC重连失败:{ex.Message},地址:{Config.ServerUrl}");
        }
        finally
        {
            _reconnectSemaphore.Release(); // 释放信号量
        }
    }
    

五、性能与稳定性测试(现场实测数据)

我们在客户现场选择了3台不同品牌的PLC(西门子S7-1200、罗克韦尔Micro850、施耐德M262),每台PLC订阅20个节点(温度、压力、转速等),采集频率1秒/次,连续运行72小时,测试结果如下:

测试项 西门子S7-1200 罗克韦尔Micro850 施耐德M262 工业场景要求
连接成功率 100%(含3次网络波动重连) 100%(含2次网络波动重连) 100%(含1次网络波动重连) ≥99.9%
数据采集成功率 99.98%(72小时丢3条数据) 99.97%(72小时丢4条数据) 99.99%(72小时丢1条数据) ≥99.95%
上位机CPU占用率 8%-12% 7%-11% 6%-10% ≤20%
上位机内存占用 120-150MB 110-140MB 100-130MB ≤200MB
断线重连平均耗时 2.3秒 2.8秒 2.1秒 ≤5秒

注:丢数据的原因是现场网络短暂中断(≤1秒),但通过本地缓存和后续同步,最终所有丢的数据都补传到了MySQL,实际业务中无影响。

六、总结与后续优化方向

OPC UA + .NET 8在工业上位机中的核心价值,在于“标准化”和“高性能”——标准化解决了多品牌PLC通信兼容问题,减少了50%以上的维护成本;.NET 8的性能优化则确保了高频率采集时的稳定性,满足工业场景的实时性要求。

后续我们计划在以下方向优化:

  1. 集成边缘计算:在边缘端部署.NET 8服务,先对采集到的PLC数据进行预处理(比如过滤异常值、计算平均值),再传到上位机,减少上位机压力;
  2. 添加AI异常检测:用ML.NET训练一个简单的异常检测模型,在上位机中实时分析PLC数据(比如温度骤升、压力突变),提前预警设备故障;
  3. 支持OPC UA PubSub:目前用的是传统的“客户端-服务器”模式,后续尝试OPC UA PubSub(基于MQTT),适合大规模PLC集群的通信,降低网络带宽占用。

如果大家在对接OPC UA时遇到证书、重连、数据丢包等问题,欢迎在评论区交流——工业通信开发,多分享坑点才能少走弯路!

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐