diff --git a/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj b/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj index a490932d..6b2d13dc 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj +++ b/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj @@ -1,13 +1,13 @@  - 9.0.0-beta01 + 9.0.0 BootstrapBlazor.OpcDa - Bootstrap Blazor WebAssembly wasm UI Components SqlSugar - Bootstrap UI components extensions of SqlSugar + Bootstrap Blazor WebAssembly wasm UI Components OpcDa PLC + Bootstrap UI components extensions of OpcDaServer diff --git a/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs b/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs index ccc0e604..1e29ec9a 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs @@ -2,6 +2,8 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Website: https://www.blazor.zone or https://argozhang.github.io/ +using Opc.Da; + namespace BootstrapBlazor.OpcDa; /// @@ -11,13 +13,15 @@ internal static class Extensions { public static Quality ToQuality(this Opc.Da.Quality quality) { - return quality.QualityBits == Opc.Da.qualityBits.good + return quality.QualityBits == qualityBits.good ? Quality.Good : Quality.Bad; } - public static ISubscription ToOpcSubscription(this Opc.Da.ISubscription subscription) + public static IOpcSubscription ToOpcSubscription(this ISubscription subscription) { return new OpcSubscription(subscription); } + + public static ISubscription CreateSubscription(this Server server, string name, int updateRate = 1000, bool active = true) => server.CreateSubscription(new SubscriptionState { Name = name, Deadband = 0, UpdateRate = updateRate, Active = active }); } diff --git a/src/extensions/BootstrapBlazor.OpcDa/Extensions/ServiceCollectionExtensions.cs b/src/extensions/BootstrapBlazor.OpcDa/Extensions/ServiceCollectionExtensions.cs index bf5db6fb..d374c569 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/Extensions/ServiceCollectionExtensions.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/Extensions/ServiceCollectionExtensions.cs @@ -8,19 +8,30 @@ namespace Microsoft.Extensions.DependencyInjection; /// -/// Opc Da 服务扩展类 +/// OpcDaServer 服务扩展类 /// public static class ServiceCollectionExtensions { /// - /// 增加 Opc 操作服务 + /// 增加 OpcDaServer 操作服务 /// /// /// [SupportedOSPlatform("windows")] - public static IServiceCollection AddOpcServer(this IServiceCollection services) + public static IServiceCollection AddOpcDaServer(this IServiceCollection services) { - services.AddSingleton(); + services.AddSingleton(); + return services; + } + + /// + /// 增加模拟 OpcDaServer 操作服务 + /// + /// + /// + public static IServiceCollection AddMockOpcDaServer(this IServiceCollection services) + { + services.AddSingleton(); return services; } } diff --git a/src/extensions/BootstrapBlazor.OpcDa/IOpcServer.cs b/src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.cs similarity index 87% rename from src/extensions/BootstrapBlazor.OpcDa/IOpcServer.cs rename to src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.cs index 344fcbd0..26f015b0 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/IOpcServer.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.cs @@ -5,9 +5,9 @@ namespace BootstrapBlazor.OpcDa; /// -/// Opc Server 接口定义 +/// OpcDaServer 接口定义 /// -public interface IOpcServer : IDisposable +public interface IOpcDaServer : IDisposable { /// /// 获得 OPC Server 是否已连接 @@ -35,7 +35,7 @@ public interface IOpcServer : IDisposable /// 取消订阅方法 /// /// - void CancelSubscription(ISubscription subscription); + void CancelSubscription(IOpcSubscription subscription); /// /// 创建订阅方法 @@ -44,7 +44,7 @@ public interface IOpcServer : IDisposable /// 更新频率 默认 1000 毫秒 /// 是否激活 默认 true /// - ISubscription CreateSubscription(string name, int updateRate = 1000, bool active = true); + IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true); /// /// 读取 Item 值方法 diff --git a/src/extensions/BootstrapBlazor.OpcDa/ISubscription.cs b/src/extensions/BootstrapBlazor.OpcDa/IOpcSubscription.cs similarity index 83% rename from src/extensions/BootstrapBlazor.OpcDa/ISubscription.cs rename to src/extensions/BootstrapBlazor.OpcDa/IOpcSubscription.cs index 9ba1241e..db2c8535 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/ISubscription.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/IOpcSubscription.cs @@ -7,8 +7,13 @@ namespace BootstrapBlazor.OpcDa; /// /// 订阅接口定义 /// -public interface ISubscription +public interface IOpcSubscription { + /// + /// 获得 订阅名称 + /// + public string Name { get; } + /// /// 获得/设置 是否保留最后一个值 /// @@ -19,12 +24,6 @@ public interface ISubscription /// Action>? DataChanged { get; set; } - /// - /// 获得 实例 - /// - /// - Opc.Da.ISubscription GetSubscription(); - /// /// 增加数据项 /// diff --git a/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs b/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs new file mode 100644 index 00000000..1af8d610 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs @@ -0,0 +1,68 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.OpcDa; + +/// +/// 模拟 OpcDa Server 实现类 +/// +sealed class MockOpcDaServer : IOpcDaServer +{ + public bool IsConnected { get; set; } + + public string? ServerName { get; set; } + + private readonly Dictionary _subscriptions = []; + + public bool Connect(string serverName) + { + ServerName = serverName; + IsConnected = true; + return true; + } + + public void Disconnect() + { + IsConnected = false; + ServerName = null; + } + + public IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true) + { + if (_subscriptions.TryGetValue(name, out var subscription)) + { + CancelSubscription(subscription); + } + + subscription = new MockOpcDaSubscription(name, updateRate, active); + _subscriptions.Add(name, subscription); + return subscription; + } + + public void CancelSubscription(IOpcSubscription subscription) + { + _subscriptions.Remove(subscription.Name); + if (subscription is IDisposable disposable) + { + disposable.Dispose(); + } + } + + public HashSet Read(params HashSet items) + { + return items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000))) + .ToHashSet(OpcItemEqualityComparer.Default); + } + + public HashSet Write(params HashSet items) + { + return items.Select(i => new OpcWriteItem(i.Name, i.Value) { Result = true }) + .ToHashSet(OpcItemEqualityComparer.Default); + } + + public void Dispose() + { + + } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcSubscription.cs b/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcSubscription.cs new file mode 100644 index 00000000..a2959cbc --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcSubscription.cs @@ -0,0 +1,83 @@ +// Copyright (c) Argo Zhang (argo@163.com). All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. +// Website: https://www.blazor.zone or https://argozhang.github.io/ + +namespace BootstrapBlazor.OpcDa; + +sealed class MockOpcDaSubscription : IOpcSubscription, IDisposable +{ + private readonly int _updateRate; + private readonly bool _active; + private readonly List _items = []; + private CancellationTokenSource? _cts; + + public MockOpcDaSubscription(string name, int updateRate = 1000, bool active = true) + { + Name = name; + _updateRate = updateRate; + _active = active; + + _cts = new CancellationTokenSource(); + _ = Task.Run(() => DoTask(_cts.Token)); + } + + public string Name { get; } + + public bool KeepLastValue { get; set; } + + public Action>? DataChanged { get; set; } + + public void AddItems(IEnumerable items) + { + _items.AddRange(items); + } + + private void UpdateValues() + { + if (DataChanged != null) + { + var values = _items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000))).ToList(); + DataChanged.Invoke(values); + } + } + + private async Task DoTask(CancellationToken token) + { + do + { + try + { + if (_active) + { + UpdateValues(); + } + + await Task.Delay(_updateRate, token); + } + catch (OperationCanceledException) + { + // ignored + } + } + while (!token.IsCancellationRequested); + } + + private void Dispose(bool disposing) + { + if (disposing) + { + if (_cts != null) + { + _cts.Cancel(); + _cts.Dispose(); + _cts = null; + } + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcServer.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs similarity index 78% rename from src/extensions/BootstrapBlazor.OpcDa/OpcServer.cs rename to src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs index b463ccf3..a1cc46e5 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/OpcServer.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs @@ -13,7 +13,7 @@ namespace BootstrapBlazor.OpcDa; /// OPC Server 操作类 /// [SupportedOSPlatform("windows")] -class OpcServer : IOpcServer +sealed class OpcDaServer : IOpcDaServer { private Opc.Da.Server? _server = null; private readonly ConcurrentDictionary> _valuesCache = []; @@ -28,6 +28,8 @@ class OpcServer : IOpcServer /// public bool IsConnected => _server?.IsConnected ?? false; + private readonly Dictionary _subscriptions = []; + /// /// 连接到 OPCServer 方法 /// @@ -54,12 +56,13 @@ public void Disconnect() { ServerName = string.Empty; - if (_server != null && _server.IsConnected) + if (_server is { IsConnected: true }) { foreach (Subscription sub in _server.Subscriptions) { _server.CancelSubscription(sub); } + _server.Disconnect(); _server = null; } @@ -72,28 +75,33 @@ public void Disconnect() /// 更新频率 默认 1000 毫秒 /// 是否激活 默认 true /// - public ISubscription CreateSubscription(string name, int updateRate = 1000, bool active = true) + public IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true) { var server = GetOpcServer(); - var subscription = server.CreateSubscription(new SubscriptionState + if (_subscriptions.TryGetValue(name, out var subscription)) { - Name = name, - Deadband = 0, - UpdateRate = updateRate, - Active = active - }); + // 已经存在该订阅 + server.CancelSubscription(subscription); + } + + subscription = server.CreateSubscription(name, updateRate, active); + _subscriptions.Add(name, subscription); return subscription.ToOpcSubscription(); } /// /// 取消订阅方法 /// - /// 订阅接口 实例 + /// 订阅接口 实例 /// - public void CancelSubscription(ISubscription subscription) + public void CancelSubscription(IOpcSubscription subscription) { var server = GetOpcServer(); - server.CancelSubscription(subscription.GetSubscription()); + var name = subscription.Name; + if (_subscriptions.Remove(name, out var sub)) + { + server.CancelSubscription(sub); + } } /// @@ -121,16 +129,17 @@ public HashSet Write(params HashSet items) return items.Select(i => { var item = results.FirstOrDefault(v => v.ItemName == i.Name); - return new OpcWriteItem(i.Name, i.Value) { Result = item != null && item.ResultID == ResultID.S_OK }; + return i with { Result = item != null && item.ResultID == ResultID.S_OK }; }).ToHashSet(OpcItemEqualityComparer.Default); } private Opc.Da.Server GetOpcServer() { - if (_server == null) + if (_server is not { IsConnected: true }) { throw new InvalidOperationException("OPC Server is not connected."); } + return _server; } @@ -138,7 +147,7 @@ private Opc.Da.Server GetOpcServer() /// Dispose 方法 /// /// - protected virtual void Dispose(bool disposing) + private void Dispose(bool disposing) { if (disposing) { diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcSubscription.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcSubscription.cs index 3c7e222e..46822c9b 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/OpcSubscription.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcSubscription.cs @@ -4,24 +4,25 @@ namespace BootstrapBlazor.OpcDa; -class OpcSubscription(Opc.Da.ISubscription subscription) : ISubscription +sealed class OpcSubscription(Opc.Da.ISubscription subscription) : IOpcSubscription { public Action>? DataChanged { get; set; } public bool KeepLastValue { get; set; } + public string Name => subscription.GetState().Name; + public Opc.Da.ISubscription GetSubscription() => subscription; private readonly List _cache = []; public void AddItems(IEnumerable items) { - var subscription = GetSubscription(); subscription.AddItems([.. items.Select(i => new Opc.Da.Item { ItemName = i })]); subscription.DataChanged += (_, _, values) => { - var items = values.Select(i => + var valueList = values.Select(i => { var item = new OpcReadItem() { @@ -32,16 +33,16 @@ public void AddItems(IEnumerable items) }; if (KeepLastValue) { - var v = _cache.Find(i => i.Name == item.Name); + var v = _cache.Find(opcItem => opcItem.Name == item.Name); item.LastValue = v.Value; } return item; }).ToList(); _cache.Clear(); - _cache.AddRange(items); + _cache.AddRange(valueList); - DataChanged?.Invoke(items); + DataChanged?.Invoke(valueList); }; } } diff --git a/test/UnitTestOpcDa/UnitTest1.cs b/test/UnitTestOpcDa/UnitTest1.cs index 27911ec6..1e3e5580 100644 --- a/test/UnitTestOpcDa/UnitTest1.cs +++ b/test/UnitTestOpcDa/UnitTest1.cs @@ -15,10 +15,10 @@ public class UnitTest1 public void Write_Ok() { var sc = new ServiceCollection(); - sc.AddOpcServer(); + sc.AddOpcDaServer(); var sp = sc.BuildServiceProvider(); - var server = sp.GetRequiredService(); + var server = sp.GetRequiredService(); var ret = server.Connect("opcda://localhost/Kepware.KEPServerEX.V6"); Assert.True(ret); Assert.True(server.IsConnected); @@ -46,10 +46,10 @@ public void Write_Ok() public async Task Subscription_Ok() { var sc = new ServiceCollection(); - sc.AddOpcServer(); + sc.AddOpcDaServer(); var sp = sc.BuildServiceProvider(); - var server = sp.GetRequiredService(); + var server = sp.GetRequiredService(); server.Connect("opcda://localhost/Kepware.KEPServerEX.V6"); var subscription = server.CreateSubscription("Test", 100);