Skip to content

Commit 851dad0

Browse files
authored
feat(OpcDa): add Mock function (#516)
* refactor: 重构代码 * refactor: 更改变量名称 * chore: bump vesion 9.0.0 * refactor: 增加 IOpcSubscription 接口 * feat: 增加 MockOpcDaServer 服务 * test: 更新单元测试 * refactor: 代码重构 * chore: bump version 9.0.0
1 parent a1ccad8 commit 851dad0

10 files changed

Lines changed: 220 additions & 45 deletions

File tree

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

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

88
<PropertyGroup>
9-
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components SqlSugar</PackageTags>
10-
<Description>Bootstrap UI components extensions of SqlSugar</Description>
9+
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components OpcDa PLC </PackageTags>
10+
<Description>Bootstrap UI components extensions of OpcDaServer</Description>
1111
</PropertyGroup>
1212

1313
<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">

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

Lines changed: 6 additions & 2 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>
@@ -11,13 +13,15 @@ internal static class Extensions
1113
{
1214
public static Quality ToQuality(this Opc.Da.Quality quality)
1315
{
14-
return quality.QualityBits == Opc.Da.qualityBits.good
16+
return quality.QualityBits == qualityBits.good
1517
? Quality.Good
1618
: Quality.Bad;
1719
}
1820

19-
public static ISubscription ToOpcSubscription(this Opc.Da.ISubscription subscription)
21+
public static IOpcSubscription ToOpcSubscription(this ISubscription subscription)
2022
{
2123
return new OpcSubscription(subscription);
2224
}
25+
26+
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 });
2327
}

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

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,19 +8,30 @@
88
namespace Microsoft.Extensions.DependencyInjection;
99

1010
/// <summary>
11-
/// Opc Da 服务扩展类
11+
/// OpcDaServer 服务扩展类
1212
/// </summary>
1313
public static class ServiceCollectionExtensions
1414
{
1515
/// <summary>
16-
/// 增加 Opc 操作服务
16+
/// 增加 OpcDaServer 操作服务
1717
/// </summary>
1818
/// <param name="services"></param>
1919
/// <returns></returns>
2020
[SupportedOSPlatform("windows")]
21-
public static IServiceCollection AddOpcServer(this IServiceCollection services)
21+
public static IServiceCollection AddOpcDaServer(this IServiceCollection services)
2222
{
23-
services.AddSingleton<IOpcServer, OpcServer>();
23+
services.AddSingleton<IOpcDaServer, OpcDaServer>();
24+
return services;
25+
}
26+
27+
/// <summary>
28+
/// 增加模拟 OpcDaServer 操作服务
29+
/// </summary>
30+
/// <param name="services"></param>
31+
/// <returns></returns>
32+
public static IServiceCollection AddMockOpcDaServer(this IServiceCollection services)
33+
{
34+
services.AddSingleton<IOpcDaServer, MockOpcDaServer>();
2435
return services;
2536
}
2637
}

src/extensions/BootstrapBlazor.OpcDa/IOpcServer.cs renamed to src/extensions/BootstrapBlazor.OpcDa/IOpcDaServer.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
namespace BootstrapBlazor.OpcDa;
66

77
/// <summary>
8-
/// Opc Server 接口定义
8+
/// OpcDaServer 接口定义
99
/// </summary>
10-
public interface IOpcServer : IDisposable
10+
public interface IOpcDaServer : IDisposable
1111
{
1212
/// <summary>
1313
/// 获得 OPC Server 是否已连接
@@ -35,7 +35,7 @@ public interface IOpcServer : IDisposable
3535
/// 取消订阅方法
3636
/// </summary>
3737
/// <param name="subscription"></param>
38-
void CancelSubscription(ISubscription subscription);
38+
void CancelSubscription(IOpcSubscription subscription);
3939

4040
/// <summary>
4141
/// 创建订阅方法
@@ -44,7 +44,7 @@ public interface IOpcServer : IDisposable
4444
/// <param name="updateRate">更新频率 默认 1000 毫秒</param>
4545
/// <param name="active">是否激活 默认 true</param>
4646
/// <returns></returns>
47-
ISubscription CreateSubscription(string name, int updateRate = 1000, bool active = true);
47+
IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true);
4848

4949
/// <summary>
5050
/// 读取 Item 值方法

