diff --git a/Data/Entities/DbVariableData.cs b/Data/Entities/DbVariableData.cs index e1daea4..742a7b0 100644 --- a/Data/Entities/DbVariableData.cs +++ b/Data/Entities/DbVariableData.cs @@ -29,6 +29,7 @@ public class DbVariableData /// /// 节点ID,用于标识变量在设备或系统中的唯一路径。 /// + [SugarColumn(IsNullable = true)] public string S7Address { get; set; } = String.Empty; /// @@ -94,7 +95,8 @@ public class DbVariableData /// /// 轮询级别,例如1秒、5秒等。 /// - [SugarColumn(ColumnDataType = "varchar(20)", SqlParameterDbType = typeof(EnumToStringConvert))] + + [SugarColumn(ColumnDataType = "varchar(20)",IsNullable =true, SqlParameterDbType = typeof(EnumToStringConvert))] public PollLevelType PollLevelType { get; set; } /// diff --git a/Models/OpcUaNode.cs b/Models/OpcUaNode.cs new file mode 100644 index 0000000..449dc6d --- /dev/null +++ b/Models/OpcUaNode.cs @@ -0,0 +1,73 @@ +using System.Collections.ObjectModel; +using CommunityToolkit.Mvvm.ComponentModel; +using Opc.Ua; + +namespace PMSWPF.Models; + +/// +/// 表示OPC UA节点,用于构建节点树。 +/// +public partial class OpcUaNode : ObservableObject +{ + /// + /// 节点的显示名称。 + /// + public string DisplayName { get; set; } + + /// + /// 节点的唯一标识符。 + /// + public NodeId NodeId { get; set; } + + /// + /// 节点的类型(例如,文件夹、变量)。 + /// + public NodeType NodeType { get; set; } + + /// + /// 子节点集合。 + /// + [ObservableProperty] + private ObservableCollection _children; + + /// + /// 指示节点是否已加载子节点。 + /// + [ObservableProperty] + private bool _isLoaded; + + /// + /// 指示节点是否正在加载子节点。 + /// + [ObservableProperty] + private bool _isLoading; + + /// + /// 节点的完整路径(可选,用于调试或显示)。 + /// + public string Path { get; set; } + + /// + /// 构造函数。 + /// + /// 显示名称。 + /// 节点ID。 + /// 节点类型。 + public OpcUaNode(string displayName, NodeId nodeId, NodeType nodeType) + { + DisplayName = displayName; + NodeId = nodeId; + NodeType = nodeType; + Children = new ObservableCollection(); + } +} + +/// +/// OPC UA节点类型枚举。 +/// +public enum NodeType +{ + Folder, + Object, + Variable +} diff --git a/PMSWPF.OpcUaClient.Config.xml b/PMSWPF.OpcUaClient.Config.xml new file mode 100644 index 0000000..093bf05 --- /dev/null +++ b/PMSWPF.OpcUaClient.Config.xml @@ -0,0 +1,58 @@ + + + PMSWPF OPC UA Client + urn:{System.Net.Dns.GetHostName()}:PMSWPF.OpcUaClient + 0 + + + + Directory + %CommonApplicationData%/OPC Foundation/CertificateStores/MachineDefault + CN=PMSWPF OPC UA Client, O=OPC Foundation, OU=UA Applications + + + Directory + %CommonApplicationData%/OPC Foundation/CertificateStores/UA Certificate Authorities + + + Directory + %CommonApplicationData%/OPC Foundation/CertificateStores/UA Applications + + + Directory + %CommonApplicationData%/OPC Foundation/CertificateStores/RejectedCertificates + + true + + + + 15000 + 4194304 + 65535 + 65535 + 4194304 + + + + 60000 + + + + + en-US + + 10000 + + + + ./Logs/OpcUaClient.log + true + 1023 + + + + \ No newline at end of file diff --git a/PMSWPF.csproj b/PMSWPF.csproj index e7fd96c..e9bdeec 100644 --- a/PMSWPF.csproj +++ b/PMSWPF.csproj @@ -37,6 +37,11 @@ Always + + + Always + + diff --git a/Services/DialogService.cs b/Services/DialogService.cs index e2a5f38..186b1a3 100644 --- a/Services/DialogService.cs +++ b/Services/DialogService.cs @@ -177,10 +177,14 @@ public class DialogService :IDialogService var vm = new MqttSelectionDialogViewModel(); var dialog = new MqttSelectionDialog(vm); var result = await dialog.ShowAsync(); - if (result == ContentDialogResult.Primary) - { - return vm.SelectedMqtt; - } - return null; + return result == ContentDialogResult.Primary ? vm.SelectedMqtt : null; + } + + public async Task> ShowOpcUaImportDialog() + { + var vm= new OpcUaImportDialogViewModel(); + var dialog = new OpcUaImportDialog(vm); + var result = await dialog.ShowAsync(); + return result == ContentDialogResult.Primary ? vm.GetSelectedVariables().ToList() : null; } } \ No newline at end of file diff --git a/Services/IDialogService.cs b/Services/IDialogService.cs index 925b516..ee4a203 100644 --- a/Services/IDialogService.cs +++ b/Services/IDialogService.cs @@ -22,4 +22,5 @@ public interface IDialogService ContentDialog ShowProcessingDialog(string title, string message); Task ShowPollLevelDialog(PollLevelType pollLevelType); Task ShowMqttSelectionDialog(); + Task> ShowOpcUaImportDialog(); } \ No newline at end of file diff --git a/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs b/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs new file mode 100644 index 0000000..b6f828f --- /dev/null +++ b/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs @@ -0,0 +1,340 @@ +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; +using PMSWPF.Enums; +using PMSWPF.Helper; +using PMSWPF.Models; + +namespace PMSWPF.ViewModels.Dialogs; + +public partial class OpcUaImportDialogViewModel : ObservableObject +{ + [ObservableProperty] + private string _endpointUrl = "opc.tcp://127.0.0.1:4855"; // 默认值 + + [ObservableProperty] + private ObservableCollection _opcUaNodes; + + [ObservableProperty] + private ObservableCollection _selectedNodeVariables; + + [ObservableProperty] + private bool _selectAllVariables; + + private Session _session; + + public OpcUaImportDialogViewModel() + { + OpcUaNodes = new ObservableCollection(); + SelectedNodeVariables = new ObservableCollection(); + } + + [RelayCommand] + private async Task Connect() + { + try + { + // 断开现有连接 + if (_session != null && _session.Connected) + { + _session.Close(); + _session.Dispose(); + _session = null; + } + + /*ApplicationInstance application = new ApplicationInstance + { + ApplicationName = "PMSWPF OPC UA Client", + ApplicationType = ApplicationType.Client, + ConfigSectionName = "PMSWPF.OpcUaClient" + }; + + ApplicationConfiguration config = await application.LoadApplicationConfiguration(false); + + // var config = new ApplicationConfiguration() + // { + // ApplicationName = application.ApplicationName, + // ApplicationUri = $"urn:{System.Net.Dns.GetHostName()}:OpcUADemoClient", + // ApplicationType = application.ApplicationType, + // SecurityConfiguration = new SecurityConfiguration + // { + // ApplicationCertificate = new CertificateIdentifier { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/MachineDefault", SubjectName = application.ApplicationName }, + // TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/UA Certificate Authorities" }, + // TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/UA Applications" }, + // RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/RejectedCertificates" }, + // AutoAcceptUntrustedCertificates = true // 自动接受不受信任的证书 (仅用于测试) + // }, + // TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, + // ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }, + // TraceConfiguration = new TraceConfiguration { OutputFilePath = "./Logs/OpcUaClient.log", DeleteOnLoad = true, TraceMasks = Utils.TraceMasks.Error | Utils.TraceMasks.Security } + // }; + // + // bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0); + // if (!haveAppCertificate) + // { + // throw new Exception("Application instance certificate invalid!"); + // } + // + // EndpointDescription selectedEndpoint + // = CoreClientUtils.SelectEndpoint(application.ApplicationConfiguration, EndpointUrl, false); + // EndpointConfiguration endpointConfiguration + // = EndpointConfiguration.Create(application.ApplicationConfiguration); + // ConfiguredEndpoint configuredEndpoint + // = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration); + // var config = new ApplicationConfiguration() + // { + // ApplicationName = application.ApplicationName, + // ApplicationUri = $"urn:{System.Net.Dns.GetHostName()}:OpcUADemoClient", + // ApplicationType = application.ApplicationType, + // SecurityConfiguration = new SecurityConfiguration + // { + // ApplicationCertificate = new CertificateIdentifier { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/MachineDefault", SubjectName = application.ApplicationName }, + // TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/UA Certificate Authorities" }, + // TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/UA Applications" }, + // RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/RejectedCertificates" }, + // AutoAcceptUntrustedCertificates = true // 自动接受不受信任的证书 (仅用于测试) + // }, + // TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, + // ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }, + // TraceConfiguration = new TraceConfiguration { OutputFilePath = "./Logs/OpcUaClient.log", DeleteOnLoad = true, TraceMasks = Utils.TraceMasks.Error | Utils.TraceMasks.Security } + // }; + + bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0); + if (!haveAppCertificate) + { + throw new Exception("Application instance certificate invalid!"); + } + + EndpointDescription selectedEndpoint + = CoreClientUtils.SelectEndpoint(application.ApplicationConfiguration, EndpointUrl, false); + EndpointConfiguration endpointConfiguration + = EndpointConfiguration.Create(application.ApplicationConfiguration); + ConfiguredEndpoint configuredEndpoint + = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration); + + _session = await Session.Create( + config, + configuredEndpoint, + false, + "PMSWPF OPC UA Session", + 60000, + new UserIdentity(new AnonymousIdentityToken()), + null); + */ + + + + // 1. 创建应用程序配置 + var application = new ApplicationInstance + { + ApplicationName = "OpcUADemoClient", + ApplicationType = ApplicationType.Client, + ConfigSectionName = "Opc.Ua.Client" + }; + + var config = new ApplicationConfiguration() + { + ApplicationName = application.ApplicationName, + ApplicationUri = $"urn:{System.Net.Dns.GetHostName()}:OpcUADemoClient", + ApplicationType = application.ApplicationType, + SecurityConfiguration = new SecurityConfiguration + { + ApplicationCertificate = new CertificateIdentifier { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/MachineDefault", SubjectName = application.ApplicationName }, + TrustedIssuerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/UA Certificate Authorities" }, + TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/UA Applications" }, + RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = "%CommonApplicationData%/OPC Foundation/CertificateStores/RejectedCertificates" }, + AutoAcceptUntrustedCertificates = true // 自动接受不受信任的证书 (仅用于测试) + }, + TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, + ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }, + TraceConfiguration = new TraceConfiguration { OutputFilePath = "./Logs/OpcUaClient.log", DeleteOnLoad = true, TraceMasks = Utils.TraceMasks.Error | Utils.TraceMasks.Security } + }; + application.ApplicationConfiguration = config; + + // 验证并检查证书 + await config.Validate(ApplicationType.Client); + await application.CheckApplicationInstanceCertificate(false, 0); + + // 2. 查找并选择端点 (将 useSecurity 设置为 false 以进行诊断) + var selectedEndpoint = CoreClientUtils.SelectEndpoint(EndpointUrl, false); + + _session = await Session.Create( + config, + new ConfiguredEndpoint(null, selectedEndpoint, EndpointConfiguration.Create(config)), + false, + "PMSWPF OPC UA Session", + 60000, + new UserIdentity(new AnonymousIdentityToken()), + null); + + NotificationHelper.ShowSuccess($"已连接到 OPC UA 服务器: {EndpointUrl}"); + + // 浏览根节点 + await BrowseNodes(OpcUaNodes, ObjectIds.ObjectsFolder); + } + catch (Exception ex) + { + NlogHelper.Error($"连接 OPC UA 服务器失败: {EndpointUrl} - {ex.Message}", ex); + NotificationHelper.ShowError($"连接 OPC UA 服务器失败: {EndpointUrl} - {ex.Message}", ex); + } + } + /// + /// 处理来自服务器的数据变化通知 + /// + private static void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e) + { + foreach (var value in item.DequeueValues()) + { + Console.WriteLine($"[通知] {item.DisplayName}: {value.Value} | 时间戳: {value.SourceTimestamp.ToLocalTime()} | 状态: {value.StatusCode}"); + } + } + + private async Task BrowseNodes(ObservableCollection nodes, NodeId parentNodeId) + { + try + { + Opc.Ua.ReferenceDescriptionCollection references; + byte[] continuationPoint = null; + + _session.Browse( + null, // RequestHeader + new ViewDescription(), + parentNodeId, + 0u, + BrowseDirection.Forward, + Opc.Ua.ReferenceTypeIds.HierarchicalReferences, + true, + (uint)Opc.Ua.NodeClass.Object | (uint)Opc.Ua.NodeClass.Variable, + out continuationPoint, + out references + ); + + foreach (var rd in references) + { + NodeType nodeType = NodeType.Folder; // 默认是文件夹 + if ((rd.NodeClass & NodeClass.Variable) != 0) + { + nodeType = NodeType.Variable; + } + else if ((rd.NodeClass & NodeClass.Object) != 0) + { + nodeType = NodeType.Object; + } + + var opcUaNode = new OpcUaNode(rd.DisplayName.Text, (NodeId)rd.NodeId, nodeType); + nodes.Add(opcUaNode); + + // 如果是文件夹或对象,添加一个虚拟子节点,用于懒加载 + if (nodeType == NodeType.Folder || nodeType == NodeType.Object) + { + opcUaNode.Children.Add(new OpcUaNode("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点 + } + } + } + catch (Exception ex) + { + NlogHelper.Error($"浏览 OPC UA 节点失败: {parentNodeId} - {ex.Message}", ex); + NotificationHelper.ShowError($"浏览 OPC UA 节点失败: {parentNodeId} - {ex.Message}", ex); + } + } + + public async Task LoadNodeVariables(OpcUaNode node) + { + if (node.NodeType == NodeType.Variable) + { + // 如果是变量节点,直接显示它 + SelectedNodeVariables.Clear(); + SelectedNodeVariables.Add(new VariableData + { + Name = node.DisplayName, + NodeId = node.NodeId.ToString(), + OpcUaNodeId = node.NodeId.ToString(), + ProtocolType = ProtocolType.OpcUA, + IsActive = true // 默认选中 + }); + return; + } + + if (node.IsLoaded || node.IsLoading) + { + return; // 已经加载或正在加载 + } + + node.IsLoading = true; + node.Children.Clear(); // 清除虚拟节点 + + try + { + Opc.Ua.ReferenceDescriptionCollection references; + byte[] continuationPoint = null; + + _session.Browse( + null, // RequestHeader + new ViewDescription(), + node.NodeId, + 0u, + BrowseDirection.Forward, + Opc.Ua.ReferenceTypeIds.HierarchicalReferences, + true, + (uint)Opc.Ua.NodeClass.Object | (uint)Opc.Ua.NodeClass.Variable, + out continuationPoint, + out references + ); + + foreach (var rd in references) + { + NodeType nodeType = NodeType.Folder; + if ((rd.NodeClass & NodeClass.Variable) != 0) + { + nodeType = NodeType.Variable; + } + else if ((rd.NodeClass & NodeClass.Object) != 0) + { + nodeType = NodeType.Object; + } + + var opcUaNode = new OpcUaNode(rd.DisplayName.Text, (NodeId)rd.NodeId, nodeType); + node.Children.Add(opcUaNode); + + if (nodeType == NodeType.Folder || nodeType == NodeType.Object) + { + opcUaNode.Children.Add(new OpcUaNode("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点 + } + + // 如果是变量,添加到右侧列表 + if (nodeType == NodeType.Variable) + { + SelectedNodeVariables.Add(new VariableData + { + Name = opcUaNode.DisplayName, + NodeId = opcUaNode.NodeId.ToString(), + OpcUaNodeId = opcUaNode.NodeId.ToString(), + ProtocolType = ProtocolType.OpcUA, + IsActive = true // 默认选中 + }); + } + } + + node.IsLoaded = true; + } + catch (Exception ex) + { + NlogHelper.Error($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex); + NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex); + } + finally + { + node.IsLoading = false; + } + } + + public ObservableCollection GetSelectedVariables() + { + return new ObservableCollection(SelectedNodeVariables.Where(v => v.IsActive)); + } +} diff --git a/ViewModels/VariableTableViewModel.cs b/ViewModels/VariableTableViewModel.cs index 75c0b55..576ce12 100644 --- a/ViewModels/VariableTableViewModel.cs +++ b/ViewModels/VariableTableViewModel.cs @@ -44,6 +44,9 @@ partial class VariableTableViewModel : ViewModelBase [ObservableProperty] private bool _isS7ProtocolSelected; + [ObservableProperty] + private bool _isOpcUaProtocolSelected; + public VariableTableViewModel(IDialogService dialogService) { _dialogService = dialogService; @@ -85,6 +88,7 @@ partial class VariableTableViewModel : ViewModelBase public override void OnLoaded() { IsS7ProtocolSelected = VariableTable.ProtocolType == ProtocolType.S7; + IsOpcUaProtocolSelected = VariableTable.ProtocolType == ProtocolType.OpcUA; if (VariableTable.DataVariables != null) { @@ -223,6 +227,44 @@ partial class VariableTableViewModel : ViewModelBase } + [RelayCommand] + private async Task ImportFromOpcUaServer() + { + try + { + var importedVariables = await _dialogService.ShowOpcUaImportDialog(); + if (importedVariables == null || !importedVariables.Any()) + { + return; // 用户取消或没有选择任何变量 + } + + ContentDialog processingDialog = _dialogService.ShowProcessingDialog("正在处理...", "正在导入OPC UA变量,请稍等片刻...."); + + foreach (var variableData in importedVariables) + { + variableData.CreateTime = DateTime.Now; + variableData.VariableTableId = VariableTable.Id; + variableData.ProtocolType = ProtocolType.OpcUA; // 确保协议类型正确 + } + + // 插入数据库 + var resVarDataCount = await _varDataRepository.AddAsync(importedVariables); + + // 更新界面 + variableTable.DataVariables = await _varDataRepository.GetAllAsync(); + _dataVariables = new ObservableCollection(variableTable.DataVariables); + processingDialog?.Hide(); + + string msgSuccess = $"成功导入OPC UA变量:{resVarDataCount}个。"; + NlogHelper.Info(msgSuccess); + NotificationHelper.ShowSuccess(msgSuccess); + } + catch (Exception e) + { + NotificationHelper.ShowError($"从OPC UA服务器导入变量的过程中发生了不可预期的错误:{e.Message}", e); + } + } + [RelayCommand] private async void AddVarData(VariableTable variableTable) diff --git a/Views/Dialogs/OpcUaImportDialog.xaml b/Views/Dialogs/OpcUaImportDialog.xaml new file mode 100644 index 0000000..6269f87 --- /dev/null +++ b/Views/Dialogs/OpcUaImportDialog.xaml @@ -0,0 +1,64 @@ + + + + + + + + + + + + + + + +