Skip to content

Commit 02bd40f

Browse files
authored
feat(OpcDa): add Browse feature (#520)
* feat: 增加浏览功能 * test: 增加单元测试 * feat: 增加浏览功能 * test: 更新单元测试 * refactor: 精简代码 * refactor: 完善 Browse 功能 * feat: 增加 OpcBrowseFilterType 枚举 * test: 增加单元测试 * chore: bump version 9.0.1
1 parent 851dad0 commit 02bd40f

10 files changed

Lines changed: 293 additions & 3 deletions

File tree

src/extensions/BootstrapBlazor.OpcDa/BootstrapBlazor.OpcDa.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<Version>9.0.0</Version>
4+
<Version>9.0.1</Version>
55
<RootNamespace>BootstrapBlazor.OpcDa</RootNamespace>
66
</PropertyGroup>
77

src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,26 @@ public static IOpcSubscription ToOpcSubscription(this ISubscription subscription
2424
}
2525

2626
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 });
27+
28+
public static BrowseFilters ToFilters(this OpcBrowseFilters filters)
29+
{
30+
return new BrowseFilters
31+
{
32+
ReturnAllProperties = filters.ReturnAllProperties,
33+
ReturnPropertyValues = filters.ReturnPropertyValues,
34+
MaxElementsReturned = filters.MaxElementsReturned,
35+
ElementNameFilter = filters.ElementNameFilter,
36+
BrowseFilter = filters.BrowseFilter.ToBrowseFilter()
37+
};
38+
}
39+
40+
public static browseFilter ToBrowseFilter(this OpcBrowseFilterType filterType)
41+
{
42+
return filterType switch
43+
{
44+
OpcBrowseFilterType.All => browseFilter.all,
45+
OpcBrowseFilterType.Branch => browseFilter.branch,
46+
_ => browseFilter.item
47+
};
48+
}
2749
}

src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
// Website: https://www.blazor.zone or https://argozhang.github.io/
44

5+
using Opc.Da;
6+
57
namespace BootstrapBlazor.OpcDa;
68

79
/// <summary>
@@ -59,4 +61,20 @@ public interface IOpcDaServer : IDisposable
5961
/// <param name="items"></param>
6062
/// <returns></returns>
6163
HashSet<OpcWriteItem> Write(params HashSet<OpcWriteItem> items);
64+
65+
/// <summary>
66+
/// 浏览 OPC Server 中的位号 (即数据项或者标签)
67+
/// </summary>
68+
/// <param name="name"></param>
69+
/// <param name="filters"></param>
70+
/// <param name="position"></param>
71+
/// <returns></returns>
72+
OpcBrowseElement[] Browse(string name, OpcBrowseFilters filters, out OpcBrowsePosition? position);
73+
74+
/// <summary>
75+
/// 浏览 OPC Server 中的位号 (即数据项或者标签)
76+
/// </summary>
77+
/// <param name="position"></param>
78+
/// <returns></returns>
79+
OpcBrowseElement[] BrowseNext(OpcBrowsePosition position);
6280
}

src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33
// Website: https://www.blazor.zone or https://argozhang.github.io/
44

5+
using Opc.Da;
6+
using System.Net.Http.Headers;
7+
58
namespace BootstrapBlazor.OpcDa;
69

710
/// <summary>
@@ -61,6 +64,82 @@ public HashSet<OpcWriteItem> Write(params HashSet<OpcWriteItem> items)
6164
.ToHashSet(OpcItemEqualityComparer<OpcWriteItem>.Default);
6265
}
6366