src/extensions/BootstrapBlazor.OpcDa/ISubscription.cs renamed to src/extensions/BootstrapBlazor.OpcDa/IOpcSubscription.cs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,13 @@ namespace BootstrapBlazor.OpcDa;
77
/// <summary>
88
/// 订阅接口定义
99
/// </summary>
10-
public interface ISubscription
10+
public interface IOpcSubscription
1111
{
12+
/// <summary>
13+
/// 获得 订阅名称
14+
/// </summary>
15+
public string Name { get; }
16+
1217
/// <summary>
1318
/// 获得/设置 是否保留最后一个值
1419
/// </summary>
@@ -19,12 +24,6 @@ public interface ISubscription
1924
/// </summary>
2025
Action<List<OpcReadItem>>? DataChanged { get; set; }
2126

22-
/// <summary>
23-
/// 获得 <see cref="Opc.Da.ISubscription"/> 实例
24-
/// </summary>
25-
/// <returns></returns>
26-
Opc.Da.ISubscription GetSubscription();
27-
2827
/// <summary>
2928
/// 增加数据项
3029
/// </summary>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
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 Server 实现类
9+
/// </summary>
10+
sealed class MockOpcDaServer : IOpcDaServer
11+
{
12+
public bool IsConnected { get; set; }
13+
14+
public string? ServerName { get; set; }
15+
16+
private readonly Dictionary<string, IOpcSubscription> _subscriptions = [];
17+
18+
public bool Connect(string serverName)
19+
{
20+
ServerName = serverName;
21+
IsConnected = true;
22+
return true;
23+
}
24+
25+
public void Disconnect()
26+
{
27+
IsConnected = false;
28+
ServerName = null;
29+
}
30+
31+
public IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true)
32+
{
33+
if (_subscriptions.TryGetValue(name, out var subscription))
34+
{
35+
CancelSubscription(subscription);
36+
}
37+
38+
subscription = new MockOpcDaSubscription(name, updateRate, active);
39+
_subscriptions.Add(name, subscription);
40+
return subscription;
41+
}
42+
43+
public void CancelSubscription(IOpcSubscription subscription)
44+
{
45+
_subscriptions.Remove(subscription.Name);
46+
if (subscription is IDisposable disposable)
47+
{
48+
disposable.Dispose();
49+
}
50+
}
51+
52+
public HashSet<OpcReadItem> Read(params HashSet<string> items)
53+
{
54+
return items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000)))
55+
.ToHashSet(OpcItemEqualityComparer<OpcReadItem>.Default);
56+
}
57+
58+
public HashSet<OpcWriteItem> Write(params HashSet<OpcWriteItem> items)
59+
{
60+
return items.Select(i => new OpcWriteItem(i.Name, i.Value) { Result = true })
61+
.ToHashSet(OpcItemEqualityComparer<OpcWriteItem>.Default);
62+
}
63+
64+
public void Dispose()
65+
{
66+
67+
}
68+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
sealed class MockOpcDaSubscription : IOpcSubscription, IDisposable
8+
{
9+
private readonly int _updateRate;
10+
private readonly bool _active;
11+
private readonly List<string> _items = [];
12+
private CancellationTokenSource? _cts;
13+
14+
public MockOpcDaSubscription(string name, int updateRate = 1000, bool active = true)
15+
{
16+
Name = name;
17+
_updateRate = updateRate;
18+
_active = active;
19+
20+
_cts = new CancellationTokenSource();
21+
_ = Task.Run(() => DoTask(_cts.Token));
22+
}
23+
24+
public string Name { get; }
25+
26+
public bool KeepLastValue { get; set; }
27+
28+
public Action<List<OpcReadItem>>? DataChanged { get; set; }
29+
30+
public void AddItems(IEnumerable<string> items)
31+
{
32+
_items.AddRange(items);
33+
}
34+
35+
private void UpdateValues()
36+
{
37+
if (DataChanged != null)
38+
{
39+
var values = _items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000))).ToList();
40+
DataChanged.Invoke(values);
41+
}
42+
}
43+
44+
private async Task DoTask(CancellationToken token)
45+
{
46+
do
47+
{
48+
try
49+
{
50+
if (_active)
51+
{
52+
UpdateValues();
53+
}
54+
55+
await Task.Delay(_updateRate, token);
56+
}
57+
catch (OperationCanceledException)
58+
{
59+
// ignored
60+
}
61+
}
62+
while (!token.IsCancellationRequested);
63+
}
64+
65+
private void Dispose(bool disposing)
66+
{
67+
if (disposing)
68+
{
69+
if (_cts != null)
70+
{
71+
_cts.Cancel();
72+
_cts.Dispose();
73+
_cts = null;
74+
}
75+
}
76+
}
77+
78+
public void Dispose()
79+
{
80+
Dispose(true);
81+
GC.SuppressFinalize(this);
82+
}
83+
}

