diff --git a/BootstrapBlazor.Extensions.sln b/BootstrapBlazor.Extensions.sln index 397b304b..1f62ca72 100644 --- a/BootstrapBlazor.Extensions.sln +++ b/BootstrapBlazor.Extensions.sln @@ -192,6 +192,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestSvgIcon", "test\Uni EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.FluentSystemIcon", "src\components\BootstrapBlazor.FluentSystemIcon\BootstrapBlazor.FluentSystemIcon.csproj", "{30449D30-0B4E-40FD-85BE-C9BAAC820162}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.OpcUa", "src\components\BootstrapBlazor.OpcUa\BootstrapBlazor.OpcUa.csproj", "{6114A9DF-9DF5-474E-A5B0-25CDF0268B52}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestOpcUa", "test\UnitTestOpcUa\UnitTestOpcUa.csproj", "{98373A64-E224-4715-AE02-A8C6DAFF3338}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "BootstrapBlazor.OpcDa", "src\extensions\BootstrapBlazor.OpcDa\BootstrapBlazor.OpcDa.csproj", "{01007B10-7C3C-4136-83FF-981CA39AD3D4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTestOpcDa", "test\UnitTestOpcDa\UnitTestOpcDa.csproj", "{835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -522,6 +530,22 @@ Global {30449D30-0B4E-40FD-85BE-C9BAAC820162}.Debug|Any CPU.Build.0 = Debug|Any CPU {30449D30-0B4E-40FD-85BE-C9BAAC820162}.Release|Any CPU.ActiveCfg = Release|Any CPU {30449D30-0B4E-40FD-85BE-C9BAAC820162}.Release|Any CPU.Build.0 = Release|Any CPU + {6114A9DF-9DF5-474E-A5B0-25CDF0268B52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6114A9DF-9DF5-474E-A5B0-25CDF0268B52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6114A9DF-9DF5-474E-A5B0-25CDF0268B52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6114A9DF-9DF5-474E-A5B0-25CDF0268B52}.Release|Any CPU.Build.0 = Release|Any CPU + {98373A64-E224-4715-AE02-A8C6DAFF3338}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {98373A64-E224-4715-AE02-A8C6DAFF3338}.Debug|Any CPU.Build.0 = Debug|Any CPU + {98373A64-E224-4715-AE02-A8C6DAFF3338}.Release|Any CPU.ActiveCfg = Release|Any CPU + {98373A64-E224-4715-AE02-A8C6DAFF3338}.Release|Any CPU.Build.0 = Release|Any CPU + {01007B10-7C3C-4136-83FF-981CA39AD3D4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {01007B10-7C3C-4136-83FF-981CA39AD3D4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {01007B10-7C3C-4136-83FF-981CA39AD3D4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {01007B10-7C3C-4136-83FF-981CA39AD3D4}.Release|Any CPU.Build.0 = Release|Any CPU + {835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}.Debug|Any CPU.Build.0 = Debug|Any CPU + {835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}.Release|Any CPU.ActiveCfg = Release|Any CPU + {835C8BA9-A9CC-4EA0-9002-34A20F8B2E86}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -612,6 +636,10 @@ Global {10D35EE5-FA31-4C80-B113-CD7A0FB76B4E} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} {7CAD5915-CE3E-31ED-B1AC-15C61C3ED8C3} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} {30449D30-0B4E-40FD-85BE-C9BAAC820162} = {FF1089BE-C704-4374-B629-C57C08E1798F} + {6114A9DF-9DF5-474E-A5B0-25CDF0268B52} = {FF1089BE-C704-4374-B629-C57C08E1798F} + {98373A64-E224-4715-AE02-A8C6DAFF3338} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} + {01007B10-7C3C-4136-83FF-981CA39AD3D4} = {7B29E81D-92DE-46C8-8EDC-1B48C8F12BC2} + {835C8BA9-A9CC-4EA0-9002-34A20F8B2E86} = {B6A98ADE-D26A-4D0B-8978-AB7AC915F5AE} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D5EB1960-6F30-4CE1-B375-EAE1F787D6FF} diff --git a/src/components/BootstrapBlazor.OpcUa/BootstrapBlazor.OpcUa.csproj b/src/components/BootstrapBlazor.OpcUa/BootstrapBlazor.OpcUa.csproj new file mode 100644 index 00000000..2d083627 --- /dev/null +++ b/src/components/BootstrapBlazor.OpcUa/BootstrapBlazor.OpcUa.csproj @@ -0,0 +1,21 @@ + + + + 9.0.1 + + + + Bootstrap Blazor WebAssembly wasm UI Components Opc Ua Client + Bootstrap UI components extensions of OpcUa + + + + + + + + + + + + diff --git a/src/components/BootstrapBlazor.OpcUa/Extensions/ServiceCollectionExtensions.cs b/src/components/BootstrapBlazor.OpcUa/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..48acecf7 --- /dev/null +++ b/src/components/BootstrapBlazor.OpcUa/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,21 @@ +// 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 Microsoft.Extensions.DependencyInjection; + +/// +/// OpcUa 服务扩展类 +/// +public static class ServiceCollectionExtensions +{ + /// + /// 增加 OpcUa 数据服务 + /// + /// + /// + public static IServiceCollection AddBootstrapBlazorOpcUaService(this IServiceCollection services) + { + return services; + } +} diff --git a/src/components/BootstrapBlazor.OpcUa/_Imports.razor b/src/components/BootstrapBlazor.OpcUa/_Imports.razor new file mode 100644 index 00000000..77285129 --- /dev/null +++ b/src/components/BootstrapBlazor.OpcUa/_Imports.razor @@ -0,0 +1 @@ +@using Microsoft.AspNetCore.Components.Web diff --git a/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj b/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj new file mode 100644 index 00000000..a490932d --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj @@ -0,0 +1,51 @@ + + + + 9.0.0-beta01 + BootstrapBlazor.OpcDa + + + + Bootstrap Blazor WebAssembly wasm UI Components SqlSugar + Bootstrap UI components extensions of SqlSugar + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs b/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs new file mode 100644 index 00000000..ccc0e604 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs @@ -0,0 +1,23 @@ +// 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; + +/// +/// 扩展方法类 +/// +internal static class Extensions +{ + public static Quality ToQuality(this Opc.Da.Quality quality) + { + return quality.QualityBits == Opc.Da.qualityBits.good + ? Quality.Good + : Quality.Bad; + } + + public static ISubscription ToOpcSubscription(this Opc.Da.ISubscription subscription) + { + return new OpcSubscription(subscription); + } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/Extensions/ServiceCollectionExtensions.cs b/src/extensions/BootstrapBlazor.OpcDa/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 00000000..bf5db6fb --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,26 @@ +// 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/ + +using BootstrapBlazor.OpcDa; +using System.Runtime.Versioning; + +namespace Microsoft.Extensions.DependencyInjection; + +/// +/// Opc Da 服务扩展类 +/// +public static class ServiceCollectionExtensions +{ + /// + /// 增加 Opc 操作服务 + /// + /// + /// + [SupportedOSPlatform("windows")] + public static IServiceCollection AddOpcServer(this IServiceCollection services) + { + services.AddSingleton(); + return services; + } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/IOpcItem.cs b/src/extensions/BootstrapBlazor.OpcDa/IOpcItem.cs new file mode 100644 index 00000000..1127e983 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/IOpcItem.cs @@ -0,0 +1,16 @@ +// 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; + +/// +/// +/// +public interface IOpcItem +{ + /// + /// + /// + string Name { get; } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/IOpcServer.cs b/src/extensions/BootstrapBlazor.OpcDa/IOpcServer.cs new file mode 100644 index 00000000..344fcbd0 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/IOpcServer.cs @@ -0,0 +1,62 @@ +// 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; + +/// +/// Opc Server 接口定义 +/// +public interface IOpcServer : IDisposable +{ + /// + /// 获得 OPC Server 是否已连接 + /// + bool IsConnected { get; } + + /// + /// 获得 OPC Server 名称 + /// + string? ServerName { get; } + + /// + /// 连接到 OPC Server 方法 + /// + /// + /// + bool Connect(string serverName); + + /// + /// 断开连接方法 + /// + void Disconnect(); + + /// + /// 取消订阅方法 + /// + /// + void CancelSubscription(ISubscription subscription); + + /// + /// 创建订阅方法 + /// + /// 订阅名称 + /// 更新频率 默认 1000 毫秒 + /// 是否激活 默认 true + /// + ISubscription CreateSubscription(string name, int updateRate = 1000, bool active = true); + + /// + /// 读取 Item 值方法 + /// + /// + /// + HashSet Read(params HashSet items); + + /// + /// 读取 Item 值方法 + /// + /// + /// + HashSet Write(params HashSet items); +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/ISubscription.cs b/src/extensions/BootstrapBlazor.OpcDa/ISubscription.cs new file mode 100644 index 00000000..9ba1241e --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/ISubscription.cs @@ -0,0 +1,33 @@ +// 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; + +/// +/// 订阅接口定义 +/// +public interface ISubscription +{ + /// + /// 获得/设置 是否保留最后一个值 + /// + public bool KeepLastValue { get; set; } + + /// + /// 获得/设置 数据变更回调 + /// + Action>? DataChanged { get; set; } + + /// + /// 获得 实例 + /// + /// + Opc.Da.ISubscription GetSubscription(); + + /// + /// 增加数据项 + /// + /// + void AddItems(IEnumerable items); +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcItemEqualityComparer.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcItemEqualityComparer.cs new file mode 100644 index 00000000..5a961ba0 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcItemEqualityComparer.cs @@ -0,0 +1,31 @@ +// 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; + +/// +/// 比较器 +/// +public class OpcItemEqualityComparer : IEqualityComparer where TItem : IOpcItem +{ + /// + /// 获得 实例 + /// + public static OpcItemEqualityComparer Default { get; } = new(); + + /// + /// + /// + /// + /// + /// + public bool Equals(TItem? x, TItem? y) => x?.Name == y?.Name; + + /// + /// + /// + /// + /// + public int GetHashCode([DisallowNull] TItem item) => item.Name.GetHashCode(); +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcReadItem.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcReadItem.cs new file mode 100644 index 00000000..adcee8af --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcReadItem.cs @@ -0,0 +1,16 @@ +// 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; + +/// +/// OPC Item 读取实体类 +/// +public record struct OpcReadItem(string Name, Quality Quality, DateTime Timestamp, object? Value) : IOpcItem +{ + /// + /// 获得 Opc Item 上次值 + /// + public object? LastValue { get; set; } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcServer.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcServer.cs new file mode 100644 index 00000000..b463ccf3 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcServer.cs @@ -0,0 +1,157 @@ +// 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/ + +using Opc; +using Opc.Da; +using System.Collections.Concurrent; +using System.Runtime.Versioning; + +namespace BootstrapBlazor.OpcDa; + +/// +/// OPC Server 操作类 +/// +[SupportedOSPlatform("windows")] +class OpcServer : IOpcServer +{ + private Opc.Da.Server? _server = null; + private readonly ConcurrentDictionary> _valuesCache = []; + + /// + /// 获得 OPC Server 名称 + /// + public string? ServerName { get; private set; } + + /// + /// 获得 OPC Server 状态 + /// + public bool IsConnected => _server?.IsConnected ?? false; + + /// + /// 连接到 OPCServer 方法 + /// + /// 服务器名称 + /// opcda://localhost/Kepware.KEPServerEX.V6 + /// 成功时返回真 + public bool Connect(string serverName) + { + ServerName = serverName; + + // 如果已经连接则先断开 + Disconnect(); + + _server = new Opc.Da.Server(new OpcCom.Factory(), new URL(serverName)); + _server.Connect(); + return IsConnected; + } + + /// + /// 断开连接方法 + /// + /// + public void Disconnect() + { + ServerName = string.Empty; + + if (_server != null && _server.IsConnected) + { + foreach (Subscription sub in _server.Subscriptions) + { + _server.CancelSubscription(sub); + } + _server.Disconnect(); + _server = null; + } + } + + /// + /// 创建订阅方法 + /// + /// 订阅名称 + /// 更新频率 默认 1000 毫秒 + /// 是否激活 默认 true + /// + public ISubscription CreateSubscription(string name, int updateRate = 1000, bool active = true) + { + var server = GetOpcServer(); + var subscription = server.CreateSubscription(new SubscriptionState + { + Name = name, + Deadband = 0, + UpdateRate = updateRate, + Active = active + }); + return subscription.ToOpcSubscription(); + } + + /// + /// 取消订阅方法 + /// + /// 订阅接口 实例 + /// + public void CancelSubscription(ISubscription subscription) + { + var server = GetOpcServer(); + server.CancelSubscription(subscription.GetSubscription()); + } + + /// + /// 读取指定 Item 值方法 + /// + /// + /// + public HashSet Read(params HashSet items) + { + var server = GetOpcServer(); + var results = server.Read([.. items.Select(i => new Item() { ItemName = i })]); + return results.Select(i => new OpcReadItem(i.ItemName, i.Quality.ToQuality(), i.Timestamp, i.Value)).ToHashSet(OpcItemEqualityComparer.Default); + } + + /// + /// 读取指定 Item 值方法 + /// + /// + /// + public HashSet Write(params HashSet items) + { + var server = GetOpcServer(); + var results = server.Write([.. items.Select(i => new ItemValue() { ItemName = i.Name, Value = i.Value })]); + + 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 }; + }).ToHashSet(OpcItemEqualityComparer.Default); + } + + private Opc.Da.Server GetOpcServer() + { + if (_server == null) + { + throw new InvalidOperationException("OPC Server is not connected."); + } + return _server; + } + + /// + /// Dispose 方法 + /// + /// + protected virtual void Dispose(bool disposing) + { + if (disposing) + { + Disconnect(); + } + } + + /// + /// + /// + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcSubscription.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcSubscription.cs new file mode 100644 index 00000000..3c7e222e --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcSubscription.cs @@ -0,0 +1,47 @@ +// 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; + +class OpcSubscription(Opc.Da.ISubscription subscription) : ISubscription +{ + public Action>? DataChanged { get; set; } + + public bool KeepLastValue { get; set; } + + 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 item = new OpcReadItem() + { + Name = i.ItemName, + Value = i.Value, + Quality = i.Quality == Opc.Da.Quality.Good ? Quality.Good : Quality.Bad, + Timestamp = i.Timestamp + }; + if (KeepLastValue) + { + var v = _cache.Find(i => i.Name == item.Name); + item.LastValue = v.Value; + } + return item; + }).ToList(); + + _cache.Clear(); + _cache.AddRange(items); + + DataChanged?.Invoke(items); + }; + } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcWriteItem.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcWriteItem.cs new file mode 100644 index 00000000..b55d2c77 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcWriteItem.cs @@ -0,0 +1,16 @@ +// 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; + +/// +/// OPC Item 写入实体类 +/// +public record struct OpcWriteItem(string Name, object? Value) : IOpcItem +{ + /// + /// 获得/设置 写入结果 + /// + public bool Result { get; set; } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/Quality.cs b/src/extensions/BootstrapBlazor.OpcDa/Quality.cs new file mode 100644 index 00000000..816e2b7e --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/Quality.cs @@ -0,0 +1,21 @@ +// 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; + +/// +/// Opc Item Quality 枚举 +/// +public enum Quality +{ + /// + /// 不可信 + /// + Bad, + + /// + /// 可信 + /// + Good +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/build/BootstrapBlazor.OpcDa.targets b/src/extensions/BootstrapBlazor.OpcDa/build/BootstrapBlazor.OpcDa.targets new file mode 100644 index 00000000..af3fd9c1 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/build/BootstrapBlazor.OpcDa.targets @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/src/extensions/BootstrapBlazor.OpcDa/lib/OpcComRcw.dll b/src/extensions/BootstrapBlazor.OpcDa/lib/OpcComRcw.dll new file mode 100644 index 00000000..c2daeb01 Binary files /dev/null and b/src/extensions/BootstrapBlazor.OpcDa/lib/OpcComRcw.dll differ diff --git a/src/extensions/BootstrapBlazor.OpcDa/lib/OpcNetApi.Com.dll b/src/extensions/BootstrapBlazor.OpcDa/lib/OpcNetApi.Com.dll new file mode 100644 index 00000000..6ae8a1ac Binary files /dev/null and b/src/extensions/BootstrapBlazor.OpcDa/lib/OpcNetApi.Com.dll differ diff --git a/src/extensions/BootstrapBlazor.OpcDa/lib/OpcNetApi.dll b/src/extensions/BootstrapBlazor.OpcDa/lib/OpcNetApi.dll new file mode 100644 index 00000000..ec681bf1 Binary files /dev/null and b/src/extensions/BootstrapBlazor.OpcDa/lib/OpcNetApi.dll differ diff --git a/test/UnitTestOpcDa/UnitTest1.cs b/test/UnitTestOpcDa/UnitTest1.cs new file mode 100644 index 00000000..27911ec6 --- /dev/null +++ b/test/UnitTestOpcDa/UnitTest1.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 UnitTestOpcDa; + +using BootstrapBlazor.OpcDa; +using Microsoft.Extensions.DependencyInjection; +using System.Runtime.Versioning; + +[SupportedOSPlatform("windows")] +public class UnitTest1 +{ + [Fact] + public void Write_Ok() + { + var sc = new ServiceCollection(); + sc.AddOpcServer(); + + var sp = sc.BuildServiceProvider(); + var server = sp.GetRequiredService(); + var ret = server.Connect("opcda://localhost/Kepware.KEPServerEX.V6"); + Assert.True(ret); + Assert.True(server.IsConnected); + + var values = server.Read("Simulation Examples.Functions.Ramp1", "Simulation Examples.Functions.Ramp2"); + Assert.Equal(2, values.Count); + Assert.All(values, v => Assert.Equal(Quality.Good, v.Quality)); + + var results = server.Write([ + new OpcWriteItem() + { + Name = "Channel1.Device1.Tag2", + Value = 123 + } + ]); + Assert.All(results, v => Assert.True(v.Result)); + + server.Disconnect(); + Assert.False(server.IsConnected); + + server.Dispose(); + } + + [Fact] + public async Task Subscription_Ok() + { + var sc = new ServiceCollection(); + sc.AddOpcServer(); + + var sp = sc.BuildServiceProvider(); + var server = sp.GetRequiredService(); + server.Connect("opcda://localhost/Kepware.KEPServerEX.V6"); + + var subscription = server.CreateSubscription("Test", 100); + subscription.AddItems( + [ + "Channel1.Device1.Tag1", + "Channel1.Device1.Tag2" + ]); + + var tcs = new TaskCompletionSource(); + var values = new List(); + subscription.KeepLastValue = true; + subscription.DataChanged = items => + { + values.Clear(); + values.AddRange(items); + tcs.TrySetResult(); + }; + + await tcs.Task; + Assert.Equal(2, values.Count); + + await Task.Delay(150); + Assert.Single(values); + + server.CancelSubscription(subscription); + + server.Disconnect(); + server.Dispose(); + } +} diff --git a/test/UnitTestOpcDa/UnitTestOpcDa.csproj b/test/UnitTestOpcDa/UnitTestOpcDa.csproj new file mode 100644 index 00000000..3eea723d --- /dev/null +++ b/test/UnitTestOpcDa/UnitTestOpcDa.csproj @@ -0,0 +1,31 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/UnitTestOpcUa/UnitTest1.cs b/test/UnitTestOpcUa/UnitTest1.cs new file mode 100644 index 00000000..3ae5feb3 --- /dev/null +++ b/test/UnitTestOpcUa/UnitTest1.cs @@ -0,0 +1,113 @@ +// 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/ + +using Microsoft.Extensions.Configuration; +using Opc.Ua; +using Opc.Ua.Client; +using Opc.Ua.Configuration; + +namespace UnitTestOpcUa; + +public class UnitTest1 +{ + [Fact] + public async Task Connect_Ok() + { + var config = new ApplicationConfiguration() + { + ApplicationName = "KEPServerEX Client", + ApplicationType = ApplicationType.Client, + ApplicationUri = "urn:" + Utils.GetHostName() + ":KEPServerEXClient", + SecurityConfiguration = new SecurityConfiguration + { + ApplicationCertificate = new CertificateIdentifier { StoreType = "X509Store", StorePath = "CurrentUser\\My" }, + TrustedPeerCertificates = new CertificateTrustList { StoreType = "Directory", StorePath = "OPC Foundation/CertificateStores/UA Applications" }, + RejectedCertificateStore = new CertificateTrustList { StoreType = "Directory", StorePath = "OPC Foundation/CertificateStores/RejectedCertificates" }, + AutoAcceptUntrustedCertificates = true // 仅用于测试环境 + }, + TransportConfigurations = new TransportConfigurationCollection(), + TransportQuotas = new TransportQuotas { OperationTimeout = 15000 }, + ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 } + }; + + //await config.Validate(ApplicationType.Client); + + // 创建端点描述 + var endpointDescription = CoreClientUtils.SelectEndpoint("opc.tcp://127.0.0.1:49320", false); + var endpointConfiguration = EndpointConfiguration.Create(config); + var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); + + // 创建会话 + var identity = new UserIdentity("BB", "123456@163.com"); // 匿名登录,或提供用户名密码 + var session = await Session.Create( + config, + endpoint, + false, + false, + config.ApplicationName, + 60000, + identity, + null); + } + + [Fact] + public async Task FindServersAsync() + { + // 配置与连接过程同前述基本客户端 + var application = new ApplicationInstance + { + ApplicationName = "OPC UA Basic Client", + ApplicationType = ApplicationType.Client + }; + var applicationConfiguration = new ApplicationConfiguration + { + ApplicationName = application.ApplicationName, + ApplicationType = application.ApplicationType, + ClientConfiguration = new ClientConfiguration() + }; + var endpointURL = "opc.tcp://127.0.0.1:49320"; + var endpointDescription = CoreClientUtils.SelectEndpoint(applicationConfiguration, endpointURL, false); + var endpointConfiguration = EndpointConfiguration.Create(); + var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration); + + var session = await Session.Create( + configuration: applicationConfiguration, + endpoint: endpoint, + updateBeforeConnect: false, + sessionName: "Opc.Session.BootstrapBlazor", + sessionTimeout: 60000, + identity: null, + preferredLocales: null); + + // Browser + var browser = new Browser(session) + { + BrowseDirection = BrowseDirection.Forward, + NodeClassMask = (int)NodeClass.Variable | (int)NodeClass.Object | (int)NodeClass.Method, + ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, + IncludeSubtypes = true, + MaxReferencesReturned = 1000 + }; + + // 浏览节点 + var references = browser.Browse(ObjectIds.ObjectsFolder); + + var readValueId = new ReadValueId + { + NodeId = new NodeId("ns=2;s=Simulation Examples.Functions.Ramp1"), + AttributeId = Attributes.Value + }; + + var readValues = new ReadValueIdCollection { readValueId }; + + // 读取节点值 + var resp = await session.ReadAsync( + null, + 0, + TimestampsToReturn.Both, + readValues, CancellationToken.None); + + await session.CloseAsync(); + } +} diff --git a/test/UnitTestOpcUa/UnitTestOpcUa.csproj b/test/UnitTestOpcUa/UnitTestOpcUa.csproj new file mode 100644 index 00000000..bcd56af9 --- /dev/null +++ b/test/UnitTestOpcUa/UnitTestOpcUa.csproj @@ -0,0 +1,25 @@ + + + + net9.0 + enable + enable + false + + + + + + + + + + + + + + + + + +