From 5dd0ed8e394879e94ca2093adcfff011293d1f13 Mon Sep 17 00:00:00 2001 From: "David P.G" Date: Tue, 8 Jul 2025 20:19:06 +0800 Subject: [PATCH] =?UTF-8?q?1=20feat:=20=E5=B0=86=20OpcUaEndpointUrl=20?= =?UTF-8?q?=E7=A7=BB=E5=8A=A8=E5=88=B0=20Device=20=E5=B9=B6=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=20OpcUaBackgroundService=202=20=E5=B0=86=20OpcUaEndpo?= =?UTF-8?q?intUrl=20=E5=B1=9E=E6=80=A7=E4=BB=8E=20VariableData=20=E5=92=8C?= =?UTF-8?q?=20DbVariableData=20=E7=A7=BB=E5=8A=A8=E5=88=B0=20Device=20?= =?UTF-8?q?=E5=92=8C=20DbDevice=E3=80=82=203=20=E6=9B=B4=E6=96=B0=20OpcUaB?= =?UTF-8?q?ackgroundService=20=E4=BB=A5=E4=BB=8E=E5=85=B3=E8=81=94?= =?UTF-8?q?=E7=9A=84=20Device=20=E5=AF=B9=E8=B1=A1=E4=B8=AD=E6=A3=80?= =?UTF-8?q?=E7=B4=A2=20OpcUaEndpointUrl=E3=80=82=204=20=E7=A1=AE=E4=BF=9D?= =?UTF-8?q?=20DataServices=20=E5=92=8C=20VarDataRepository=20=E6=AD=A3?= =?UTF-8?q?=E7=A1=AE=E5=8A=A0=E8=BD=BD=E5=85=B3=E8=81=94=E7=9A=84=20Variab?= =?UTF-8?q?leTable=20=E5=92=8C=20Device=20=E6=95=B0=E6=8D=AE=E3=80=82=205?= =?UTF-8?q?=20=E5=9C=A8=20VariableData=20=E6=A8=A1=E5=9E=8B=E4=B8=AD?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20VariableTable=20=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E4=BB=A5=E6=AD=A3=E7=A1=AE=E8=A7=A3=E6=9E=90=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.idea.PMSWPF/.idea/.gitignore | 13 ++++++ .idea/.idea.PMSWPF/.idea/encodings.xml | 4 ++ .idea/.idea.PMSWPF/.idea/indexLayout.xml | 8 ++++ .idea/.idea.PMSWPF/.idea/vcs.xml | 6 +++ Data/Entities/DbDevice.cs | 6 +++ Data/Repositories/DeviceRepository.cs | 6 ++- Data/Repositories/VarDataRepository.cs | 2 + Models/Device.cs | 6 +++ Models/VariableData.cs | 5 +++ Services/DataServices.cs | 10 +++++ Services/OpcUaBackgroundService.cs | 50 +++++++++++++++--------- 11 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 .idea/.idea.PMSWPF/.idea/.gitignore create mode 100644 .idea/.idea.PMSWPF/.idea/encodings.xml create mode 100644 .idea/.idea.PMSWPF/.idea/indexLayout.xml create mode 100644 .idea/.idea.PMSWPF/.idea/vcs.xml diff --git a/.idea/.idea.PMSWPF/.idea/.gitignore b/.idea/.idea.PMSWPF/.idea/.gitignore new file mode 100644 index 0000000..0b5e731 --- /dev/null +++ b/.idea/.idea.PMSWPF/.idea/.gitignore @@ -0,0 +1,13 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# Rider 忽略的文件 +/.idea.PMSWPF.iml +/projectSettingsUpdater.xml +/modules.xml +/contentModel.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.PMSWPF/.idea/encodings.xml b/.idea/.idea.PMSWPF/.idea/encodings.xml new file mode 100644 index 0000000..df87cf9 --- /dev/null +++ b/.idea/.idea.PMSWPF/.idea/encodings.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/.idea.PMSWPF/.idea/indexLayout.xml b/.idea/.idea.PMSWPF/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.PMSWPF/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.PMSWPF/.idea/vcs.xml b/.idea/.idea.PMSWPF/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.PMSWPF/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Data/Entities/DbDevice.cs b/Data/Entities/DbDevice.cs index cd28670..ceb9b3f 100644 --- a/Data/Entities/DbDevice.cs +++ b/Data/Entities/DbDevice.cs @@ -79,6 +79,12 @@ public class DbDevice [SugarColumn(ColumnDataType = "varchar(20)", SqlParameterDbType = typeof(EnumToStringConvert))] public ProtocolType ProtocolType { get; set; } + /// + /// OPC UA Endpoint URL。 + /// + [SugarColumn(IsNullable = true)] + public string? OpcUaEndpointUrl { get; set; } + /// /// 设备关联的变量表列表。 /// diff --git a/Data/Repositories/DeviceRepository.cs b/Data/Repositories/DeviceRepository.cs index 478bbb4..8847a2f 100644 --- a/Data/Repositories/DeviceRepository.cs +++ b/Data/Repositories/DeviceRepository.cs @@ -89,7 +89,7 @@ public class DeviceRepository /// /// 设备ID。 /// 对应的DbDevice对象。 - public async Task GetById(int id) + public async Task GetById(int id) { Stopwatch stopwatch = new Stopwatch(); stopwatch.Start(); @@ -99,7 +99,7 @@ public class DeviceRepository .FirstAsync(p => p.Id == id); stopwatch.Stop(); NlogHelper.Info($"根据ID '{id}' 获取设备耗时:{stopwatch.ElapsedMilliseconds}ms"); - return result; + return result.CopyTo(); } } @@ -252,4 +252,6 @@ public class DeviceRepository await _menuRepository.AddVarTableMenu(addDevice, addDeviceMenuId, db); return addDevice.CopyTo(); } + + } \ No newline at end of file diff --git a/Data/Repositories/VarDataRepository.cs b/Data/Repositories/VarDataRepository.cs index 7403905..78cab22 100644 --- a/Data/Repositories/VarDataRepository.cs +++ b/Data/Repositories/VarDataRepository.cs @@ -46,6 +46,8 @@ public class VarDataRepository using (var _db = DbContext.GetInstance()) { var result = await _db.Queryable() + .Includes(d => d.VariableTable) + .Includes(d => d.VariableTable.Device) .ToListAsync(); stopwatch.Stop(); NlogHelper.Info($"获取所有VariableData耗时:{stopwatch.ElapsedMilliseconds}ms"); diff --git a/Models/Device.cs b/Models/Device.cs index bf99298..e681023 100644 --- a/Models/Device.cs +++ b/Models/Device.cs @@ -88,6 +88,12 @@ public partial class Device : ObservableObject /// public ProtocolType ProtocolType { get; set; } + /// + /// OPC UA Endpoint URL。 + /// + [ObservableProperty] + private string? opcUaEndpointUrl; + /// /// 设备关联的变量表列表。 /// diff --git a/Models/VariableData.cs b/Models/VariableData.cs index 5ace03a..3ca0349 100644 --- a/Models/VariableData.cs +++ b/Models/VariableData.cs @@ -154,6 +154,11 @@ public partial class VariableData : ObservableObject /// public int VariableTableId { get; set; } + /// + /// 关联的变量表实体。 + /// + public VariableTable? VariableTable { get; set; } + /// /// 关联的MQTT配置列表。 /// diff --git a/Services/DataServices.cs b/Services/DataServices.cs index 801da7d..8244896 100644 --- a/Services/DataServices.cs +++ b/Services/DataServices.cs @@ -193,6 +193,16 @@ public partial class DataServices : ObservableRecipient, IRecipient return await _varDataRepository.GetAllAsync(); } + /// + /// 异步根据ID获取设备数据。 + /// + /// 设备ID。 + /// 设备对象,如果不存在则为null。 + public async Task GetDeviceByIdAsync(int id) + { + return await _deviceRepository.GetById(id); + } + /// /// 异步加载变量数据。 /// diff --git a/Services/OpcUaBackgroundService.cs b/Services/OpcUaBackgroundService.cs index b670eff..f855362 100644 --- a/Services/OpcUaBackgroundService.cs +++ b/Services/OpcUaBackgroundService.cs @@ -151,8 +151,13 @@ namespace PMSWPF.Services { if (_opcUaVariables.TryGetValue(id, out var variable)) { - // 断开与该变量相关的 OPC UA 会话。 - await DisconnectOpcUaSession(variable.OpcUaEndpointUrl); + // 获取关联的设备信息 + var device = await _dataServices.GetDeviceByIdAsync(variable.VariableTable.DeviceId??0); + if (device != null) + { + // 断开与该变量相关的 OPC UA 会话。 + await DisconnectOpcUaSession(device.OpcUaEndpointUrl); + } _opcUaVariables.Remove(id); } } @@ -160,20 +165,28 @@ namespace PMSWPF.Services // 处理新增或更新的变量。 foreach (var variable in opcUaVariables) { + // 获取关联的设备信息 + var device = await _dataServices.GetDeviceByIdAsync(variable.VariableTable.DeviceId??0); + if (device == null) + { + NlogHelper.Warn($"变量 '{variable.Name}' (ID: {variable.Id}) 关联的设备不存在。"); + continue; + } + if (!_opcUaVariables.ContainsKey(variable.Id)) { // 如果是新变量,则添加到字典并建立连接和订阅。 _opcUaVariables.Add(variable.Id, variable); - await ConnectAndSubscribeOpcUa(variable); + await ConnectAndSubscribeOpcUa(variable, device); } else { // 如果变量已存在,则更新其信息。 _opcUaVariables[variable.Id] = variable; // 如果终结点 URL 对应的会话已断开,则尝试重新连接。 - if (_opcUaSessions.ContainsKey(variable.OpcUaEndpointUrl) && !_opcUaSessions[variable.OpcUaEndpointUrl].Connected) + if (_opcUaSessions.ContainsKey(device.OpcUaEndpointUrl) && !_opcUaSessions[device.OpcUaEndpointUrl].Connected) { - await ConnectAndSubscribeOpcUa(variable); + await ConnectAndSubscribeOpcUa(variable, device); } } } @@ -183,10 +196,11 @@ namespace PMSWPF.Services /// 连接到 OPC UA 服务器并订阅指定的变量。 /// /// 要订阅的变量信息。 - private async Task ConnectAndSubscribeOpcUa(VariableData variable) + /// 变量所属的设备信息。 + private async Task ConnectAndSubscribeOpcUa(VariableData variable, Device device) { NlogHelper.Info($"正在为变量 '{variable.Name}' 连接和订阅 OPC UA 服务器..."); - if (string.IsNullOrEmpty(variable.OpcUaEndpointUrl) || string.IsNullOrEmpty(variable.OpcUaNodeId)) + if (string.IsNullOrEmpty(device.OpcUaEndpointUrl) || string.IsNullOrEmpty(variable.OpcUaNodeId)) { NlogHelper.Warn($"OPC UA variable {variable.Name} has invalid EndpointUrl or NodeId."); return; @@ -194,9 +208,9 @@ namespace PMSWPF.Services Session session = null; // 检查是否已存在到该终结点的活动会话。 - if (_opcUaSessions.TryGetValue(variable.OpcUaEndpointUrl, out session) && session.Connected) + if (_opcUaSessions.TryGetValue(device.OpcUaEndpointUrl, out session) && session.Connected) { - NlogHelper.Info($"Already connected to OPC UA endpoint: {variable.OpcUaEndpointUrl}"); + NlogHelper.Info($"Already connected to OPC UA endpoint: {device.OpcUaEndpointUrl}"); } else { @@ -221,12 +235,12 @@ namespace PMSWPF.Services } // 4. 发现服务器提供的终结点。 - DiscoveryClient discoveryClient = DiscoveryClient.Create(new Uri(variable.OpcUaEndpointUrl)); - EndpointDescriptionCollection endpoints = discoveryClient.GetEndpoints(new Opc.Ua.StringCollection { variable.OpcUaEndpointUrl }); + DiscoveryClient discoveryClient = DiscoveryClient.Create(new Uri(device.OpcUaEndpointUrl)); + EndpointDescriptionCollection endpoints = discoveryClient.GetEndpoints(new Opc.Ua.StringCollection { device.OpcUaEndpointUrl }); // 简化处理:选择第一个无安全策略的终结点。在生产环境中应选择合适的安全策略。 // ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(null, endpoints.First(e => e.SecurityMode == MessageSecurityMode.None), config); - EndpointDescription selectedEndpoint = CoreClientUtils.SelectEndpoint(application.ApplicationConfiguration, variable.OpcUaEndpointUrl, false); + EndpointDescription selectedEndpoint = CoreClientUtils.SelectEndpoint(application.ApplicationConfiguration, device.OpcUaEndpointUrl, false); EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(application.ApplicationConfiguration); ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration); @@ -240,9 +254,9 @@ namespace PMSWPF.Services new UserIdentity(new AnonymousIdentityToken()), // 使用匿名用户身份 null); - _opcUaSessions[variable.OpcUaEndpointUrl] = session; - NlogHelper.Info($"Connected to OPC UA server: {variable.OpcUaEndpointUrl}"); - NotificationHelper.ShowSuccess($"已连接到 OPC UA 服务器: {variable.OpcUaEndpointUrl}"); + _opcUaSessions[device.OpcUaEndpointUrl] = session; + NlogHelper.Info($"Connected to OPC UA server: {device.OpcUaEndpointUrl}"); + NotificationHelper.ShowSuccess($"已连接到 OPC UA 服务器: {device.OpcUaEndpointUrl}"); // 6. 创建订阅。 Subscription subscription = new Subscription(session.DefaultSubscription); @@ -250,7 +264,7 @@ namespace PMSWPF.Services session.AddSubscription(subscription); subscription.Create(); - _opcUaSubscriptions[variable.OpcUaEndpointUrl] = subscription; + _opcUaSubscriptions[device.OpcUaEndpointUrl] = subscription; // 7. 创建监控项并添加到订阅中。 MonitoredItem monitoredItem = new MonitoredItem(subscription.DefaultItem); @@ -268,8 +282,8 @@ namespace PMSWPF.Services } catch (Exception ex) { - NlogHelper.Error($"连接或订阅 OPC UA 服务器失败: {variable.OpcUaEndpointUrl} - {ex.Message}", ex); - NotificationHelper.ShowError($"连接或订阅 OPC UA 服务器失败: {variable.OpcUaEndpointUrl} - {ex.Message}", ex); + NlogHelper.Error($"连接或订阅 OPC UA 服务器失败: {device.OpcUaEndpointUrl} - {ex.Message}", ex); + NotificationHelper.ShowError($"连接或订阅 OPC UA 服务器失败: {device.OpcUaEndpointUrl} - {ex.Message}", ex); } } }