src/extensions/BootstrapBlazor.OpcDa/OpcServer.cs renamed to src/extensions/BootstrapBlazor.OpcDa/OpcDaServer.cs

Lines changed: 24 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace BootstrapBlazor.OpcDa;
1313
/// OPC Server 操作类
1414
/// </summary>
1515
[SupportedOSPlatform("windows")]
16-
class OpcServer : IOpcServer
16+
sealed class OpcDaServer : IOpcDaServer
1717
{
1818
private Opc.Da.Server? _server = null;
1919
private readonly ConcurrentDictionary<string, HashSet<OpcReadItem>> _valuesCache = [];
@@ -28,6 +28,8 @@ class OpcServer : IOpcServer
2828
/// </summary>
2929
public bool IsConnected => _server?.IsConnected ?? false;
3030

31+
private readonly Dictionary<string, ISubscription> _subscriptions = [];
32+
3133
/// <summary>
3234
/// 连接到 OPCServer 方法
3335
/// </summary>
@@ -54,12 +56,13 @@ public void Disconnect()
5456
{
5557
ServerName = string.Empty;
5658

57-
if (_server != null && _server.IsConnected)
59+
if (_server is { IsConnected: true })
5860
{
5961
foreach (Subscription sub in _server.Subscriptions)
6062
{
6163
_server.CancelSubscription(sub);
6264
}
65+
6366
_server.Disconnect();
6467
_server = null;
6568
}
@@ -72,28 +75,33 @@ public void Disconnect()
7275
/// <param name="updateRate">更新频率 默认 1000 毫秒</param>
7376
/// <param name="active">是否激活 默认 true</param>
7477
/// <returns></returns>
75-
public ISubscription CreateSubscription(string name, int updateRate = 1000, bool active = true)
78+
public IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true)
7679
{
7780
var server = GetOpcServer();
78-
var subscription = server.CreateSubscription(new SubscriptionState
81+
if (_subscriptions.TryGetValue(name, out var subscription))
7982
{
80-
Name = name,
81-
Deadband = 0,
82-
UpdateRate = updateRate,
83-
Active = active
84-
});
83+
// 已经存在该订阅
84+
server.CancelSubscription(subscription);
85+
}
86+
87+
subscription = server.CreateSubscription(name, updateRate, active);
88+
_subscriptions.Add(name, subscription);
8589
return subscription.ToOpcSubscription();
8690
}
8791

8892
/// <summary>
8993
/// 取消订阅方法
9094
/// </summary>
91-
/// <param name="subscription">订阅接口 <see cref="ISubscription"/> 实例</param>
95+
/// <param name="subscription">订阅接口 <see cref="IOpcSubscription"/> 实例</param>
9296
/// <returns></returns>
93-
public void CancelSubscription(ISubscription subscription)
97+
public void CancelSubscription(IOpcSubscription subscription)
9498
{
9599
var server = GetOpcServer();
96-
server.CancelSubscription(subscription.GetSubscription());
100+
var name = subscription.Name;
101+
if (_subscriptions.Remove(name, out var sub))
102+
{
103+
server.CancelSubscription(sub);
104+
}
97105
}
98106

99107
/// <summary>
@@ -121,24 +129,25 @@ public HashSet<OpcWriteItem> Write(params HashSet<OpcWriteItem> items)
121129
return items.Select(i =>
122130
{
123131
var item = results.FirstOrDefault(v => v.ItemName == i.Name);
124-
return new OpcWriteItem(i.Name, i.Value) { Result = item != null && item.ResultID == ResultID.S_OK };
132+
return i with { Result = item != null && item.ResultID == ResultID.S_OK };
125133
}).ToHashSet(OpcItemEqualityComparer<OpcWriteItem>.Default);
126134
}
127135

128136
private Opc.Da.Server GetOpcServer()
129137
{
130-
if (_server == null)
138+
if (_server is not { IsConnected: true })
131139
{
132140
throw new InvalidOperationException("OPC Server is not connected.");
133141
}
142+
134143
return _server;
135144
}
136145

137146
/// <summary>
138147
/// Dispose 方法
139148
/// </summary>
140149
/// <param name="disposing"></param>
141-
protected virtual void Dispose(bool disposing)
150+
private void Dispose(bool disposing)
142151
{
143152
if (disposing)
144153
{

0 commit comments

Comments
 (0)