Skip to content

Commit e040600

Browse files
committed
feat: 增加基金会 OpcDa 组件
1 parent f07957c commit e040600

4 files changed

Lines changed: 256 additions & 0 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<Version>9.0.0</Version>
5+
</PropertyGroup>
6+
7+
<PropertyGroup>
8+
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components SqlSugar</PackageTags>
9+
<Description>Bootstrap UI components extensions of SqlSugar</Description>
10+
</PropertyGroup>
11+
12+
<ItemGroup>
13+
<Using Remove="BootstrapBlazor.Components" />
14+
<Using Remove="Microsoft.AspNetCore.Components" />
15+
<Using Remove="Microsoft.Extensions.DependencyInjection" />
16+
</ItemGroup>
17+
18+
<ItemGroup>
19+
<!--<PackageReference Include="OpcNetApi.Com" Version="2.1.108" />-->
20+
<PackageReference Include="ThingsGateway.Foundation.OpcDa" Version="10.9.69" />
21+
</ItemGroup>
22+
23+
</Project>
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
/// <see cref="Opc.Da.ISubscription"/> 扩展方法类
9+
/// </summary>
10+
public static class ISubscriptionExtensions
11+
{
12+
/// <summary>
13+
///
14+
/// </summary>
15+
/// <param name="subscription"></param>
16+
public static List<OpcItem> Read(this Opc.Da.ISubscription subscription)
17+
{
18+
return [];
19+
}
20+
}
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+
using Opc.Da;
8+
9+
/// <summary>
10+
/// OPC Item 配置实体类
11+
/// </summary>
12+
public record struct OpcItem(string Name, Quality Quality, DateTime Timestamp, object? Value)
13+
{
14+
/// <summary>
15+
/// 获得 Opc Item 上次值
16+
/// </summary>
17+
public object? LastValue { get; set; }
18+
}
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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;
6+
using Opc.Da;
7+
using System.Collections.Concurrent;
8+
9+
namespace BootstrapBlazor.OpcDa;
10+
11+
/// <summary>
12+
/// OPC Server 操作类
13+
/// </summary>
14+
public partial class OpcServer : IDisposable
15+
{
16+
private Opc.Da.Server? _server = null;
17+
private readonly ConcurrentDictionary<string, HashSet<OpcItem>> _valuesCache = [];
18+
private readonly TaskCompletionSource _readTask = new();
19+
20+
/// <summary>
21+
/// 获得 OPC Server 名称
22+
/// </summary>
23+
public string? ServerName { get; private set; }
24+
25+
/// <summary>
26+
/// 获得 OPC Server 状态
27+
/// </summary>
28+
public bool IsConnected => _server?.IsConnected ?? false;
29+
30+
/// <summary>
31+
/// 连接到 OPCServer 方法
32+
/// </summary>
33+
/// <param name="serverName">服务器名称</param>
34+
/// <param name="token"></param>
35+
/// <remarks>opcda://localhost/Kepware.KEPServerEX.V6</remarks>
36+
/// <returns>成功时返回真</returns>
37+
public async Task<bool> Connect(string serverName, CancellationToken? token = null)
38+
{
39+
ServerName = serverName;
40+
41+
try
42+
{
43+
// 如果已经连接则先断开
44+
Disconnect();
45+
46+
await Task.Run(() =>
47+
{
48+
_server = new Opc.Da.Server(new OpcCom.Factory(), new URL(serverName));
49+
_server.Connect();
50+
}, token ?? CancellationToken.None);
51+
}
52+
catch (OperationCanceledException)
53+
{
54+
55+
}
56+
return IsConnected;
57+
}
58+
59+
/// <summary>
60+
/// 断开连接方法
61+
/// </summary>
62+
/// <returns></returns>
63+
public void Disconnect()
64+
{
65+
ServerName = string.Empty;
66+
67+
if (_server != null && _server.IsConnected)
68+
{
69+
foreach (Subscription sub in _server.Subscriptions)
70+
{
71+
_server.CancelSubscription(sub);
72+
}
73+
_server.Disconnect();
74+
_server = null;
75+
}
76+
}
77+
78+
/// <summary>
79+
/// 创建订阅方法
80+
/// </summary>
81+
/// <param name="name">订阅名称</param>
82+
/// <param name="updateRate"></param>
83+
/// <param name="active"></param>
84+
/// <param name="items"></param>
85+
/// <returns></returns>
86+
public ISubscription CreateSubscription(string name, int updateRate, bool active = true, Item[]? items = null)
87+
{
88+
var server = GetOpcServer();
89+
var subscription = server.CreateSubscription(new SubscriptionState
90+
{
91+
Name = name,
92+
Active = active,
93+
UpdateRate = updateRate,
94+
Deadband = 0
95+
});
96+
97+
if (items is { Length: > 0 })
98+
{
99+
subscription.AddItems(items);
100+
subscription.DataChanged += (subscriptionHandle, requestHandle, values) =>
101+
{
102+
foreach (var value in values)
103+
{
104+
_valuesCache.AddOrUpdate(name, key => AddFactory(value), (key, v) => UpdateFactory(v, value));
105+
}
106+
107+
_readTask.TrySetResult();
108+
};
109+
}
110+
return subscription;
111+
}
112+
113+
private static HashSet<OpcItem> AddFactory(ItemValueResult value)
114+
{
115+
return new HashSet<OpcItem>(OpcItemEqualityComparer.Instance)
116+
{
117+
new(value.ItemName, value.Quality, value.Timestamp, value.Value)
118+
};
119+
}
120+
121+
private static HashSet<OpcItem> UpdateFactory(HashSet<OpcItem> items, ItemValueResult value)
122+
{
123+
var item = new OpcItem(value.ItemName, value.Quality, value.Timestamp, value.Value);
124+
if (items.TryGetValue(item, out var v))
125+
{
126+
item.LastValue = v.Value;
127+
}
128+
items.Remove(item);
129+
items.Add(item);
130+
return items;
131+
}
132+
133+
/// <summary>
134+
/// 取消订阅方法
135+
/// </summary>
136+
/// <param name="subscription">订阅接口 <see cref="ISubscription"/> 实例</param>
137+
/// <returns></returns>
138+
public void CancelSubscription(ISubscription subscription)
139+
{
140+
var server = GetOpcServer();
141+
server.CancelSubscription(subscription);
142+
}
143+
144+
/// <summary>
145+
/// 读取指定 Item 值方法
146+
/// </summary>
147+
/// <param name="items"></param>
148+
/// <returns></returns>
149+
/// <exception cref="InvalidOperationException"></exception>
150+
public HashSet<OpcItem> Read(params List<string> items)
151+
{
152+
var server = GetOpcServer();
153+
var results = server.Read([.. items.Select(i => new Item() { ItemName = i })]);
154+
return results.Select(i => new OpcItem(i.ItemName, i.Quality, i.Timestamp, i.Value)).ToHashSet(OpcItemEqualityComparer.Instance);
155+
}
156+
157+
private Opc.Da.Server GetOpcServer()
158+
{
159+
if (_server == null)
160+
{
161+
throw new InvalidOperationException("OPC Server is not connected.");
162+
}
163+
return _server;
164+
}
165+
166+
/// <summary>
167+
/// Dispose 方法
168+
/// </summary>
169+
/// <param name="disposing"></param>
170+
protected virtual void Dispose(bool disposing)
171+
{
172+
if (disposing)
173+
{
174+
Disconnect();
175+
}
176+
}
177+
178+
/// <summary>
179+
/// <inheritdoc/>
180+
/// </summary>
181+
public void Dispose()
182+
{
183+
Dispose(true);
184+
GC.SuppressFinalize(this);
185+
}
186+
187+
class OpcItemEqualityComparer : IEqualityComparer<OpcItem>
188+
{
189+
public static OpcItemEqualityComparer Instance = new();
190+
191+
public bool Equals(OpcItem x, OpcItem y) => x.Name == y.Name;
192+
193+
public int GetHashCode([DisallowNull] OpcItem item) => item.Name.GetHashCode();
194+
}
195+
}

0 commit comments

Comments
 (0)