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
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+