diff --git a/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj b/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj index 6b2d13dc..66f7b903 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj +++ b/src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj @@ -1,7 +1,7 @@  - 9.0.0 + 9.0.1 BootstrapBlazor.OpcDa diff --git a/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs b/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs index 1e29ec9a..df6a4425 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs @@ -24,4 +24,26 @@ public static IOpcSubscription ToOpcSubscription(this ISubscription 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 }); + + public static BrowseFilters ToFilters(this OpcBrowseFilters filters) + { + return new BrowseFilters + { + ReturnAllProperties = filters.ReturnAllProperties, + ReturnPropertyValues = filters.ReturnPropertyValues, + MaxElementsReturned = filters.MaxElementsReturned, + ElementNameFilter = filters.ElementNameFilter, + BrowseFilter = filters.BrowseFilter.ToBrowseFilter() + }; + } + + public static browseFilter ToBrowseFilter(this OpcBrowseFilterType filterType) + { + return filterType switch + { + OpcBrowseFilterType.All => browseFilter.all, + OpcBrowseFilterType.Branch => browseFilter.branch, + _ => browseFilter.item + }; + } } diff --git a/src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.cs b/src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.cs index 26f015b0..79fdb5d6 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.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; /// @@ -59,4 +61,20 @@ public interface IOpcDaServer : IDisposable /// /// HashSet Write(params HashSet items); + + /// + /// 浏览 OPC Server 中的位号 (即数据项或者标签) + /// + /// + /// + /// + /// + OpcBrowseElement[] Browse(string name, OpcBrowseFilters filters, out OpcBrowsePosition? position); + + /// + /// 浏览 OPC Server 中的位号 (即数据项或者标签) + /// + /// + /// + OpcBrowseElement[] BrowseNext(OpcBrowsePosition position); } diff --git a/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs b/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs index 1af8d610..04e7df62 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs @@ -2,6 +2,9 @@ // 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; +using System.Net.Http.Headers; + namespace BootstrapBlazor.OpcDa; /// @@ -61,6 +64,82 @@ public HashSet Write(params HashSet items) .ToHashSet(OpcItemEqualityComparer.Default); } + /// + /// 浏览 OPC Server 中的位号 (即数据项或者标签) + /// + /// + /// + /// + /// + public OpcBrowseElement[] Browse(string name, OpcBrowseFilters filters, out OpcBrowsePosition? position) + { + position = null; + if (string.IsNullOrEmpty(name)) + { + return [ + new OpcBrowseElement() + { + Name ="Channel1", + ItemName = "Channel1", + IsItem = false, + HasChildren = true + }, + new OpcBrowseElement() + { + Name ="Channel2", + ItemName = "Channel2", + IsItem = false, + HasChildren = true + } + ]; + } + + if (name == "Channel1") + { + return [ + new OpcBrowseElement() + { + Name ="Device1", + ItemName = "Channel1.Device1", + IsItem = false, + HasChildren = true + } + ]; + } + + if (name == "Channel1.Device1") + { + return [ + new OpcBrowseElement() + { + Name ="Tag1", + ItemName = "Channel1.Device1.Tag1", + IsItem = true, + HasChildren = false + }, + new OpcBrowseElement() + { + Name ="Tag2", + ItemName = "Channel1.Device1.Tag2", + IsItem = true, + HasChildren = false + } + ]; + } + + return []; + } + + /// + /// 浏览 OPC Server 中的位号 (即数据项或者标签) + /// + /// + /// + public OpcBrowseElement[] BrowseNext(OpcBrowsePosition position) + { + return []; + } + public void Dispose() { diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseElement.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseElement.cs new file mode 100644 index 00000000..02cad814 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseElement.cs @@ -0,0 +1,50 @@ +// 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.Da; + +namespace BootstrapBlazor.OpcDa; + +/// +/// 对 OpcDataServer BrowseElement 的封装类 +/// +public class OpcBrowseElement +{ + /// + /// 获得/设置 节点名称 + /// + public string Name { get; set; } + + /// + /// 获得/设置 Item 名称 + /// + public string ItemName { get; set; } + + /// + /// 获得/设置 是否是数据项 + /// + public bool IsItem { get; set; } + + /// + /// 获得/设置 是否有子节点 + /// + public bool HasChildren { get; set; } + + /// + /// 构造函数 + /// + public OpcBrowseElement() + { + Name = ""; + ItemName = ""; + } + + internal OpcBrowseElement(BrowseElement element) + { + Name = element.Name; + ItemName = element.ItemName; + IsItem = element.IsItem; + HasChildren = element.HasChildren; + } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseFilterType.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseFilterType.cs new file mode 100644 index 00000000..b41e28b2 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseFilterType.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/ + +namespace BootstrapBlazor.OpcDa; + +/// +/// OpcDa 浏览过滤器类型枚举 +/// +public enum OpcBrowseFilterType +{ + /// + /// 全部 + /// + All, + + /// + /// 分支 + /// + Branch, + + /// + /// 数据项 + /// + Item +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseFilters.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseFilters.cs new file mode 100644 index 00000000..165f9381 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcBrowseFilters.cs @@ -0,0 +1,36 @@ +// 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; + +/// +/// 对 OpcDataServer BrowseFilters 的封装类 +/// +public class OpcBrowseFilters +{ + /// + /// 获得/设置 最大返回节点数量 + /// + public int MaxElementsReturned { get; set; } + + /// + /// 获得/设置 元素名称过滤器 + /// + public string? ElementNameFilter { get; set; } + + /// + /// 获得/设置 是否返回所有属性 + /// + public bool ReturnAllProperties { get; set; } + + /// + /// 获得/设置 是否返回属性值 + /// + public bool ReturnPropertyValues { get; set; } + + /// + /// 获得/设置 浏览过滤器类型 + /// + public OpcBrowseFilterType BrowseFilter { get; set; } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcBrowsePosition.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcBrowsePosition.cs new file mode 100644 index 00000000..f2404848 --- /dev/null +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcBrowsePosition.cs @@ -0,0 +1,18 @@ +// 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; + +/// +/// 对 OpcDataServer BrowsePosition 的封装类 +/// +public class OpcBrowsePosition +{ + internal OpcBrowsePosition(Opc.Da.BrowsePosition? position) + { + Position = position; + } + + internal Opc.Da.BrowsePosition? Position { get; set; } +} diff --git a/src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs b/src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs index a1cc46e5..150e61b0 100644 --- a/src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs +++ b/src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs @@ -4,7 +4,6 @@ using Opc; using Opc.Da; -using System.Collections.Concurrent; using System.Runtime.Versioning; namespace BootstrapBlazor.OpcDa; @@ -16,7 +15,6 @@ namespace BootstrapBlazor.OpcDa; sealed class OpcDaServer : IOpcDaServer { private Opc.Da.Server? _server = null; - private readonly ConcurrentDictionary> _valuesCache = []; /// /// 获得 OPC Server 名称 @@ -143,6 +141,34 @@ private Opc.Da.Server GetOpcServer() return _server; } + /// + /// + /// + /// + /// + /// + /// + public OpcBrowseElement[] Browse(string name, OpcBrowseFilters filters, out OpcBrowsePosition? position) + { + var server = GetOpcServer(); + var results = server.Browse(new ItemIdentifier(name), filters.ToFilters(), out var pos) ?? []; + position = pos == null ? null : new OpcBrowsePosition(pos); + return [.. results.Select(element => new OpcBrowseElement(element))]; + } + + /// + /// + /// + /// + /// + public OpcBrowseElement[] BrowseNext(OpcBrowsePosition position) + { + var server = GetOpcServer(); + var pos = position.Position; + var results = server.BrowseNext(ref pos) ?? []; + return [.. results.Select(element => new OpcBrowseElement(element))]; + } + /// /// Dispose 方法 /// diff --git a/test/UnitTestOpcDa/UnitTest1.cs b/test/UnitTestOpcDa/UnitTest1.cs index 1e3e5580..45b98aa0 100644 --- a/test/UnitTestOpcDa/UnitTest1.cs +++ b/test/UnitTestOpcDa/UnitTest1.cs @@ -80,4 +80,19 @@ public async Task Subscription_Ok() server.Disconnect(); server.Dispose(); } + + [Fact] + public void Browser_Ok() + { + var sc = new ServiceCollection(); + sc.AddOpcDaServer(); + + var sp = sc.BuildServiceProvider(); + var server = sp.GetRequiredService(); + server.Connect("opcda://localhost/Kepware.KEPServerEX.V6"); + + var elements = server.Browse("Channel1", new OpcBrowseFilters(), out var position); + Assert.Equal(3, elements.Length); + Assert.Null(position); + } }