Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Version>9.0.0-beta01</Version>
<Version>9.0.0</Version>
<RootNamespace>BootstrapBlazor.OpcDa</RootNamespace>
</PropertyGroup>

<PropertyGroup>
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components SqlSugar</PackageTags>
<Description>Bootstrap UI components extensions of SqlSugar</Description>
<PackageTags>Bootstrap Blazor WebAssembly wasm UI Components OpcDa PLC </PackageTags>
<Description>Bootstrap UI components extensions of OpcDaServer</Description>
</PropertyGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net6.0'">
Expand Down
8 changes: 6 additions & 2 deletions src/extensions/BootstrapBlazor.OpcDa/Extensions/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
Expand All @@ -11,13 +13,15 @@ internal static class Extensions
{
public static Quality ToQuality(this Opc.Da.Quality quality)
{
return quality.QualityBits == Opc.Da.qualityBits.good
return quality.QualityBits == qualityBits.good
? Quality.Good
: Quality.Bad;
}

public static ISubscription ToOpcSubscription(this Opc.Da.ISubscription subscription)
public static IOpcSubscription ToOpcSubscription(this ISubscription subscription)
{
return new OpcSubscription(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 });
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,30 @@
namespace Microsoft.Extensions.DependencyInjection;

/// <summary>
/// Opc Da 服务扩展类
/// OpcDaServer 服务扩展类
/// </summary>
public static class ServiceCollectionExtensions
{
/// <summary>
/// 增加 Opc 操作服务
/// 增加 OpcDaServer 操作服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
[SupportedOSPlatform("windows")]
public static IServiceCollection AddOpcServer(this IServiceCollection services)
public static IServiceCollection AddOpcDaServer(this IServiceCollection services)
{
services.AddSingleton<IOpcServer, OpcServer>();
services.AddSingleton<IOpcDaServer, OpcDaServer>();
return services;
}

/// <summary>
/// 增加模拟 OpcDaServer 操作服务
/// </summary>
/// <param name="services"></param>
/// <returns></returns>
public static IServiceCollection AddMockOpcDaServer(this IServiceCollection services)
{
services.AddSingleton<IOpcDaServer, MockOpcDaServer>();
return services;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@
namespace BootstrapBlazor.OpcDa;

/// <summary>
/// Opc Server 接口定义
/// OpcDaServer 接口定义
/// </summary>
public interface IOpcServer : IDisposable
public interface IOpcDaServer : IDisposable
{
/// <summary>
/// 获得 OPC Server 是否已连接
Expand Down Expand Up @@ -35,7 +35,7 @@ public interface IOpcServer : IDisposable
/// 取消订阅方法
/// </summary>
/// <param name="subscription"></param>
void CancelSubscription(ISubscription subscription);
void CancelSubscription(IOpcSubscription subscription);

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

/// <summary>
/// 读取 Item 值方法
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@ namespace BootstrapBlazor.OpcDa;
/// <summary>
/// 订阅接口定义
/// </summary>
public interface ISubscription
public interface IOpcSubscription
{
/// <summary>
/// 获得 订阅名称
/// </summary>
public string Name { get; }

/// <summary>
/// 获得/设置 是否保留最后一个值
/// </summary>
Expand All @@ -19,12 +24,6 @@ public interface ISubscription
/// </summary>
Action<List<OpcReadItem>>? DataChanged { get; set; }

/// <summary>
/// 获得 <see cref="Opc.Da.ISubscription"/> 实例
/// </summary>
/// <returns></returns>
Opc.Da.ISubscription GetSubscription();

/// <summary>
/// 增加数据项
/// </summary>
Expand Down
68 changes: 68 additions & 0 deletions src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcDaServer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// 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;

/// <summary>
/// 模拟 OpcDa Server 实现类
/// </summary>
sealed class MockOpcDaServer : IOpcDaServer
{
public bool IsConnected { get; set; }

public string? ServerName { get; set; }

private readonly Dictionary<string, IOpcSubscription> _subscriptions = [];

public bool Connect(string serverName)
{
ServerName = serverName;
IsConnected = true;
return true;
}

public void Disconnect()
{
IsConnected = false;
ServerName = null;
Comment on lines +25 to +28
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Disconnect does not clear or dispose subscriptions.

Please ensure all items in _subscriptions are disposed and cleared in Disconnect to prevent resource leaks or unintended background activity.

}

public IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true)
{
if (_subscriptions.TryGetValue(name, out var subscription))
{
CancelSubscription(subscription);
}

subscription = new MockOpcDaSubscription(name, updateRate, active);
_subscriptions.Add(name, subscription);
return subscription;
}

public void CancelSubscription(IOpcSubscription subscription)
{
_subscriptions.Remove(subscription.Name);
if (subscription is IDisposable disposable)
{
disposable.Dispose();
}
}

public HashSet<OpcReadItem> Read(params HashSet<string> items)
{
return items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000)))
.ToHashSet(OpcItemEqualityComparer<OpcReadItem>.Default);
}

public HashSet<OpcWriteItem> Write(params HashSet<OpcWriteItem> items)
{
return items.Select(i => new OpcWriteItem(i.Name, i.Value) { Result = true })
.ToHashSet(OpcItemEqualityComparer<OpcWriteItem>.Default);
}

public void Dispose()
{

}
}
83 changes: 83 additions & 0 deletions src/extensions/BootstrapBlazor.OpcDa/Mock/MockOpcSubscription.cs
Original file line number Diff line number Diff line change
@@ -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 BootstrapBlazor.OpcDa;

sealed class MockOpcDaSubscription : IOpcSubscription, IDisposable
{
private readonly int _updateRate;
private readonly bool _active;
private readonly List<string> _items = [];
private CancellationTokenSource? _cts;

public MockOpcDaSubscription(string name, int updateRate = 1000, bool active = true)
{
Name = name;
_updateRate = updateRate;
_active = active;

_cts = new CancellationTokenSource();
_ = Task.Run(() => DoTask(_cts.Token));
}

public string Name { get; }

public bool KeepLastValue { get; set; }

public Action<List<OpcReadItem>>? DataChanged { get; set; }

public void AddItems(IEnumerable<string> items)
{
_items.AddRange(items);
}

private void UpdateValues()
{
if (DataChanged != null)
{
var values = _items.Select(i => new OpcReadItem(i, Quality.Good, DateTime.Now, Random.Shared.Next(1000, 2000))).ToList();
DataChanged.Invoke(values);
}
}

private async Task DoTask(CancellationToken token)
{
do
{
try
{
if (_active)
{
UpdateValues();
}

await Task.Delay(_updateRate, token);
}
catch (OperationCanceledException)
{
// ignored
}
}
while (!token.IsCancellationRequested);
}

private void Dispose(bool disposing)
{
if (disposing)
{
if (_cts != null)
{
_cts.Cancel();
_cts.Dispose();
_cts = null;
}
}
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ namespace BootstrapBlazor.OpcDa;
/// OPC Server 操作类
/// </summary>
[SupportedOSPlatform("windows")]
class OpcServer : IOpcServer
sealed class OpcDaServer : IOpcDaServer
{
private Opc.Da.Server? _server = null;
private readonly ConcurrentDictionary<string, HashSet<OpcReadItem>> _valuesCache = [];
Expand All @@ -28,6 +28,8 @@ class OpcServer : IOpcServer
/// </summary>
public bool IsConnected => _server?.IsConnected ?? false;

private readonly Dictionary<string, ISubscription> _subscriptions = [];

/// <summary>
/// 连接到 OPCServer 方法
/// </summary>
Expand All @@ -54,12 +56,13 @@ public void Disconnect()
{
ServerName = string.Empty;

if (_server != null && _server.IsConnected)
if (_server is { IsConnected: true })
{
foreach (Subscription sub in _server.Subscriptions)
{
_server.CancelSubscription(sub);
}

_server.Disconnect();
_server = null;
}
Expand All @@ -72,28 +75,33 @@ public void Disconnect()
/// <param name="updateRate">更新频率 默认 1000 毫秒</param>
/// <param name="active">是否激活 默认 true</param>
/// <returns></returns>
public ISubscription CreateSubscription(string name, int updateRate = 1000, bool active = true)
public IOpcSubscription CreateSubscription(string name, int updateRate = 1000, bool active = true)
{
var server = GetOpcServer();
var subscription = server.CreateSubscription(new SubscriptionState
if (_subscriptions.TryGetValue(name, out var subscription))
{
Name = name,
Deadband = 0,
UpdateRate = updateRate,
Active = active
});
// 已经存在该订阅
server.CancelSubscription(subscription);
}

subscription = server.CreateSubscription(name, updateRate, active);
_subscriptions.Add(name, subscription);
return subscription.ToOpcSubscription();
}

/// <summary>
/// 取消订阅方法
/// </summary>
/// <param name="subscription">订阅接口 <see cref="ISubscription"/> 实例</param>
/// <param name="subscription">订阅接口 <see cref="IOpcSubscription"/> 实例</param>
/// <returns></returns>
public void CancelSubscription(ISubscription subscription)
public void CancelSubscription(IOpcSubscription subscription)
{
var server = GetOpcServer();
server.CancelSubscription(subscription.GetSubscription());
var name = subscription.Name;
if (_subscriptions.Remove(name, out var sub))
{
server.CancelSubscription(sub);
}
}

/// <summary>
Expand Down Expand Up @@ -121,24 +129,25 @@ public HashSet<OpcWriteItem> Write(params HashSet<OpcWriteItem> items)
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 };
return i with { Result = item != null && item.ResultID == ResultID.S_OK };
}).ToHashSet(OpcItemEqualityComparer<OpcWriteItem>.Default);
}

private Opc.Da.Server GetOpcServer()
{
if (_server == null)
if (_server is not { IsConnected: true })
{
throw new InvalidOperationException("OPC Server is not connected.");
}

return _server;
}

/// <summary>
/// Dispose 方法
/// </summary>
/// <param name="disposing"></param>
protected virtual void Dispose(bool disposing)
private void Dispose(bool disposing)
{
if (disposing)
{
Expand Down
Loading