From b1e79a63df9e8992b7241b98d8efd96cfbc4e759 Mon Sep 17 00:00:00 2001 From: "David P.G" Date: Wed, 9 Jul 2025 21:35:51 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=89=93=E5=BC=80=E5=AF=BC?= =?UTF-8?q?=E5=85=A5OPC=E5=8F=98=E9=87=8F=E5=AF=B9=E8=AF=9D=E6=A1=86?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=BF=9E=E6=8E=A5=E6=9C=8D=E5=8A=A1=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Services/DialogService.cs | 3 +- Services/IDialogService.cs | 2 +- Services/OpcUaBackgroundService.cs | 99 ++++++++++++------- .../Dialogs/OpcUaImportDialogViewModel.cs | 2 + ViewModels/VariableTableViewModel.cs | 8 +- Views/Dialogs/OpcUaImportDialog.xaml | 23 +---- 6 files changed, 81 insertions(+), 56 deletions(-) diff --git a/Services/DialogService.cs b/Services/DialogService.cs index 186b1a3..181524b 100644 --- a/Services/DialogService.cs +++ b/Services/DialogService.cs @@ -180,9 +180,10 @@ public class DialogService :IDialogService return result == ContentDialogResult.Primary ? vm.SelectedMqtt : null; } - public async Task> ShowOpcUaImportDialog() + public async Task> ShowOpcUaImportDialog(string endpointUrl) { var vm= new OpcUaImportDialogViewModel(); + vm.EndpointUrl = endpointUrl; var dialog = new OpcUaImportDialog(vm); var result = await dialog.ShowAsync(); return result == ContentDialogResult.Primary ? vm.GetSelectedVariables().ToList() : null; diff --git a/Services/IDialogService.cs b/Services/IDialogService.cs index ee4a203..5210744 100644 --- a/Services/IDialogService.cs +++ b/Services/IDialogService.cs @@ -22,5 +22,5 @@ public interface IDialogService ContentDialog ShowProcessingDialog(string title, string message); Task ShowPollLevelDialog(PollLevelType pollLevelType); Task ShowMqttSelectionDialog(); - Task> ShowOpcUaImportDialog(); + Task> ShowOpcUaImportDialog(string endpointUrl); } \ No newline at end of file diff --git a/Services/OpcUaBackgroundService.cs b/Services/OpcUaBackgroundService.cs index f855362..80be192 100644 --- a/Services/OpcUaBackgroundService.cs +++ b/Services/OpcUaBackgroundService.cs @@ -216,43 +216,76 @@ namespace PMSWPF.Services { try { - // 1. 创建应用程序实例。 - ApplicationInstance application = new ApplicationInstance - { - ApplicationName = "PMSWPF OPC UA Client", - ApplicationType = ApplicationType.Client, - ConfigSectionName = "PMSWPF.OpcUaClient" - }; + // 1. 创建应用程序配置 + var application = new ApplicationInstance + { + ApplicationName = "OpcUADemoClient", + ApplicationType = ApplicationType.Client, + ConfigSectionName = "Opc.Ua.Client" + }; - // 2. 加载应用程序配置。 - 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 + } + }; + application.ApplicationConfiguration = config; - // 3. 检查应用程序实例证书。 - bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 0); - if (!haveAppCertificate) - { - throw new Exception("Application instance certificate invalid!"); - } + // 验证并检查证书 + await config.Validate(ApplicationType.Client); + await application.CheckApplicationInstanceCertificate(false, 0); - // 4. 发现服务器提供的终结点。 - 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, device.OpcUaEndpointUrl, false); - EndpointConfiguration endpointConfiguration = EndpointConfiguration.Create(application.ApplicationConfiguration); - ConfiguredEndpoint configuredEndpoint = new ConfiguredEndpoint(null, selectedEndpoint, endpointConfiguration); + // 2. 查找并选择端点 (将 useSecurity 设置为 false 以进行诊断) + var selectedEndpoint = CoreClientUtils.SelectEndpoint(device.OpcUaEndpointUrl, false); - // 5. 创建会话。 - session = await Session.Create( - config, - configuredEndpoint, - false, // 不更新证书 - "PMSWPF OPC UA Session", // 会话名称 - 60000, // 会话超时时间(毫秒) - new UserIdentity(new AnonymousIdentityToken()), // 使用匿名用户身份 - null); + session = await Session.Create( + config, + new ConfiguredEndpoint(null, selectedEndpoint, EndpointConfiguration.Create(config)), + false, + "PMSWPF OPC UA Session", + 60000, + new UserIdentity(new AnonymousIdentityToken()), + null); _opcUaSessions[device.OpcUaEndpointUrl] = session; NlogHelper.Info($"Connected to OPC UA server: {device.OpcUaEndpointUrl}"); diff --git a/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs b/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs index f98c70b..22020a7 100644 --- a/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs +++ b/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs @@ -37,6 +37,8 @@ public partial class OpcUaImportDialogViewModel : ObservableObject { OpcUaNodes = new ObservableCollection(); SelectedNodeVariables = new ObservableCollection(); + // Automatically connect when the ViewModel is created + _ = Connect().ConfigureAwait(false); } [RelayCommand] diff --git a/ViewModels/VariableTableViewModel.cs b/ViewModels/VariableTableViewModel.cs index 576ce12..d219bab 100644 --- a/ViewModels/VariableTableViewModel.cs +++ b/ViewModels/VariableTableViewModel.cs @@ -232,7 +232,13 @@ partial class VariableTableViewModel : ViewModelBase { try { - var importedVariables = await _dialogService.ShowOpcUaImportDialog(); + string opcUaEndpointUrl = VariableTable?.Device?.OpcUaEndpointUrl; + if (string.IsNullOrEmpty(opcUaEndpointUrl)) + { + NotificationHelper.ShowError("OPC UA Endpoint URL 未设置。请在设备详情中配置。"); + return; + } + var importedVariables = await _dialogService.ShowOpcUaImportDialog(opcUaEndpointUrl); if (importedVariables == null || !importedVariables.Any()) { return; // 用户取消或没有选择任何变量 diff --git a/Views/Dialogs/OpcUaImportDialog.xaml b/Views/Dialogs/OpcUaImportDialog.xaml index ece1ce8..d71d2a9 100644 --- a/Views/Dialogs/OpcUaImportDialog.xaml +++ b/Views/Dialogs/OpcUaImportDialog.xaml @@ -12,11 +12,9 @@ SecondaryButtonText="取消" PrimaryButtonClick="ContentDialog_PrimaryButtonClick" SecondaryButtonClick="ContentDialog_SecondaryButtonClick" - Width="800" - Height="600"> + > - @@ -24,23 +22,8 @@ - - - - -