67+
/// <summary>
68+
/// 浏览 OPC Server 中的位号 (即数据项或者标签)
69+
/// </summary>
70+
/// <param name="name"></param>
71+
/// <param name="filters"></param>
72+
/// <param name="position"></param>
73+
/// <returns></returns>
74+
public OpcBrowseElement[] Browse(string name, OpcBrowseFilters filters, out OpcBrowsePosition? position)
75+
{
76+
position = null;
77+
if (string.IsNullOrEmpty(name))
78+
{
79+
return [
80+
new OpcBrowseElement()
81+
{
82+
Name ="Channel1",
83+
ItemName = "Channel1",
84+
IsItem = false,
85+
HasChildren = true
86+
},
87+
new OpcBrowseElement()
88+
{
89+
Name ="Channel2",
90+
ItemName = "Channel2",
91+
IsItem = false,
92+
HasChildren = true
93+
}
94+
];
95+
}
96+
97+
if (name == "Channel1")
98+
{
99+
return [
100+
new OpcBrowseElement()
101+
{
102+
Name ="Device1",
103+
ItemName = "Channel1.Device1",
104+
IsItem = false,
105+
HasChildren = true
106+
}
107+
];
108+
}
109+
110+
if (name == "Channel1.Device1")
111+
{
112+
return [
113+
new OpcBrowseElement()
114+
{
115+
Name ="Tag1",
116+
ItemName = "Channel1.Device1.Tag1",
117+
IsItem = true,
118+
HasChildren = false
119+
},
120+
new OpcBrowseElement()
121+
{
122+
Name ="Tag2",
123+
ItemName = "Channel1.Device1.Tag2",
124+
IsItem = true,
125+
HasChildren = false
126+
}
127+
];
128+
}
129+
130+
return [];
131+
}
132+
133+
/// <summary>
134+
/// 浏览 OPC Server 中的位号 (即数据项或者标签)
135+
/// </summary>
136+
/// <param name="position"></param>
137+
/// <returns></returns>
138+
public OpcBrowseElement[] BrowseNext(OpcBrowsePosition position)
139+
{
140+
return [];
141+
}
142+
64143
public void Dispose()
65144
{
66145

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Website: https://www.blazor.zone or https://argozhang.github.io/
4+
5+
using Opc.Da;
6+
7+
namespace BootstrapBlazor.OpcDa;
8+
9+
/// <summary>
10+
/// 对 OpcDataServer BrowseElement 的封装类
11+
/// </summary>
12+
public class OpcBrowseElement
13+
{
14+
/// <summary>
15+
/// 获得/设置 节点名称
16+
/// </summary>
17+
public string Name { get; set; }
18+
19+
/// <summary>
20+
/// 获得/设置 Item 名称
21+
/// </summary>
22+
public string ItemName { get; set; }
23+
24+
/// <summary>
25+
/// 获得/设置 是否是数据项
26+
/// </summary>
27+
public bool IsItem { get; set; }
28+
29+
/// <summary>
30+
/// 获得/设置 是否有子节点
31+
/// </summary>
32+
public bool HasChildren { get; set; }
33+
34+
/// <summary>
35+
/// 构造函数
36+
/// </summary>
37+
public OpcBrowseElement()
38+
{
39+
Name = "";
40+
ItemName = "";
41+
}
42+
43+
internal OpcBrowseElement(BrowseElement element)
44+
{
45+
Name = element.Name;
46+
ItemName = element.ItemName;
47+
IsItem = element.IsItem;
48+
HasChildren = element.HasChildren;
49+
}
50+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Website: https://www.blazor.zone or https://argozhang.github.io/
4+
5+
namespace BootstrapBlazor.OpcDa;
6+
7+
/// <summary>
8+
/// OpcDa 浏览过滤器类型枚举
9+
/// </summary>
10+
public enum OpcBrowseFilterType
11+
{
12+
/// <summary>
13+
/// 全部
14+
/// </summary>
15+
All,
16+
17+
/// <summary>
18+
/// 分支
19+
/// </summary>
20+
Branch,
21+
22+
/// <summary>
23+
/// 数据项
24+
/// </summary>
25+
Item
26+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Website: https://www.blazor.zone or https://argozhang.github.io/
4+
5+
namespace BootstrapBlazor.OpcDa;
6+
7+
/// <summary>
8+
/// 对 OpcDataServer BrowseFilters 的封装类
9+
/// </summary>
10+
public class OpcBrowseFilters
11+
{
12+
/// <summary>
13+
/// 获得/设置 最大返回节点数量
14+
/// </summary>
15+
public int MaxElementsReturned { get; set; }
16+
17+
/// <summary>
18+
/// 获得/设置 元素名称过滤器
19+
/// </summary>
20+
public string? ElementNameFilter { get; set; }
21+
22+
/// <summary>
23+
/// 获得/设置 是否返回所有属性
24+
/// </summary>
25+
public bool ReturnAllProperties { get; set; }
26+
27+
/// <summary>
28+
/// 获得/设置 是否返回属性值
29+
/// </summary>
30+
public bool ReturnPropertyValues { get; set; }
31+
32+
/// <summary>
33+
/// 获得/设置 浏览过滤器类型
34+
/// </summary>
35+
public OpcBrowseFilterType BrowseFilter { get; set; }
36+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Argo Zhang (argo@163.com). All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
// Website: https://www.blazor.zone or https://argozhang.github.io/
4+
5+
namespace BootstrapBlazor.OpcDa;
6+
7+
/// <summary>
8+
/// 对 OpcDataServer BrowsePosition 的封装类
9+
/// </summary>
10+
public class OpcBrowsePosition
11+
{
12+
internal OpcBrowsePosition(Opc.Da.BrowsePosition? position)
13+
{
14+
Position = position;
15+
}
16+
17+
internal Opc.Da.BrowsePosition? Position { get; set; }
18+
}

src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44

55
using Opc;
66
using Opc.Da;
7-
using System.Collections.Concurrent;
87
using System.Runtime.Versioning;
98

109
namespace BootstrapBlazor.OpcDa;
@@ -16,7 +15,6 @@ namespace BootstrapBlazor.OpcDa;
1615
sealed class OpcDaServer : IOpcDaServer
1716
{
1817
private Opc.Da.Server? _server = null;
19-
private readonly ConcurrentDictionary<string, HashSet<OpcReadItem>> _valuesCache = [];
2018

2119
/// <summary>
2220
/// 获得 OPC Server 名称
@@ -143,6 +141,34 @@ private Opc.Da.Server GetOpcServer()
143141
return _server;
144142
}
145143

144+
/// <summary>
145+
/// <inheritdoc/>
146+
/// </summary>
147+
/// <param name="name"></param>
148+
/// <param name="filters"></param>
149+
/// <param name="position"></param>
150+
/// <returns></returns>
151+
public OpcBrowseElement[] Browse(string name, OpcBrowseFilters filters, out OpcBrowsePosition? position)
152+
{
153+
var server = GetOpcServer();
154+
var results = server.Browse(new ItemIdentifier(name), filters.ToFilters(), out var pos) ?? [];
155+
position = pos == null ? null : new OpcBrowsePosition(pos);
156+
return [.. results.Select(element => new OpcBrowseElement(element))];
157+
}
158+
159+
/// <summary>
160+
/// <inheritdoc/>
161+
/// </summary>
162+
/// <param name="position"></param>
163+
/// <returns></returns>
164+
public OpcBrowseElement[] BrowseNext(OpcBrowsePosition position)
165+
{
166+
var server = GetOpcServer();
167+
var pos = position.Position;
168+
var results = server.BrowseNext(ref pos) ?? [];
169+
return [.. results.Select(element => new OpcBrowseElement(element))];
170+
}
171+
146172
/// <summary>
147173
/// Dispose 方法
148174
/// </summary>

test/UnitTestOpcDa/UnitTest1.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,19 @@ public async Task Subscription_Ok()
8080
server.Disconnect();
8181
server.Dispose();
8282
}
83+
84+
[Fact]
85+
public void Browser_Ok()
86+
{
87+
var sc = new ServiceCollection();
88+
sc.AddOpcDaServer();
89+
90+
var sp = sc.BuildServiceProvider();
91+
var server = sp.GetRequiredService<IOpcDaServer>();
92+
server.Connect("opcda://localhost/Kepware.KEPServerEX.V6");
93+
94+
var elements = server.Browse("Channel1", new OpcBrowseFilters(), out var position);
95+
Assert.Equal(3, elements.Length);
96+
Assert.Null(position);
97+
}
8398
}

0 commit comments

Comments
 (0)