From 6f16a1c4e4a0b16f948251c833c63547b020d827 Mon Sep 17 00:00:00 2001 From: "David P.G" Date: Sun, 13 Jul 2025 16:22:07 +0800 Subject: [PATCH] =?UTF-8?q?1=EF=BC=8C=E9=87=8D=E6=96=B0=E6=A2=B3=E7=90=86?= =?UTF-8?q?=E4=BA=86=E4=BB=A3=E7=A0=81=EF=BC=8C=E5=B0=86=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=A4=9A=E7=BA=BF=E7=A8=8B=EF=BC=8C=E5=B9=B6=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=E4=BA=86=E6=89=B9=E9=87=8F=E8=AF=BB=E5=8F=96=E5=8F=98=E9=87=8F?= =?UTF-8?q?=202=EF=BC=8C=E6=B7=BB=E5=8A=A0OpcUa=E6=9B=B4=E6=96=B0=E6=96=B9?= =?UTF-8?q?=E5=BC=8F=E7=9A=84=E4=BF=AE=E6=94=B9=E5=AF=B9=E8=AF=9D=E6=A1=86?= =?UTF-8?q?=203=EF=BC=8C=E4=BF=AE=E5=A4=8D=E4=BA=86=E4=B8=80=E4=BA=9B?= =?UTF-8?q?=E5=B7=B2=E7=9F=A5=E7=9A=84Bug=204=EF=BC=8C=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E4=BA=86=E4=B8=8D=E5=BF=85=E8=A6=81=E7=9A=84=E5=87=BD=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- App.xaml.cs | 2 +- Config/nlog.config | 2 +- Data/Repositories/VarDataRepository.cs | 12 +- Helper/OpcUaServiceHelper.cs | 103 ---- Helper/ServiceHelper.cs | 184 +++++++ Helper/ThemeHelper.cs | 96 ++++ Services/DataServices.cs | 49 +- Services/DialogService.cs | 12 + Services/IDialogService.cs | 1 + Services/MqttBackgroundService.cs | 24 +- Services/OpcUaBackgroundService.cs | 418 +++++++-------- Services/S7BackgroundService.cs | 477 +++++++++--------- ViewModels/DevicesViewModel.cs | 11 +- .../Dialogs/OpcUaImportDialogViewModel.cs | 2 +- .../Dialogs/OpcUaUpdateTypeDialogViewModel.cs | 18 + ViewModels/MainViewModel.cs | 12 +- ViewModels/MqttsViewModel.cs | 2 +- ViewModels/VariableTableViewModel.cs | 60 ++- Views/Dialogs/OpcUaUpdateTypeDialog.xaml | 23 + Views/Dialogs/OpcUaUpdateTypeDialog.xaml.cs | 14 + Views/VariableTableView.xaml | 17 +- Views/VariableTableView.xaml.cs | 42 -- 22 files changed, 877 insertions(+), 704 deletions(-) delete mode 100644 Helper/OpcUaServiceHelper.cs create mode 100644 Helper/ServiceHelper.cs create mode 100644 Helper/ThemeHelper.cs create mode 100644 ViewModels/Dialogs/OpcUaUpdateTypeDialogViewModel.cs create mode 100644 Views/Dialogs/OpcUaUpdateTypeDialog.xaml create mode 100644 Views/Dialogs/OpcUaUpdateTypeDialog.xaml.cs diff --git a/App.xaml.cs b/App.xaml.cs index 45b405c..7ee3d63 100644 --- a/App.xaml.cs +++ b/App.xaml.cs @@ -89,7 +89,7 @@ public partial class App : Application // 停止服务 Host.Services.GetRequiredService().StopService(); Host.Services.GetRequiredService().StopService(); - await Host.Services.GetRequiredService().StopService(); + Host.Services.GetRequiredService().StopService(); await Host.StopAsync(); Host.Dispose(); diff --git a/Config/nlog.config b/Config/nlog.config index f96939d..2360386 100644 --- a/Config/nlog.config +++ b/Config/nlog.config @@ -19,7 +19,7 @@ + layout="${date:format=HH\:mm\:ss} ${level} ${threadid} ${message}${exception:format=tostring}"/> diff --git a/Data/Repositories/VarDataRepository.cs b/Data/Repositories/VarDataRepository.cs index 623f214..8ec83d1 100644 --- a/Data/Repositories/VarDataRepository.cs +++ b/Data/Repositories/VarDataRepository.cs @@ -181,16 +181,8 @@ public class VarDataRepository /// public async Task UpdateAsync(List variableDatas) { - Stopwatch stopwatch = new Stopwatch(); - stopwatch.Start(); - using (var _db = DbContext.GetInstance()) - { - var result = await UpdateAsync(variableDatas, _db); - - stopwatch.Stop(); - NlogHelper.Info($"更新VariableData {variableDatas.Count()}个 耗时:{stopwatch.ElapsedMilliseconds}ms"); - return result; - } + using var _db = DbContext.GetInstance(); + return await UpdateAsync(variableDatas, _db); } /// diff --git a/Helper/OpcUaServiceHelper.cs b/Helper/OpcUaServiceHelper.cs deleted file mode 100644 index fe65e4b..0000000 --- a/Helper/OpcUaServiceHelper.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Opc.Ua; -using Opc.Ua.Client; -using Opc.Ua.Configuration; - -namespace PMSWPF.Helper; - -public static class OpcUaServiceHelper -{ - - /// - /// 创建并配置 OPC UA 会话。 - /// - /// OPC UA 服务器的终结点 URL。 - /// 创建的 Session 对象,如果失败则返回 null。 - public static async Task CreateOpcUaSessionAsync(string endpointUrl) - { - try - { - // 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); - - var 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}"); - return session; - } - catch (Exception ex) - { - NotificationHelper.ShowError($"连接 OPC UA 服务器失败: {endpointUrl} - {ex.Message}", ex); - return null; - } - } - - - -} \ No newline at end of file diff --git a/Helper/ServiceHelper.cs b/Helper/ServiceHelper.cs new file mode 100644 index 0000000..585aa9b --- /dev/null +++ b/Helper/ServiceHelper.cs @@ -0,0 +1,184 @@ +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; +using PMSWPF.Enums; + +namespace PMSWPF.Helper; + +public static class ServiceHelper +{ + // 定义不同轮询级别的间隔时间。 + public static Dictionary PollingIntervals = new Dictionary + { + { + PollLevelType.TenMilliseconds, + TimeSpan.FromMilliseconds( + (int)PollLevelType.TenMilliseconds) + }, + { + PollLevelType.HundredMilliseconds, + TimeSpan.FromMilliseconds( + (int)PollLevelType + .HundredMilliseconds) + }, + { + PollLevelType.FiveHundredMilliseconds, + TimeSpan.FromMilliseconds( + (int)PollLevelType + .FiveHundredMilliseconds) + }, + { + PollLevelType.OneSecond, + TimeSpan.FromMilliseconds( + (int)PollLevelType.OneSecond) + }, + { + PollLevelType.FiveSeconds, + TimeSpan.FromMilliseconds( + (int)PollLevelType.FiveSeconds) + }, + { + PollLevelType.TenSeconds, + TimeSpan.FromMilliseconds( + (int)PollLevelType.TenSeconds) + }, + { + PollLevelType.TwentySeconds, + TimeSpan.FromMilliseconds( + (int)PollLevelType.TwentySeconds) + }, + { + PollLevelType.ThirtySeconds, + TimeSpan.FromMilliseconds( + (int)PollLevelType.ThirtySeconds) + }, + { + PollLevelType.OneMinute, + TimeSpan.FromMilliseconds( + (int)PollLevelType.OneMinute) + }, + { + PollLevelType.ThreeMinutes, + TimeSpan.FromMilliseconds( + (int)PollLevelType.ThreeMinutes) + }, + { + PollLevelType.FiveMinutes, + TimeSpan.FromMilliseconds( + (int)PollLevelType.FiveMinutes) + }, + { + PollLevelType.TenMinutes, + TimeSpan.FromMilliseconds( + (int)PollLevelType.TenMinutes) + }, + { + PollLevelType.ThirtyMinutes, + TimeSpan.FromMilliseconds( + (int)PollLevelType.ThirtyMinutes) + } + }; + + /// + /// 创建并配置 OPC UA 会话。 + /// + /// OPC UA 服务器的终结点 URL。 + /// 创建的 Session 对象,如果失败则返回 null。 + public static Session CreateOpcUaSession(string endpointUrl) + { + return CreateOpcUaSessionAsync(endpointUrl) + .GetAwaiter() + .GetResult(); + } + + /// + /// 创建并配置 OPC UA 会话。 + /// + /// OPC UA 服务器的终结点 URL。 + /// 创建的 Session 对象,如果失败则返回 null。 + public static async Task CreateOpcUaSessionAsync(string endpointUrl) + { + try + { + // 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); + + var 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}"); + return session; + } + catch (Exception ex) + { + NotificationHelper.ShowError($"连接 OPC UA 服务器失败: {endpointUrl} - {ex.Message}", ex); + return null; + } + } +} \ No newline at end of file diff --git a/Helper/ThemeHelper.cs b/Helper/ThemeHelper.cs new file mode 100644 index 0000000..70c5047 --- /dev/null +++ b/Helper/ThemeHelper.cs @@ -0,0 +1,96 @@ +using System; +using System.Linq; +using System.Windows; +using iNKORE.UI.WPF.Modern; +using Microsoft.Win32; +using PMSWPF.Config; + +namespace PMSWPF.Helper; + +public static class ThemeHelper +{ + public static void ApplyTheme(string themeName) + { + ApplicationTheme theme; + + if (themeName == "跟随系统") + { + theme = IsSystemInDarkTheme() ? ApplicationTheme.Dark : ApplicationTheme.Light; + } + else + { + theme = themeName switch + { + "浅色" => ApplicationTheme.Light, + "深色" => ApplicationTheme.Dark, + _ => IsSystemInDarkTheme() ? ApplicationTheme.Dark : ApplicationTheme.Light + }; + } + + // Apply theme for iNKORE controls + ThemeManager.Current.ApplicationTheme = theme; + + // Apply theme for HandyControl + UpdateHandyControlTheme(theme); + } + + public static void InitializeTheme() + { + var settings = ConnectionSettings.Load(); + ApplyTheme(settings.Theme); + + // Listen for system theme changes + SystemEvents.UserPreferenceChanged += (s, e) => + { + if (e.Category == UserPreferenceCategory.General && ConnectionSettings.Load().Theme == "跟随系统") + { + Application.Current.Dispatcher.Invoke(() => { ApplyTheme("跟随系统"); }); + } + }; + } + + private static void UpdateHandyControlTheme(ApplicationTheme theme) + { + var dictionaries = Application.Current.Resources.MergedDictionaries; + + // Find and remove the existing HandyControl skin dictionary + var existingSkin = dictionaries.FirstOrDefault(d => + d.Source != null && d.Source.OriginalString.Contains("HandyControl;component/Themes/Skin")); + if (existingSkin != null) + { + dictionaries.Remove(existingSkin); + } + + // Determine the new skin URI + string skinUri = theme == ApplicationTheme.Dark + ? "pack://application:,,,/HandyControl;component/Themes/SkinDark.xaml" + : "pack://application:,,,/HandyControl;component/Themes/SkinDefault.xaml"; + + // Add the new skin dictionary + dictionaries.Add(new ResourceDictionary { Source = new Uri(skinUri, UriKind.Absolute) }); + + // To force refresh of dynamic resources, remove and re-add the main theme dictionary + var existingTheme = dictionaries.FirstOrDefault(d => d.Source != null && d.Source.OriginalString.Contains("HandyControl;component/Themes/Theme.xaml")); + if (existingTheme != null) + { + dictionaries.Remove(existingTheme); + dictionaries.Add(existingTheme); + } + } + + private static bool IsSystemInDarkTheme() + { + try + { + const string keyName = @"HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"; + const string valueName = "AppsUseLightTheme"; + var value = Registry.GetValue(keyName, valueName, 1); + return value is 0; + } + catch + { + // Default to light theme if registry access fails + return false; + } + } +} \ No newline at end of file diff --git a/Services/DataServices.cs b/Services/DataServices.cs index 293279c..d32f8f3 100644 --- a/Services/DataServices.cs +++ b/Services/DataServices.cs @@ -51,48 +51,14 @@ public partial class DataServices : ObservableRecipient, IRecipient private readonly VarDataRepository _varDataRepository; // 设备列表变更事件,当设备列表数据更新时触发。 - public event EventHandler> OnDeviceListChanged; + public event Action> OnDeviceListChanged; // 菜单树列表变更事件,当菜单树数据更新时触发。 - public event EventHandler> OnMenuTreeListChanged; + public event Action> OnMenuTreeListChanged; // MQTT列表变更事件,当MQTT配置数据更新时触发。 - public event EventHandler> OnMqttListChanged; + public event Action> OnMqttListChanged; - // 变量数据变更事件,当变量数据更新时触发。 - public event Action> OnVariableDataChanged; - - /// - /// 当_devices属性值改变时触发的局部方法,用于调用OnDeviceListChanged事件。 - /// - /// 新的设备列表。 - partial void OnDevicesChanged(List devices) - { - OnDeviceListChanged?.Invoke(this, devices); - - - VariableDatas.Clear(); - foreach (Device device in devices) - { - foreach (VariableTable variableTable in device.VariableTables) - { - foreach (VariableData variableData in variableTable.DataVariables) - { - VariableDatas.Add(variableData); - } - } - } - OnVariableDataChanged?.Invoke(VariableDatas); - } - - /// - /// 当menuTrees属性值改变时触发的局部方法,用于调用OnMenuTreeListChanged事件。 - /// - /// 新的菜单树列表。 - partial void OnMenuTreesChanged(List MenuTrees) - { - OnMenuTreeListChanged?.Invoke(this, MenuTrees); - } /// /// 当_mqtts属性值改变时触发的局部方法,用于调用OnMqttListChanged事件。 @@ -100,7 +66,7 @@ public partial class DataServices : ObservableRecipient, IRecipient /// 新的MQTT配置列表。 partial void OnMqttsChanged(List mqtts) { - OnMqttListChanged?.Invoke(this, mqtts); + OnMqttListChanged?.Invoke(mqtts); } // 注释掉的代码块,可能用于变量数据变更事件的触发,但目前未启用。 @@ -164,6 +130,7 @@ public partial class DataServices : ObservableRecipient, IRecipient private async Task LoadDevices() { Devices = await _deviceRepository.GetAll(); + OnDeviceListChanged?.Invoke(Devices); } /// @@ -178,6 +145,8 @@ public partial class DataServices : ObservableRecipient, IRecipient MenuHelper.MenuAddParent(menu); // 为菜单添加父级引用 DataServicesHelper.SortMenus(menu); // 排序菜单 } + + OnMenuTreeListChanged?.Invoke(MenuTrees); } /// @@ -186,7 +155,9 @@ public partial class DataServices : ObservableRecipient, IRecipient /// 包含所有MQTT配置的列表。 public async Task> GetMqttsAsync() { - return await _mqttRepository.GetAll(); + var mqtts = await _mqttRepository.GetAll(); + OnMqttListChanged?.Invoke(mqtts); + return mqtts; } /// diff --git a/Services/DialogService.cs b/Services/DialogService.cs index 181524b..5700bc8 100644 --- a/Services/DialogService.cs +++ b/Services/DialogService.cs @@ -188,4 +188,16 @@ public class DialogService :IDialogService var result = await dialog.ShowAsync(); return result == ContentDialogResult.Primary ? vm.GetSelectedVariables().ToList() : null; } + + public async Task ShowOpcUaUpdateTypeDialog() + { + var vm = new OpcUaUpdateTypeDialogViewModel(); + var dialog = new OpcUaUpdateTypeDialog(vm); + var result = await dialog.ShowAsync(); + if (result == ContentDialogResult.Primary) + { + return vm.SelectedUpdateType; + } + return null; + } } \ No newline at end of file diff --git a/Services/IDialogService.cs b/Services/IDialogService.cs index 5210744..b9f7120 100644 --- a/Services/IDialogService.cs +++ b/Services/IDialogService.cs @@ -23,4 +23,5 @@ public interface IDialogService Task ShowPollLevelDialog(PollLevelType pollLevelType); Task ShowMqttSelectionDialog(); Task> ShowOpcUaImportDialog(string endpointUrl); + Task ShowOpcUaUpdateTypeDialog(); } \ No newline at end of file diff --git a/Services/MqttBackgroundService.cs b/Services/MqttBackgroundService.cs index 19a58d3..87495ca 100644 --- a/Services/MqttBackgroundService.cs +++ b/Services/MqttBackgroundService.cs @@ -55,7 +55,7 @@ namespace PMSWPF.Services // 订阅MQTT列表和变量数据变化的事件,以便在数据更新时重新加载配置和数据。 _dataServices.OnMqttListChanged += HandleMqttListChanged; - _dataServices.OnVariableDataChanged += HandleVariableDataChanged; + _dataServices.OnDeviceListChanged += HandleDeviceListChanged; // 初始加载MQTT配置和变量数据。 await LoadMqttConfigurations(); @@ -68,6 +68,13 @@ namespace PMSWPF.Services // await Task.Delay(Timeout.Infinite, stoppingToken); } + private async void HandleDeviceListChanged( List devices) + { + NlogHelper.Info("Variable data changed. Reloading variable associations."); // 记录变量数据变化信息 + // 重新加载变量数据。 + await LoadVariableData(); + } + /// /// 停止MQTT后台服务。 /// @@ -80,7 +87,7 @@ namespace PMSWPF.Services // 取消订阅事件。 _dataServices.OnMqttListChanged -= HandleMqttListChanged; - _dataServices.OnVariableDataChanged -= HandleVariableDataChanged; + _dataServices.OnDeviceListChanged -= HandleDeviceListChanged; // 断开所有已连接的MQTT客户端。 foreach (var client in _mqttClients.Values) @@ -286,7 +293,7 @@ namespace PMSWPF.Services /// /// 事件发送者。 /// 更新后的MQTT配置列表。 - private async void HandleMqttListChanged(object sender, List mqtts) + private async void HandleMqttListChanged( List mqtts) { NlogHelper.Info("MQTT list changed. Reloading configurations."); // 记录MQTT列表变化信息 // 重新加载MQTT配置和变量数据。 @@ -294,16 +301,5 @@ namespace PMSWPF.Services await LoadVariableData(); // 重新加载变量数据,以防关联发生变化 } - /// - /// 处理变量数据变化事件的回调方法。 - /// - /// 事件发送者。 - /// 更新后的变量数据列表。 - private async void HandleVariableDataChanged( List variableDatas) - { - NlogHelper.Info("Variable data changed. Reloading variable associations."); // 记录变量数据变化信息 - // 重新加载变量数据。 - await LoadVariableData(); - } } } \ No newline at end of file diff --git a/Services/OpcUaBackgroundService.cs b/Services/OpcUaBackgroundService.cs index a5bac28..005fc55 100644 --- a/Services/OpcUaBackgroundService.cs +++ b/Services/OpcUaBackgroundService.cs @@ -14,97 +14,42 @@ using PMSWPF.Models; namespace PMSWPF.Services { - /// - /// OPC UA 后台服务,负责处理所有与 OPC UA 相关的操作,包括连接服务器、订阅变量和接收数据更新。 - /// - /// - /// ## 调用逻辑 - /// 1. **实例化与启动**: - /// - 在 `App.xaml.cs` 的 `ConfigureServices` 方法中,通过 `services.AddSingleton()` 将该服务注册为单例。 - /// - 在 `OnStartup` 方法中,通过 `Host.Services.GetRequiredService().StartAsync()` 启动服务。 - /// - /// 2. **核心执行流程 (`ExecuteAsync`)**: - /// - 服务启动后,`ExecuteAsync` 方法被调用。 - /// - 订阅 `DataServices.OnVariableDataChanged` 事件,以便在变量配置发生变化时动态更新 OPC UA 的连接和订阅。 - /// - 调用 `LoadOpcUaVariables()` 方法,初始化加载所有协议类型为 `OpcUA` 且状态为激活的变量。 - /// - 进入主循环,等待应用程序关闭信号。 - /// - /// 3. **变量加载与管理 (`LoadOpcUaVariables`)**: - /// - 从 `DataServices` 获取所有变量数据。 - /// - 清理本地缓存中已不存在或不再活跃的变量,并断开相应的 OPC UA 连接。 - /// - 遍历所有活动的 OPC UA 变量,为新增的变量调用 `ConnectOpcUaService` 方法建立连接和订阅。 - /// - 如果变量已存在但会话断开,则尝试重新连接。 - /// - /// 4. **连接与订阅 (`ConnectOpcUaService`)**: - /// - 检查变量是否包含有效的终结点 URL 和节点 ID。 - /// - 如果到目标终结点的会话已存在且已连接,则复用该会话。 - /// - 否则,创建一个新的 OPC UA `Session`,并将其缓存到 `_opcUaSessions` 字典中。 - /// - 创建一个 `Subscription`,并将其缓存到 `_opcUaSubscriptions` 字典中。 - /// - 创建一个 `MonitoredItem` 来监控指定变量节点的值变化,并将其添加到订阅中。 - /// - 注册 `OnSubNotification` 事件回调,用于处理接收到的数据更新。 - /// - /// 5. **数据接收 (`OnSubNotification`)**: - /// - 当订阅的变量值发生变化时,OPC UA 服务器会发送通知。 - /// - `OnSubNotification` 方法被触发,处理接收到的数据。 - /// - /// 6. **动态更新 (`HandleVariableDataChanged`)**: - /// - 当 `DataServices` 中的变量数据发生增、删、改时,会触发 `OnVariableDataChanged` 事件。 - /// - `HandleVariableDataChanged` 方法被调用,重新执行 `LoadOpcUaVariables()` 以应用最新的变量配置。 - /// - /// 7. **服务停止 (`ExecuteAsync` 退出循环)**: - /// - 当应用程序关闭时,`stoppingToken` 被触发。 - /// - 取消对 `OnVariableDataChanged` 事件的订阅。 - /// - 调用 `DisconnectAllOpcUaSessions()` 方法,关闭所有活动的 OPC UA 会话和订阅。 - /// public class OpcUaBackgroundService { - private CancellationTokenSource _cancellationTokenSource; - private Task _executingTask; - private readonly DataServices _dataServices; + // 存储 OPC UA 设备,键为设备Id,值为会话对象。 + private readonly Dictionary _deviceDic; + // 存储 OPC UA 会话,键为终结点 URL,值为会话对象。 - private readonly Dictionary _opcUaSessions; + private readonly Dictionary _sessionsDic; // 存储 OPC UA 订阅,键为终结点 URL,值为订阅对象。 - private readonly Dictionary _opcUaSubscriptions; + private readonly Dictionary _subscriptionsDic; // 存储活动的 OPC UA 变量,键为变量的OpcNodeId private readonly Dictionary _opcUaNodeIdVariableDic; // Key: VariableData.Id // 储存所有要轮询更新的变量,键是Device.Id,值是这个设备所有要轮询的变量 - private readonly Dictionary> _opcUaPollVariableDic; // Key: VariableData.Id + private readonly Dictionary> _pollVariableDic; // Key: VariableData.Id // 储存所有要订阅更新的变量,键是Device.Id,值是这个设备所有要轮询的变量 - private readonly Dictionary> _opcUaSubVariableDic; + private readonly Dictionary> _subVariableDic; - // private readonly Dictionary - // _opcUaPollingTasks; // Key: VariableData.Id, Value: CancellationTokenSource for polling task - private List _opcUaDevices; + + // 后台服务的主线程,负责连接服务器,加载变量,订阅变量 + private Thread _serviceMainThread; + + // 轮询线程 + private Thread _pollThread; + + // 重新加载事件 + private readonly ManualResetEvent _reloadEvent = new ManualResetEvent(false); + + // 停止事件,触发后会停止整个Opc后台服务 + private readonly ManualResetEvent _stopdEvent = new ManualResetEvent(false); - // 定义不同轮询级别的间隔时间。 - private readonly Dictionary _pollingIntervals = new Dictionary - { - { PollLevelType.TenMilliseconds, TimeSpan.FromMilliseconds((int)PollLevelType.TenMilliseconds) }, - { - PollLevelType.HundredMilliseconds, TimeSpan.FromMilliseconds((int)PollLevelType.HundredMilliseconds) - }, - { - PollLevelType.FiveHundredMilliseconds, - TimeSpan.FromMilliseconds((int)PollLevelType.FiveHundredMilliseconds) - }, - { PollLevelType.OneSecond, TimeSpan.FromMilliseconds((int)PollLevelType.OneSecond) }, - { PollLevelType.FiveSeconds, TimeSpan.FromMilliseconds((int)PollLevelType.FiveSeconds) }, - { PollLevelType.TenSeconds, TimeSpan.FromMilliseconds((int)PollLevelType.TenSeconds) }, - { PollLevelType.TwentySeconds, TimeSpan.FromMilliseconds((int)PollLevelType.TwentySeconds) }, - { PollLevelType.ThirtySeconds, TimeSpan.FromMilliseconds((int)PollLevelType.ThirtySeconds) }, - { PollLevelType.OneMinute, TimeSpan.FromMilliseconds((int)PollLevelType.OneMinute) }, - { PollLevelType.ThreeMinutes, TimeSpan.FromMilliseconds((int)PollLevelType.ThreeMinutes) }, - { PollLevelType.FiveMinutes, TimeSpan.FromMilliseconds((int)PollLevelType.FiveMinutes) }, - { PollLevelType.TenMinutes, TimeSpan.FromMilliseconds((int)PollLevelType.TenMinutes) }, - { PollLevelType.ThirtyMinutes, TimeSpan.FromMilliseconds((int)PollLevelType.ThirtyMinutes) } - }; /// /// OpcUaBackgroundService 的构造函数。 @@ -113,11 +58,12 @@ namespace PMSWPF.Services public OpcUaBackgroundService(DataServices dataServices) { _dataServices = dataServices; - _opcUaSessions = new Dictionary(); - _opcUaSubscriptions = new Dictionary(); + _sessionsDic = new Dictionary(); + _subscriptionsDic = new Dictionary(); _opcUaNodeIdVariableDic = new(); - _opcUaPollVariableDic = new(); - _opcUaSubVariableDic = new(); + _pollVariableDic = new(); + _subVariableDic = new(); + _deviceDic = new(); } /// @@ -128,37 +74,60 @@ namespace PMSWPF.Services public void StartService() { NlogHelper.Info("OPC UA 服务正在启动..."); - _cancellationTokenSource = new CancellationTokenSource(); - _executingTask = ExecuteAsync(_cancellationTokenSource.Token); + _reloadEvent.Set(); + _serviceMainThread = new Thread(Execute); + _serviceMainThread.IsBackground = true; + _serviceMainThread.Name = "OpcUaServiceThread"; + _serviceMainThread.Start(); } - public async Task StopService() + public void StopService() { NlogHelper.Info("OPC UA 服务正在停止..."); - if (_executingTask == null) - { - return; - } + _stopdEvent.Set(); + DisconnectAllOpcUaSessions(); - try - { - _cancellationTokenSource.Cancel(); - } - finally - { - await Task.WhenAny(_executingTask, Task.Delay(Timeout.Infinite, CancellationToken.None)); - } + _reloadEvent.Close(); + _stopdEvent.Close(); + NlogHelper.Info("OPC UA 服务已经停止。"); } - protected async Task ExecuteAsync(CancellationToken stoppingToken) + private void Execute() { - NlogHelper.Info("OpcUa后台服务已启动。"); - // 订阅变量数据变化事件,以便在变量配置发生变化时重新加载。 - _dataServices.OnVariableDataChanged += HandleVariableDataChanged; + _dataServices.OnDeviceListChanged += HandleDeviceListChanged; + + while (!_stopdEvent.WaitOne(0)) + { + _reloadEvent.WaitOne(); + + if (_dataServices.Devices == null || _dataServices.Devices.Count == 0) + { + _reloadEvent.Reset(); + continue; + } + + NlogHelper.Info("OpcUa后台服务开始加载变量..."); + // 初始化时加载所有活动的 OPC UA 变量。 + LoadOpcUaVariables(); + + //连接服务器 + ConnectOpcUaService(); + // // 添加订阅变量 + SetupOpcUaSubscription(); + + if (_pollThread == null) + { + _pollThread = new Thread(PollOpcUaVariable); + _pollThread.IsBackground = true; + _pollThread.Name = "OpcUaPollThread"; + _pollThread.Start(); + } + + NlogHelper.Info("OpcUa后台服务已启动。"); + _reloadEvent.Reset(); + } - // 初始化时加载所有活动的 OPC UA 变量。 - await LoadOpcUaVariables(); // 循环运行,直到接收到停止信号。 // while (!stoppingToken.IsCancellationRequested) @@ -168,116 +137,121 @@ namespace PMSWPF.Services // } NlogHelper.Info("OpcUa后台服务正在停止。"); // 服务停止时,取消订阅事件并断开所有 OPC UA 连接。 - _dataServices.OnVariableDataChanged -= HandleVariableDataChanged; - await DisconnectAllOpcUaSessions(); + _dataServices.OnDeviceListChanged -= HandleDeviceListChanged; + } + + private void HandleDeviceListChanged(List devices) + { + NlogHelper.Info("变量数据已更改。正在重新加载 OPC UA 变量。"); + _reloadEvent.Set(); } /// /// 从数据库加载所有活动的 OPC UA 变量,并进行相应的连接和订阅管理。 /// - private async Task LoadOpcUaVariables() + private void LoadOpcUaVariables() { - Console.WriteLine("正在加载 OPC UA 变量..."); - // var allVariables = await _dataServices.GetAllVariableDataAsync(); - _opcUaDevices = _dataServices.Devices.Where(d => d.ProtocolType == ProtocolType.OpcUA && d.IsActive == true) - .ToList(); - if (_opcUaDevices.Count == 0) - return; - // 是否是初次加载 - - - _opcUaPollVariableDic.Clear(); - _opcUaSubVariableDic.Clear(); - _opcUaNodeIdVariableDic.Clear(); - foreach (var opcUaDevice in _opcUaDevices) + try { - //查找设备中所有要轮询的变量 - var dPollList = opcUaDevice?.VariableTables?.SelectMany(vt => vt.DataVariables) - .Where(vd => vd.IsActive == true && - vd.ProtocolType == ProtocolType.OpcUA && - vd.OpcUaUpdateType == OpcUaUpdateType.OpcUaPoll) - .ToList(); - // 将变量保存到字典中,方便Read后还原 - foreach (var variableData in dPollList) + var _opcUaDevices = _dataServices + .Devices.Where(d => d.ProtocolType == ProtocolType.OpcUA && d.IsActive == true) + .ToList(); + + if (_opcUaDevices.Count == 0) + return; + _deviceDic.Clear(); + _pollVariableDic.Clear(); + _subVariableDic.Clear(); + _opcUaNodeIdVariableDic.Clear(); + foreach (var opcUaDevice in _opcUaDevices) { - _opcUaNodeIdVariableDic.Add(variableData.OpcUaNodeId, variableData); + // 将设备保存到字典中,方便之后查找 + _deviceDic.Add(opcUaDevice.Id, opcUaDevice); + //查找设备中所有要轮询的变量 + var dPollList = opcUaDevice.VariableTables?.SelectMany(vt => vt.DataVariables) + .Where(vd => vd.IsActive == true && + vd.ProtocolType == ProtocolType.OpcUA && + vd.OpcUaUpdateType == OpcUaUpdateType.OpcUaPoll) + .ToList(); + // 将变量保存到字典中,方便Read后还原 + foreach (var variableData in dPollList) + { + _opcUaNodeIdVariableDic.Add(variableData.OpcUaNodeId, variableData); + } + + NlogHelper.Info($"加载OpcUa轮询变量:{dPollList.Count}个"); + _pollVariableDic.Add(opcUaDevice.Id, dPollList); + //查找设备中所有要订阅的变量 + var dSubList = opcUaDevice.VariableTables?.SelectMany(vt => vt.DataVariables) + .Where(vd => vd.IsActive == true && + vd.ProtocolType == ProtocolType.OpcUA && + vd.OpcUaUpdateType == OpcUaUpdateType.OpcUaSubscription) + .ToList(); + _subVariableDic.Add(opcUaDevice.Id, dSubList); + NlogHelper.Info($"加载OpcUa订阅变量:{dSubList.Count}个"); } - - _opcUaPollVariableDic.Add(opcUaDevice.Id, dPollList); - Console.WriteLine("已更新OPC UA轮询变量字典"); - //查找设备中所有要订阅的变量 - var dSubList = opcUaDevice?.VariableTables?.SelectMany(vt => vt.DataVariables) - .Where(vd => vd.IsActive == true && - vd.ProtocolType == ProtocolType.OpcUA && - vd.OpcUaUpdateType == OpcUaUpdateType.OpcUaSubscription) - .ToList(); - _opcUaSubVariableDic.Add(opcUaDevice.Id, dSubList); } - - - //连接服务器 - await ConnectOpcUaService(); - // // 添加订阅变量 - SetupOpcUaSubscription(); - - await PollOpcUaVariable(); + catch (Exception e) + { + NotificationHelper.ShowError($"加载OpcUa变量的过程中发生了错误:{e.Message}"); + } } /// /// 连接到 OPC UA 服务器并订阅或轮询指定的变量。 /// - /// 要订阅或轮询的变量信息。 - /// 变量所属的设备信息。 - private async Task ConnectOpcUaService() + private void ConnectOpcUaService() { - foreach (Device device in _opcUaDevices) + foreach (Device device in _deviceDic.Values) { Session session = null; // 检查是否已存在到该终结点的活动会话。 - if (_opcUaSessions.TryGetValue(device.OpcUaEndpointUrl, out session) && session.Connected) + if (_sessionsDic.TryGetValue(device.OpcUaEndpointUrl, out session) && session.Connected) { - NlogHelper.Info($"已连接到 OPC UA 终结点: {device.OpcUaEndpointUrl}"); + NlogHelper.Info($"已连接到 OPC UA 服务器: {device.OpcUaEndpointUrl}"); + continue; } - else - { - session = await OpcUaServiceHelper.CreateOpcUaSessionAsync(device.OpcUaEndpointUrl); - if (session == null) - return; // 连接失败,直接返回 - _opcUaSessions[device.OpcUaEndpointUrl] = session; - } + session = ServiceHelper.CreateOpcUaSession(device.OpcUaEndpointUrl); + if (session == null) + return; // 连接失败,直接返回 + + _sessionsDic[device.OpcUaEndpointUrl] = session; } } - private async Task PollOpcUaVariable() + private void PollOpcUaVariable() { - while (!_cancellationTokenSource.Token.IsCancellationRequested) + NlogHelper.Info("OpcUa轮询变量线程已启动,开始轮询变量...."); + while (!_stopdEvent.WaitOne(0)) { try { - foreach (var deviceId in _opcUaPollVariableDic.Keys.ToList()) + foreach (var deviceId in _pollVariableDic.Keys.ToList()) { - await Task.Delay(100,_cancellationTokenSource.Token); - var device = _dataServices.Devices.FirstOrDefault(d => d.Id == deviceId); - if (device == null || device.OpcUaEndpointUrl == String.Empty) + Thread.Sleep(100); + if (!_deviceDic.TryGetValue(deviceId, out var device) || device.OpcUaEndpointUrl == null) { NlogHelper.Warn( - $"OpcUa轮询变量时,在DataService中未找到ID为 {deviceId} 的设备,或其服务器地址为空,请检查!"); + $"OpcUa轮询变量时,在deviceDic中未找到ID为 {deviceId} 的设备,或其服务器地址为空,请检查!"); continue; } - _opcUaSessions.TryGetValue(device.OpcUaEndpointUrl, out Session session); + _sessionsDic.TryGetValue(device.OpcUaEndpointUrl, out Session session); if (session == null || !session.Connected) { - NlogHelper.Warn( - $"用于 {device.OpcUaEndpointUrl} 的 OPC UA 会话未连接。正在尝试重新连接..."); - // 尝试重新连接会话 - await ConnectOpcUaService(); - continue; + if (!_stopdEvent.WaitOne(0)) + { + NlogHelper.Warn( + $"用于 {device.OpcUaEndpointUrl} 的 OPC UA 会话未连接。正在尝试重新连接..."); + // 尝试重新连接会话 + ConnectOpcUaService(); + continue; + } } var nodesToRead = new ReadValueIdCollection(); - if (!_opcUaPollVariableDic.TryGetValue(deviceId, out var variableList)) + if (!_pollVariableDic.TryGetValue(deviceId, out var variableList)) { continue; } @@ -285,7 +259,7 @@ namespace PMSWPF.Services foreach (var variable in variableList) { // 获取变量的轮询间隔。 - if (!_pollingIntervals.TryGetValue(variable.PollLevelType, out var interval)) + if (!ServiceHelper.PollingIntervals.TryGetValue(variable.PollLevelType, out var interval)) { NlogHelper.Info($"未知的轮询级别 {variable.PollLevelType},跳过变量 {variable.Name}。"); continue; @@ -316,8 +290,7 @@ namespace PMSWPF.Services out DiagnosticInfoCollection diagnosticInfos); if (results == null || results.Count == 0) - return; - Console.WriteLine($"结果循环数量----------------------:{results.Count}"); + continue; for (int i = 0; i < results.Count; i++) { var value = results[i]; @@ -337,11 +310,12 @@ namespace PMSWPF.Services continue; } - Console.WriteLine($"轮询变量:{variable.Name}"); + // 更新变量数据 variable.DataValue = value.Value.ToString(); variable.DisplayValue = value.Value.ToString(); // 或者根据需要进行格式化 variable.UpdateTime = DateTime.Now; + NlogHelper.Info($"轮询变量:{variable.Name},值:{variable.DataValue}"); // Console.WriteLine($"结果变量跟更新时间:{variable.UpdateTime}"); // await _dataServices.UpdateVariableDataAsync(variable); } @@ -349,11 +323,11 @@ namespace PMSWPF.Services } catch (Exception ex) { - NlogHelper.Error($"OPC UA 轮询期间发生错误: {ex.Message}", ex); + NotificationHelper.ShowError($"OPC UA 轮询期间发生错误: {ex.Message}", ex); } } - NlogHelper.Info($"OPC UA 变量轮询已停止。"); + NlogHelper.Info("OpcUa轮询变量线程已停止。"); } /// @@ -372,74 +346,32 @@ namespace PMSWPF.Services } } - /// - /// 根据 PollLevelType 获取轮询间隔。 - /// - /// 轮询级别类型。 - /// 时间间隔。 - private TimeSpan GetPollInterval(PollLevelType pollLevelType) - { - return pollLevelType switch - { - PollLevelType.OneSecond => TimeSpan.FromSeconds(1), - PollLevelType.FiveSeconds => TimeSpan.FromSeconds(5), - PollLevelType.TenSeconds => TimeSpan.FromSeconds(10), - PollLevelType.ThirtySeconds => TimeSpan.FromSeconds(30), - PollLevelType.OneMinute => TimeSpan.FromMinutes(1), - PollLevelType.FiveMinutes => TimeSpan.FromMinutes(5), - PollLevelType.TenMinutes => TimeSpan.FromMinutes(10), - PollLevelType.ThirtyMinutes => TimeSpan.FromMinutes(30), - PollLevelType.OneHour => TimeSpan.FromHours(1), - _ => TimeSpan.FromSeconds(1), // 默认1秒 - }; - } - - /// - /// 断开与指定 OPC UA 服务器的连接。 - /// - /// OPC UA 服务器的终结点 URL。 - private async Task DisconnectOpcUaSession(string endpointUrl) - { - NlogHelper.Info($"正在断开 OPC UA 会话: {endpointUrl}"); - if (_opcUaSessions.TryGetValue(endpointUrl, out var session)) - { - if (_opcUaSubscriptions.TryGetValue(endpointUrl, out var subscription)) - { - // 删除订阅。 - subscription.Delete(true); - _opcUaSubscriptions.Remove(endpointUrl); - } - - - // 关闭会话。 - session.Close(); - _opcUaSessions.Remove(endpointUrl); - NotificationHelper.ShowInfo($"已从 OPC UA 服务器断开连接: {endpointUrl}"); - } - } /// /// 断开所有 OPC UA 会话。 /// - private async Task DisconnectAllOpcUaSessions() + private void DisconnectAllOpcUaSessions() { NlogHelper.Info("正在断开所有 OPC UA 会话..."); - foreach (var endpointUrl in _opcUaSessions.Keys.ToList()) + foreach (var endpointUrl in _sessionsDic.Keys.ToList()) { - await DisconnectOpcUaSession(endpointUrl); - } - } + NlogHelper.Info($"正在断开 OPC UA 会话: {endpointUrl}"); + if (_sessionsDic.TryGetValue(endpointUrl, out var session)) + { + if (_subscriptionsDic.TryGetValue(endpointUrl, out var subscription)) + { + // 删除订阅。 + subscription.Delete(true); + _subscriptionsDic.Remove(endpointUrl); + } - /// - /// 处理变量数据变化的事件。 - /// 当数据库中的变量信息发生变化时,此方法被调用以重新加载和配置 OPC UA 变量。 - /// - /// 事件发送者。 - /// 变化的变量数据列表。 - private async void HandleVariableDataChanged(List variableDatas) - { - NlogHelper.Info("变量数据已更改。正在重新加载 OPC UA 变量。"); - await LoadOpcUaVariables(); + + // 关闭会话。 + session.Close(); + _sessionsDic.Remove(endpointUrl); + NotificationHelper.ShowInfo($"已从 OPC UA 服务器断开连接: {endpointUrl}"); + } + } } @@ -451,15 +383,19 @@ namespace PMSWPF.Services /// OPC UA 服务器的终结点 URL。 private void SetupOpcUaSubscription() { - foreach (var deviceId in _opcUaSubVariableDic.Keys) + foreach (var deviceId in _subVariableDic.Keys) { var device = _dataServices.Devices.FirstOrDefault(d => d.Id == deviceId); Subscription subscription = null; // 得到session - _opcUaSessions.TryGetValue(device.OpcUaEndpointUrl, out var session); + if (!_sessionsDic.TryGetValue(device.OpcUaEndpointUrl, out var session)) + { + NlogHelper.Info($"从OpcUa会话字典中获取会话失败: {device.OpcUaEndpointUrl} "); + continue; + } // 判断设备是否已经添加了订阅 - if (_opcUaSubscriptions.TryGetValue(device.OpcUaEndpointUrl, out subscription)) + if (_subscriptionsDic.TryGetValue(device.OpcUaEndpointUrl, out subscription)) { NlogHelper.Info($"OPC UA 终结点 {device.OpcUaEndpointUrl} 已存在订阅。"); } @@ -469,11 +405,11 @@ namespace PMSWPF.Services subscription.PublishingInterval = 1000; // 发布间隔(毫秒) session.AddSubscription(subscription); subscription.Create(); - _opcUaSubscriptions[device.OpcUaEndpointUrl] = subscription; + _subscriptionsDic[device.OpcUaEndpointUrl] = subscription; } // 将变量添加到订阅 - foreach (VariableData variable in _opcUaSubVariableDic[deviceId]) + foreach (VariableData variable in _subVariableDic[deviceId]) { // 7. 创建监控项并添加到订阅中。 MonitoredItem monitoredItem = new MonitoredItem(subscription.DefaultItem); @@ -490,7 +426,7 @@ namespace PMSWPF.Services subscription.ApplyChanges(); // 应用更改 } - NlogHelper.Info($"设备: {device.Name}, 添加了 {(_opcUaSubVariableDic[deviceId]?.Count ?? 0)} 个订阅变量。"); + NlogHelper.Info($"设备: {device.Name}, 添加了 {(_subVariableDic[deviceId]?.Count ?? 0)} 个订阅变量。"); } } } diff --git a/Services/S7BackgroundService.cs b/Services/S7BackgroundService.cs index 18da0f4..1e591d6 100644 --- a/Services/S7BackgroundService.cs +++ b/Services/S7BackgroundService.cs @@ -10,6 +10,8 @@ using S7.Net; using PMSWPF.Models; using PMSWPF.Enums; using PMSWPF.Helper; +using S7.Net.Types; +using DateTime = System.DateTime; namespace PMSWPF.Services { @@ -21,46 +23,31 @@ namespace PMSWPF.Services // 数据服务实例,用于访问和操作应用程序数据,如设备配置。 private readonly DataServices _dataServices; + // 存储 S7设备,键为设备Id,值为会话对象。 + private readonly Dictionary _deviceDic; + + // 储存所有要轮询更新的变量,键是Device.Id,值是这个设备所有要轮询的变量 + private readonly Dictionary> _pollVariableDic; // Key: VariableData.Id + // 存储S7 PLC客户端实例的字典,键为设备ID,值为Plc对象。 - private readonly Dictionary _s7PlcClients = new Dictionary(); + private readonly Dictionary _s7PlcClientDic; + + // 储存所有变量的字典,方便通过id获取变量对象 + private readonly Dictionary _variableDic; + // S7轮询一次读取的变量数,不得大于15 + private readonly int S7PollOnceReadMultipleVars = 9; + // S7轮询一遍后的等待时间 + private readonly int S7PollOnceSleepTimeMs = 100; // 轮询数据的线程。 private Thread _pollingThread; - // 用于取消轮询操作的CancellationTokenSource。 - private CancellationTokenSource _cancellationTokenSource; + private ManualResetEvent _reloadEvent = new ManualResetEvent(false); + private ManualResetEvent _stopEvent = new ManualResetEvent(false); - // 读取变量计数器。 - private int readCount = 0; - - // 跳过变量计数器(未到轮询时间)。 - private int TGCount = 0; - - // 定义不同轮询级别的间隔时间。 - private readonly Dictionary _pollingIntervals = new Dictionary - { - { PollLevelType.TenMilliseconds, TimeSpan.FromMilliseconds((int)PollLevelType.TenMilliseconds) }, - { - PollLevelType.HundredMilliseconds, TimeSpan.FromMilliseconds((int)PollLevelType.HundredMilliseconds) - }, - { - PollLevelType.FiveHundredMilliseconds, - TimeSpan.FromMilliseconds((int)PollLevelType.FiveHundredMilliseconds) - }, - { PollLevelType.OneSecond, TimeSpan.FromMilliseconds((int)PollLevelType.OneSecond) }, - { PollLevelType.FiveSeconds, TimeSpan.FromMilliseconds((int)PollLevelType.FiveSeconds) }, - { PollLevelType.TenSeconds, TimeSpan.FromMilliseconds((int)PollLevelType.TenSeconds) }, - { PollLevelType.TwentySeconds, TimeSpan.FromMilliseconds((int)PollLevelType.TwentySeconds) }, - { PollLevelType.ThirtySeconds, TimeSpan.FromMilliseconds((int)PollLevelType.ThirtySeconds) }, - { PollLevelType.OneMinute, TimeSpan.FromMilliseconds((int)PollLevelType.OneMinute) }, - { PollLevelType.ThreeMinutes, TimeSpan.FromMilliseconds((int)PollLevelType.ThreeMinutes) }, - { PollLevelType.FiveMinutes, TimeSpan.FromMilliseconds((int)PollLevelType.FiveMinutes) }, - { PollLevelType.TenMinutes, TimeSpan.FromMilliseconds((int)PollLevelType.TenMinutes) }, - { PollLevelType.ThirtyMinutes, TimeSpan.FromMilliseconds((int)PollLevelType.ThirtyMinutes) } - }; // 存储S7设备列表。 - private List? _s7Devices; + private Thread _serviceMainThread; /// /// 构造函数,注入ILogger和DataServices。 @@ -70,6 +57,10 @@ namespace PMSWPF.Services public S7BackgroundService(DataServices dataServices) { _dataServices = dataServices; + _deviceDic = new(); + _pollVariableDic = new(); + _s7PlcClientDic = new(); + _variableDic = new(); // 订阅设备列表变更事件,以便在设备配置更新时重新加载。 _dataServices.OnDeviceListChanged += HandleDeviceListChanged; } @@ -79,15 +70,10 @@ namespace PMSWPF.Services /// /// 事件发送者。 /// 更新后的设备列表。 - private void HandleDeviceListChanged(object sender, List devices) + private void HandleDeviceListChanged(List devices) { - // 过滤出S7协议且激活的设备。 - _s7Devices = devices.Where(d => d.ProtocolType == ProtocolType.S7 && d.IsActive) - .ToList(); - // 当设备列表变化时,更新PLC客户端 - // 这里需要更复杂的逻辑来处理连接的关闭和新连接的建立 - // 简单起见,这里只做日志记录 NlogHelper.Info("设备列表已更改。S7客户端可能需要重新初始化。"); + _reloadEvent.Set(); } /// @@ -96,237 +82,272 @@ namespace PMSWPF.Services public void StartService() { NlogHelper.Info("S7后台服务正在启动。"); - // 创建一个CancellationTokenSource,用于控制轮询线程的取消。 - _cancellationTokenSource = new CancellationTokenSource(); - - // 创建并启动轮询线程。 - _pollingThread = new Thread(() => PollingLoop(_cancellationTokenSource.Token)) - { - IsBackground = true // 设置为后台线程,随主程序退出而退出 - }; - _pollingThread.Start(); + _reloadEvent.Set(); + _serviceMainThread = new Thread(Execute); + _serviceMainThread.IsBackground = true; + _serviceMainThread.Name = "S7ServiceMainThread"; + _serviceMainThread.Start(); } - /// - /// 轮询循环方法,在新线程中执行,周期性地读取S7设备数据。 - /// - /// 用于取消轮询的CancellationToken。 - private void PollingLoop(CancellationToken stoppingToken) + private void Execute() { - NlogHelper.Info("S7轮询线程已启动。"); - // 注册取消回调,当服务停止时记录日志。 - stoppingToken.Register(() => NlogHelper.Info("S7后台服务正在停止。")); - - // 初始加载S7设备列表。 - _s7Devices = _dataServices.Devices?.Where(d => d.ProtocolType == ProtocolType.S7 && d.IsActive) - .ToList(); - - // 轮询循环,直到收到取消请求。 - while (!stoppingToken.IsCancellationRequested) - { - // _logger.LogDebug("S7后台服务正在执行后台工作。"); - // _logger.LogDebug($"开始轮询变量,当前时间:{DateTime.Now}"); - readCount = 0; - TGCount = 0; - - Stopwatch stopwatch = Stopwatch.StartNew(); // 启动计时器,测量轮询耗时 - PollS7Devices(stoppingToken); // 执行S7设备轮询 - stopwatch.Stop(); // 停止计时器 - // _logger.LogDebug($"结束轮询变量,当前时间:{DateTime.Now}"); - NlogHelper.Info($"读取变量数:{readCount}个,跳过变量数:{TGCount}",throttle:true); - - // 短暂休眠以防止CPU占用过高,并控制轮询频率。 - Thread.Sleep(100); - } - - NlogHelper.Info("S7轮询线程已停止。"); - } - - - /// - /// 初始化或重新连接PLC客户端。 - /// - /// S7设备。 - /// 连接成功的Plc客户端实例,如果连接失败则返回null。 - private Plc? InitializePlcClient(Device device) - { - // 检查字典中是否已存在该设备的PLC客户端。 - if (!_s7PlcClients.TryGetValue(device.Id, out var plcClient)) - { - // 如果不存在,则创建新的Plc客户端。 - try - { - plcClient = new Plc(device.CpuType, device.Ip, (short)device.Prot, device.Rack, device.Slot); - plcClient.Open(); // 尝试打开连接。 - _s7PlcClients[device.Id] = plcClient; // 将新创建的客户端添加到字典。 - NlogHelper.Info($"已连接到S7 PLC: {device.Name} ({device.Ip})"); - } - catch (Exception ex) - { - NotificationHelper.ShowError($"连接S7 PLC失败: {device.Name} ({device.Ip})", ex); - return null; // 连接失败,返回null。 - } - } - else if (!plcClient.IsConnected) - { - // 如果存在但未连接,则尝试重新连接。 - try - { - plcClient.Open(); // 尝试重新打开连接。 - NlogHelper.Info($"已重新连接到S7 PLC: {device.Name} ({device.Ip})"); - } - catch (Exception ex) - { - NotificationHelper.ShowError($"重新连接S7 PLC失败: {device.Name} ({device.Ip})", ex); - return null; // 重新连接失败,返回null。 - } - } - - return plcClient; // 返回连接成功的Plc客户端。 - } - - /// - /// 轮询S7设备数据。 - /// - /// 取消令牌。 - private void PollS7Devices(CancellationToken stoppingToken) - { - // 如果没有活跃的S7设备,则等待一段时间后重试。 - if (_s7Devices == null || !_s7Devices.Any()) - { - NlogHelper.Info( - "未找到活跃的S7设备进行轮询。等待5秒后重试。",throttle:true); - try - { - // 使用CancellationToken来使等待可取消。 - Task.Delay(TimeSpan.FromSeconds(5), stoppingToken) - .Wait(stoppingToken); - } - catch (OperationCanceledException) - { - // 如果在等待期间取消,则退出。 - return; - } - - return; - } - - // 遍历所有S7设备。 - foreach (var device in _s7Devices) - { - if (stoppingToken.IsCancellationRequested) return; // 如果收到取消请求,则停止。 - - // 尝试获取或初始化PLC客户端连接。 - var plcClient = InitializePlcClient(device); - if (plcClient == null) - { - continue; // 如果连接失败,则跳过当前设备。 - } - - - // 读取设备变量。 - ReadDeviceVariables(plcClient, device, stoppingToken); - } - } - - /// - /// 读取设备的S7变量并更新其值。 - /// - /// 已连接的Plc客户端实例。 - /// S7设备。 - /// 取消令牌。 - private void ReadDeviceVariables(Plc plcClient, Device device, CancellationToken stoppingToken) - { - // 过滤出当前设备和S7协议相关的变量。 - var s7Variables = device.VariableTables - .Where(vt => vt.ProtocolType == ProtocolType.S7 && vt.IsActive) - .SelectMany(vt => vt.DataVariables) - .ToList(); - - if (!s7Variables.Any()) - { - NlogHelper.Info($"设备 {device.Name} 没有找到活跃的S7变量。"); - return; - } - try { - // 遍历并读取每个S7变量。 - foreach (var variable in s7Variables) + while (!_stopEvent.WaitOne(0)) { - // Stopwatch stopwatch = Stopwatch.StartNew(); - if (stoppingToken.IsCancellationRequested) return; // 如果取消令牌被请求,则停止读取。 - - // 获取变量的轮询间隔。 - if (!_pollingIntervals.TryGetValue(variable.PollLevelType, out var interval)) + _reloadEvent.WaitOne(); + if (_dataServices.Devices == null || _dataServices.Devices.Count == 0) { - NlogHelper.Info($"未知轮询级别 {variable.PollLevelType},跳过变量 {variable.Name}。"); + _reloadEvent.Reset(); continue; } - // 检查是否达到轮询时间。 - if ((DateTime.Now - variable.LastPollTime) < interval) + LoadVariables(); + ConnectS7Service(); + if (_pollingThread == null) { - TGCount++; - continue; // 未到轮询时间,跳过。 + _pollingThread = new Thread(PollS7Variable); + _pollingThread.IsBackground = true; + _pollingThread.Name = "S7ServicePollingThread"; + _pollingThread.Start(); } + _reloadEvent.Reset(); + } + } + catch (Exception e) + { + NlogHelper.Error($"S7后台服务主线程运行中发生了错误:{e.Message}",e); + DisconnectAllPlc(); + } + } + + private void PollS7Variable() + { + try + { + NlogHelper.Info("S7后台服务开始轮询变量...."); + while (!_stopEvent.WaitOne(0)) + { + Thread.Sleep(S7PollOnceSleepTimeMs); try { - // 从PLC读取变量值。 - var value = plcClient.Read(variable.S7Address); - if (value != null) + // Stopwatch sw = Stopwatch.StartNew(); + int varCount = 0; + // 遍历并读取每个S7变量。 + foreach (var deviceId in _pollVariableDic.Keys.ToList()) { - // 更新变量的原始数据值和显示值。 - variable.DataValue = value.ToString(); - variable.DisplayValue - = SiemensHelper.ConvertS7Value(value, variable.DataType, variable.Converstion); - variable.UpdateTime = DateTime.Now; + if (!_deviceDic.TryGetValue(deviceId, out var device)) + { + NlogHelper.Warn($"S7服务轮询时在deviceDic中没有找到Id为:{deviceId}的设备"); + continue; + } - variable.LastPollTime = DateTime.Now; // 更新最后轮询时间。 - readCount++; - // _logger.LogDebug($"线程ID:{Environment.CurrentManagedThreadId},已读取变量 {variable.Name}: {variable.DataValue}"); + if (!_s7PlcClientDic.TryGetValue(device.Ip, out var plcClient)) + { + NlogHelper.Warn($"S7服务轮询时没有找到设备I:{deviceId}的初始化好的Plc客户端对象!"); + continue; + } + + if (!plcClient.IsConnected) + { + NlogHelper.Warn($"S7服务轮询时设备:{device.Name},没有连接,正在重新连接..."); + ConnectS7Service(); + continue; + } + + if (!_pollVariableDic.TryGetValue(deviceId, out var variableList)) + { + NlogHelper.Warn($"S7服务轮询时没有找到设备I:{deviceId},要轮询的变量列表!"); + continue; + } + + Dictionary readVarDic = new Dictionary(); + + foreach (var variable in variableList) + { + // 获取变量的轮询间隔。 + if (!ServiceHelper.PollingIntervals.TryGetValue(variable.PollLevelType, out var interval)) + { + NlogHelper.Info($"未知轮询级别 {variable.PollLevelType},跳过变量 {variable.Name}。"); + continue; + } + + // 检查是否达到轮询时间。 + if ((DateTime.Now - variable.UpdateTime) < interval) + continue; // 未到轮询时间,跳过。 + + try + { + readVarDic.Add(variable.Id, DataItem.FromAddress(variable.S7Address)); + varCount++; + if (readVarDic.Count == S7PollOnceReadMultipleVars) + { + // 批量读取 + plcClient.ReadMultipleVars(readVarDic.Values.ToList()); + // 批量读取后还原结果 + foreach (var varId in readVarDic.Keys.ToList()) + { + DataItem dataItem = readVarDic[varId]; + if (!_variableDic.TryGetValue(varId, out var variableData)) + { + NlogHelper.Warn($"S7后台服务批量读取变量后,还原值,在_variableDic中找不到ID为{varId}的变量"); + continue; + } + + // 更新变量的原始数据值和显示值。 + variableData.DataValue = dataItem.Value.ToString(); + variableData.UpdateTime = DateTime.Now; + Console.WriteLine($"S7轮询变量:{variableData.Name},值:{variableData.DataValue}"); + } + + readVarDic.Clear(); + } + + } + catch (Exception ex) + { + NlogHelper.Warn($"从设备 {device.Name} 读取变量 {variable.Name} 失败:{ex.Message}"); + } + } } + + // sw.Stop(); + // Console.WriteLine( + // $"S7轮询设备数:{_pollVariableDic.Count},共轮询变量数:{varCount},总耗时:{sw.ElapsedMilliseconds}ms"); } catch (Exception ex) { - NotificationHelper.ShowError( $"从设备 {device.Name} 读取变量 {variable.Name} 失败。",ex); + NotificationHelper.ShowError($"S7后台服务在轮询变量过程中发生错误:{ex.Message}", ex); } - // stopwatch.Stop(); - // NlogHelper.Info($"读取变量耗时:{stopwatch.ElapsedMilliseconds}ms "); } + + NlogHelper.Info("S7后台服务轮询变量结束。"); } - catch (Exception ex) + catch (Exception e) { - NotificationHelper.ShowError($"设备 {device.Name} 批量读取过程中发生错误。",ex); + NlogHelper.Error($"S7后台服务轮询变量线程运行中发生了错误:{e.Message}",e); + DisconnectAllPlc(); } } + private void ConnectS7Service() + { + try + { + // 检查字典中是否已存在该设备的PLC客户端。 + foreach (var device in _deviceDic.Values.ToList()) + { + if (_s7PlcClientDic.TryGetValue(device.Ip, out var plc)) + { + NlogHelper.Info($"已连接到 OPC UA 服务器: {device.Ip}:{device.Prot}"); + continue; + } + + // 如果不存在或者没有连接,则创建新的Plc客户端。 + var plcClient = new Plc(device.CpuType, device.Ip, (short)device.Prot, device.Rack, device.Slot); + plcClient.Open(); // 尝试打开连接。 + // 将新创建的客户端添加到字典。 + if (_s7PlcClientDic.ContainsKey(device.Ip)) + { + _s7PlcClientDic[device.Ip] = plcClient; + } + else + { + _s7PlcClientDic.Add(device.Ip, plcClient); + } + + NotificationHelper.ShowSuccess($"已连接到S7 PLC: {device.Name} ({device.Ip})"); + } + } + catch (Exception e) + { + NotificationHelper.ShowError($"S7服务连接PLC的过程中发生错误:{e}"); + } + } + + private void LoadVariables() + { + try + { + _deviceDic.Clear(); + _pollVariableDic.Clear(); + + NlogHelper.Info("开始加载S7变量...."); + var _s7Devices = _dataServices + .Devices.Where(d => d.IsActive == true && d.ProtocolType == ProtocolType.S7) + .ToList(); + int varCount = 0; + foreach (var device in _s7Devices) + { + _deviceDic.Add(device.Id, device); + // 过滤出当前设备和S7协议相关的变量。 + var s7Variables = device.VariableTables + .Where(vt => vt.ProtocolType == ProtocolType.S7 && vt.IsActive) + .SelectMany(vt => vt.DataVariables) + .Where(vd => vd.IsActive == true) + .ToList(); + // 将变量存储到字典中,方便以后通过ID快速查找 + foreach (var s7Variable in s7Variables) + { + if (_variableDic.ContainsKey(s7Variable.Id)) + { + _variableDic[s7Variable.Id] = s7Variable; + } + else + { + _variableDic.Add(s7Variable.Id, s7Variable); + } + } + + varCount += s7Variables.Count(); + _pollVariableDic.Add(device.Id, s7Variables); + } + + NlogHelper.Info($"S7变量加载成功,共加载S7设备:{_s7Devices.Count}个,变量数:{varCount}"); + } + catch (Exception e) + { + NotificationHelper.ShowError($"S7后台服务加载变量时发生了错误:{e.Message}", e); + } + } + + /// /// 停止S7后台服务。 /// public void StopService() { - NlogHelper.Info("S7 Background Service is stopping."); + NlogHelper.Info("S7后台服务正在关闭...."); + _stopEvent.Set(); + + _pollingThread.Interrupt(); + _serviceMainThread.Interrupt(); + DisconnectAllPlc(); - // 发出信号,请求轮询线程停止。 - _cancellationTokenSource?.Cancel(); - // 等待轮询线程完成。 - _pollingThread?.Join(); + _reloadEvent.Close(); + _stopEvent.Reset(); + _stopEvent.Close(); + // 清空所有字典。 + _deviceDic.Clear(); + _s7PlcClientDic.Clear(); + _pollVariableDic.Clear(); + NlogHelper.Info("S7后台服务已关闭"); + } + private void DisconnectAllPlc() + { + if (_s7PlcClientDic==null || _s7PlcClientDic.Count == 0) + return; // 关闭所有活跃的PLC连接。 - foreach (var plcClient in _s7PlcClients.Values) + foreach (var plcClient in _s7PlcClientDic.Values.ToList()) { if (plcClient.IsConnected) { plcClient.Close(); - NlogHelper.Info($"Closed S7 PLC connection: {plcClient.IP}"); + NlogHelper.Info($"关闭S7连接: {plcClient.IP}"); } } - - _s7PlcClients.Clear(); // 清空PLC客户端字典。 - - } } } \ No newline at end of file diff --git a/ViewModels/DevicesViewModel.cs b/ViewModels/DevicesViewModel.cs index e3de85c..70d4c3b 100644 --- a/ViewModels/DevicesViewModel.cs +++ b/ViewModels/DevicesViewModel.cs @@ -52,13 +52,20 @@ public partial class DevicesViewModel : ViewModelBase _dialogService = dialogService; _dataServices = dataServices; - MessageHelper.SendLoadMessage(LoadTypes.Devices); - _dataServices.OnDeviceListChanged += (sender, devices) => + _dataServices.OnDeviceListChanged += (devices) => { Devices = new ObservableCollection(devices); }; } + public override void OnLoaded() + { + if (_dataServices.Devices!=null && _dataServices.Devices.Count>0) + { + Devices=new ObservableCollection(_dataServices.Devices); + } + } + /// /// 添加设备命令。 /// diff --git a/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs b/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs index cdc8a22..1ea1eea 100644 --- a/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs +++ b/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs @@ -58,7 +58,7 @@ public partial class OpcUaImportDialogViewModel : ObservableObject OpcUaNodes.Clear(); SelectedNodeVariables.Clear(); - _session = await OpcUaServiceHelper.CreateOpcUaSessionAsync(EndpointUrl); + _session = await ServiceHelper.CreateOpcUaSessionAsync(EndpointUrl); NotificationHelper.ShowSuccess($"已连接到 OPC UA 服务器: {EndpointUrl}"); IsConnected = true; diff --git a/ViewModels/Dialogs/OpcUaUpdateTypeDialogViewModel.cs b/ViewModels/Dialogs/OpcUaUpdateTypeDialogViewModel.cs new file mode 100644 index 0000000..806ab73 --- /dev/null +++ b/ViewModels/Dialogs/OpcUaUpdateTypeDialogViewModel.cs @@ -0,0 +1,18 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using PMSWPF.Enums; + +namespace PMSWPF.ViewModels.Dialogs +{ + public partial class OpcUaUpdateTypeDialogViewModel : ObservableObject + { + [ObservableProperty] + private OpcUaUpdateType _selectedUpdateType; + + + public OpcUaUpdateTypeDialogViewModel() + { + // 默认选中第一个 + SelectedUpdateType = OpcUaUpdateType.OpcUaPoll; + } + } +} \ No newline at end of file diff --git a/ViewModels/MainViewModel.cs b/ViewModels/MainViewModel.cs index 123bfd0..db64b48 100644 --- a/ViewModels/MainViewModel.cs +++ b/ViewModels/MainViewModel.cs @@ -1,9 +1,8 @@ using System.Collections.ObjectModel; -using System.Windows; // Add this using directive +using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; -using CommunityToolkit.Mvvm.Input; // Add this using directive +using CommunityToolkit.Mvvm.Input; using iNKORE.UI.WPF.Modern.Common.IconKeys; -using iNKORE.UI.WPF.Modern.Controls; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using PMSWPF.Data; @@ -12,6 +11,9 @@ using PMSWPF.Enums; using PMSWPF.Helper; using PMSWPF.Models; using PMSWPF.Services; +using PMSWPF.Views; +// Add this using directive +// Add this using directive namespace PMSWPF.ViewModels; @@ -63,7 +65,7 @@ public partial class MainViewModel : ViewModelBase // 发送消息加载数据 MessageHelper.SendLoadMessage(LoadTypes.All); // 当菜单加载成功后,在前台显示菜单 - dataServices.OnMenuTreeListChanged += (sender, menus) => { Menus = new ObservableCollection(menus); }; + dataServices.OnMenuTreeListChanged += ( menus) => { Menus = new ObservableCollection(menus); }; } /// @@ -72,7 +74,7 @@ public partial class MainViewModel : ViewModelBase [RelayCommand] private void ShowWindow() { - if (Application.Current.MainWindow is Views.MainView mainWindow) + if (Application.Current.MainWindow is MainView mainWindow) { mainWindow.ShowApplication(); } diff --git a/ViewModels/MqttsViewModel.cs b/ViewModels/MqttsViewModel.cs index 57430e6..ac14924 100644 --- a/ViewModels/MqttsViewModel.cs +++ b/ViewModels/MqttsViewModel.cs @@ -70,7 +70,7 @@ public partial class MqttsViewModel : ViewModelBase } - _dataServices.OnMqttListChanged += (sender, mqtts) => + _dataServices.OnMqttListChanged += ( mqtts) => { Mqtts = new ObservableCollection(mqtts); }; diff --git a/ViewModels/VariableTableViewModel.cs b/ViewModels/VariableTableViewModel.cs index f8eba35..b7d63bb 100644 --- a/ViewModels/VariableTableViewModel.cs +++ b/ViewModels/VariableTableViewModel.cs @@ -344,6 +344,7 @@ partial class VariableTableViewModel : ViewModelBase foreach (var variableData in importVarDataList) { variableData.CreateTime = DateTime.Now; + variableData.IsActive = true; variableData.VariableTableId = VariableTable.Id; } @@ -538,31 +539,66 @@ partial class VariableTableViewModel : ViewModelBase /// /// 要修改轮询频率的变量数据列表。 [RelayCommand] - public async Task ChangePollLevel(List variablesToChange) + public async Task ChangePollLevel(IList variablesToChange) { + var validVariables = variablesToChange?.OfType().ToList(); + // 检查是否有变量被选中 - if (variablesToChange == null || !variablesToChange.Any()) + if (validVariables == null || !validVariables.Any()) { NotificationHelper.ShowInfo("请选择要修改轮询频率的变量"); return; } // 显示轮询频率选择对话框,并传入第一个变量的当前轮询频率作为默认值 - var newPollLevelType = await _dialogService.ShowPollLevelDialog(variablesToChange.First() + var newPollLevelType = await _dialogService.ShowPollLevelDialog(validVariables.First() .PollLevelType); if (newPollLevelType.HasValue) { // 更新所有选定变量的轮询频率和修改状态 - foreach (var variable in variablesToChange) + foreach (var variable in validVariables) { variable.PollLevelType = newPollLevelType.Value; variable.IsModified = false; // 标记为未修改,因为已保存到数据库 } // 批量更新数据库中的变量数据 - await _varDataRepository.UpdateAsync(variablesToChange); + await _varDataRepository.UpdateAsync(validVariables); // 显示成功通知 - NotificationHelper.ShowSuccess($"已成功更新 {variablesToChange.Count} 个变量的轮询频率"); + NotificationHelper.ShowSuccess($"已成功更新 {validVariables.Count} 个变量的轮询频率"); + } + } + + /// + /// 修改选定变量的OPC UA更新方式(轮询或订阅)。 + /// + /// 要修改更新方式的变量数据列表。 + [RelayCommand] + public async Task ModifyOpcUaUpdateType(IList variablesToChange) + { + // 过滤出有效的VariableData对象 + var validVariables = variablesToChange?.OfType().ToList(); + + if (validVariables == null || !validVariables.Any()) + { + NotificationHelper.ShowInfo("请选择要修改更新方式的OPC UA变量"); + return; + } + + + // 显示更新方式选择对话框 + var newUpdateType = await _dialogService.ShowOpcUaUpdateTypeDialog(); + if (newUpdateType.HasValue) + { + // 更新所有选定变量的更新方式 + foreach (var variable in validVariables) + { + variable.OpcUaUpdateType = newUpdateType.Value; + } + + // 批量更新数据库 + await _varDataRepository.UpdateAsync(validVariables); + NotificationHelper.ShowSuccess($"已成功为 {validVariables.Count} 个变量更新OPC UA更新方式。"); } } @@ -572,10 +608,12 @@ partial class VariableTableViewModel : ViewModelBase /// /// 要添加MQTT服务器的变量数据列表。 [RelayCommand] - public async Task AddMqttServerToVariables(IList variablesToAddMqtt) + public async Task AddMqttServerToVariables(IList variablesToAddMqtt) { + var validVariables = variablesToAddMqtt?.OfType().ToList(); + // 检查是否有变量被选中 - if (variablesToAddMqtt == null || !variablesToAddMqtt.Any()) + if (validVariables == null || !validVariables.Any()) { NotificationHelper.ShowInfo("请选择要添加MQTT服务器的变量"); return; @@ -591,7 +629,7 @@ partial class VariableTableViewModel : ViewModelBase } // 遍历所有选定的变量,并为其添加选定的MQTT服务器 - foreach (VariableData variable in variablesToAddMqtt) + foreach (VariableData variable in validVariables) { // 如果变量的Mqtts集合为空,则初始化它 if (variable.Mqtts == null) @@ -608,9 +646,9 @@ partial class VariableTableViewModel : ViewModelBase } // 批量更新数据库中的变量数据 - await _varDataRepository.UpdateAsync(variablesToAddMqtt.ToList()); + await _varDataRepository.UpdateAsync(validVariables.ToList()); // 显示成功通知 - NotificationHelper.ShowSuccess($"已成功为 {variablesToAddMqtt.Count} 个变量添加MQTT服务器: {selectedMqtt.Name}"); + NotificationHelper.ShowSuccess($"已成功为 {validVariables.Count} 个变量添加MQTT服务器: {selectedMqtt.Name}"); } catch (Exception ex) { diff --git a/Views/Dialogs/OpcUaUpdateTypeDialog.xaml b/Views/Dialogs/OpcUaUpdateTypeDialog.xaml new file mode 100644 index 0000000..2fc827e --- /dev/null +++ b/Views/Dialogs/OpcUaUpdateTypeDialog.xaml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/Views/Dialogs/OpcUaUpdateTypeDialog.xaml.cs b/Views/Dialogs/OpcUaUpdateTypeDialog.xaml.cs new file mode 100644 index 0000000..85f9296 --- /dev/null +++ b/Views/Dialogs/OpcUaUpdateTypeDialog.xaml.cs @@ -0,0 +1,14 @@ +using iNKORE.UI.WPF.Modern.Controls; +using PMSWPF.ViewModels.Dialogs; + +namespace PMSWPF.Views.Dialogs +{ + public partial class OpcUaUpdateTypeDialog + { + public OpcUaUpdateTypeDialog(OpcUaUpdateTypeDialogViewModel viewModel) + { + InitializeComponent(); + DataContext = viewModel; + } + } +} diff --git a/Views/VariableTableView.xaml b/Views/VariableTableView.xaml index de5ecdc..b59cd03 100644 --- a/Views/VariableTableView.xaml +++ b/Views/VariableTableView.xaml @@ -210,13 +210,23 @@ + Command="{Binding ChangePollLevelCommand}" + CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType=ContextMenu}}"> + + + + + + Command="{Binding AddMqttServerToVariablesCommand}" + CommandParameter="{Binding PlacementTarget.SelectedItems, RelativeSource={RelativeSource AncestorType=ContextMenu}}"> @@ -271,9 +281,6 @@ Header="OPCUA节点ID" Visibility="{Binding Source={StaticResource proxy}, Path=Data.IsOpcUaProtocolSelected, Converter={StaticResource BooleanToVisibilityConverter}}" Binding="{Binding OpcUaNodeId}" /> - - - diff --git a/Views/VariableTableView.xaml.cs b/Views/VariableTableView.xaml.cs index 9ff1542..5fedba4 100644 --- a/Views/VariableTableView.xaml.cs +++ b/Views/VariableTableView.xaml.cs @@ -120,46 +120,4 @@ public partial class VariableTableView : UserControl NotificationHelper.ShowInfo("请选择要删除的变量"); } } - - private async void ChangePollLevel_Click(object sender, RoutedEventArgs args) - { - try - { - _viewModel = (VariableTableViewModel)this.DataContext; - var selectedVariables = BasicGridView.SelectedItems.Cast().ToList(); - if (selectedVariables.Any()) - { - await _viewModel.ChangePollLevel(selectedVariables); - } - else - { - NotificationHelper.ShowInfo("请选择要修改轮询频率的变量"); - } - } - catch (Exception e) - { - NotificationHelper.ShowError("修改轮询频率的过程中发生了错误:" + e.Message, e); - } - } - - private async void AddMqttServerToVariables_Click(object sender, RoutedEventArgs e) - { - try - { - _viewModel = (VariableTableViewModel)this.DataContext; - var selectedVariables = BasicGridView.SelectedItems.Cast().ToList(); - if (selectedVariables.Any()) - { - await _viewModel.AddMqttServerToVariables(selectedVariables); - } - else - { - NotificationHelper.ShowInfo("请选择要添加MQTT服务器的变量"); - } - } - catch (Exception ex) - { - NotificationHelper.ShowError("给变量添加MQTT服务器的过程中发生了错误:" + ex.Message, ex); - } - } } \ No newline at end of file