457 lines
14 KiB
C#
457 lines
14 KiB
C#
#region copyright
|
||
//------------------------------------------------------------------------------
|
||
// 此代码版权声明为全文件覆盖,如有原作者特别声明,会在下方手动补充
|
||
// 此代码版权(除特别声明外的代码)归作者本人Diego所有
|
||
// 源代码使用协议遵循本仓库的开源协议及附加协议
|
||
// Gitee源代码仓库:https://gitee.com/diego2098/ThingsGateway
|
||
// Github源代码仓库:https://github.com/kimdiego2098/ThingsGateway
|
||
// 使用文档:https://diego2098.gitee.io/thingsgateway-docs/
|
||
// QQ群:605534569
|
||
//------------------------------------------------------------------------------
|
||
#endregion
|
||
|
||
using System.Runtime.InteropServices;
|
||
using System.Text;
|
||
using System.Timers;
|
||
|
||
using ThingsGateway.Foundation.Adapter.OPCDA.Da;
|
||
using ThingsGateway.Foundation.Adapter.OPCDA.Rcw;
|
||
|
||
using Timer = System.Timers.Timer;
|
||
|
||
//部分非托管交互代码来自https://gitee.com/Zer0Day/opc-client与OPC基金会opcnet库,更改部分逻辑
|
||
|
||
namespace ThingsGateway.Foundation.Adapter.OPCDA;
|
||
/// <summary>
|
||
/// 订阅变化项
|
||
/// </summary>
|
||
/// <param name="values"></param>
|
||
public delegate void DataChangedEventHandler(List<ItemReadResult> values);
|
||
/// <summary>
|
||
/// OPCDAClient
|
||
/// </summary>
|
||
public class OPCDAClient : IDisposable
|
||
{
|
||
/// <summary>
|
||
/// LogAction
|
||
/// </summary>
|
||
private readonly Action<byte, object, string, Exception> _logAction;
|
||
|
||
private readonly object checkLock = new();
|
||
|
||
private Timer checkTimer;
|
||
|
||
private int IsExit = 1;
|
||
/// <summary>
|
||
/// 当前保存的需订阅列表
|
||
/// </summary>
|
||
private Dictionary<string, List<OpcItem>> ItemDicts = new();
|
||
|
||
private OpcServer m_server;
|
||
private bool publicConnect;
|
||
/// <summary>
|
||
/// <inheritdoc/>
|
||
/// </summary>
|
||
public OPCDAClient(Action<byte, object, string, Exception> logAction)
|
||
{
|
||
#if (NET6_0_OR_GREATER)
|
||
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
|
||
{
|
||
throw new NotSupportedException("不支持非windows系统");
|
||
}
|
||
#endif
|
||
_logAction = logAction;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 数据变化事件
|
||
/// </summary>
|
||
public event DataChangedEventHandler DataChangedHandler;
|
||
|
||
/// <summary>
|
||
/// 是否连接成功
|
||
/// </summary>
|
||
public bool IsConnected => m_server?.IsConnected == true;
|
||
|
||
/// <summary>
|
||
/// 当前配置
|
||
/// </summary>
|
||
public OPCNode OPCNode { get; private set; }
|
||
private List<OpcGroup> Groups => m_server.OpcGroups;
|
||
|
||
/// <summary>
|
||
/// 添加节点,需要在连接成功后执行
|
||
/// </summary>
|
||
/// <param name="items">组名称/变量节点,注意每次添加的组名称不能相同</param>
|
||
public void AddItems(Dictionary<string, List<OpcItem>> items)
|
||
{
|
||
if (IsExit == 1) throw new("对象已释放");
|
||
foreach (var item in items)
|
||
{
|
||
if (IsExit == 1) throw new("对象已释放");
|
||
try
|
||
{
|
||
var subscription = m_server.AddGroup(item.Key, true, OPCNode.UpdateRate, OPCNode.DeadBand);
|
||
subscription.ActiveSubscribe = OPCNode.ActiveSubscribe;
|
||
subscription.OnDataChanged += Subscription_OnDataChanged;
|
||
subscription.OnReadCompleted += Subscription_OnDataChanged;
|
||
|
||
var result = subscription.AddOpcItem(item.Value.ToArray());
|
||
StringBuilder stringBuilder = new StringBuilder();
|
||
if (result.Count > 0)
|
||
{
|
||
foreach (var item1 in result)
|
||
{
|
||
stringBuilder.Append($"{item1.Item1.ItemID}:{item1.Item2}");
|
||
}
|
||
_logAction?.Invoke(3, this, $"添加变量失败:{stringBuilder}", null);
|
||
}
|
||
else
|
||
{
|
||
ItemDicts.AddOrUpdate(item.Key, item.Value.Where(a => !result.Select(b => b.Item1).Contains(a)).ToList());
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logAction?.Invoke(3, this, $"添加组失败:{ex.Message}", ex);
|
||
}
|
||
}
|
||
for (int i = 0; i < Groups?.Count; i++)
|
||
{
|
||
var group = Groups[i];
|
||
if (group != null)
|
||
{
|
||
if (group.OpcItems.Count == 0)
|
||
{
|
||
ItemDicts.Remove(group.Name);
|
||
m_server.RemoveGroup(group);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 设置节点并保存,每次重连会自动添加节点
|
||
/// </summary>
|
||
/// <param name="items"></param>
|
||
/// <returns></returns>
|
||
public Dictionary<string, List<OpcItem>> AddItemsWithSave(List<string> items)
|
||
{
|
||
int i = 0;
|
||
ItemDicts = items.ToList().ConvertAll(o => new OpcItem(o)).ChunkTrivialBetter(OPCNode.GroupSize).ToDictionary(a => "default" + (i++));
|
||
return ItemDicts;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 连接服务器
|
||
/// </summary>
|
||
public void Connect()
|
||
{
|
||
publicConnect = true;
|
||
Interlocked.CompareExchange(ref IsExit, 0, 1);
|
||
PrivateConnect();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 断开连接
|
||
/// </summary>
|
||
public void Disconnect()
|
||
{
|
||
Interlocked.CompareExchange(ref IsExit, 1, 0);
|
||
PrivateDisconnect();
|
||
}
|
||
|
||
/// <inheritdoc/>
|
||
public void Dispose()
|
||
{
|
||
try
|
||
{
|
||
PrivateDisconnect();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logAction?.Invoke(3, this, $"连接释放失败:{ex.Message}", ex);
|
||
}
|
||
Interlocked.CompareExchange(ref IsExit, 1, 0);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 浏览节点
|
||
/// </summary>
|
||
/// <param name="itemId"></param>
|
||
/// <returns></returns>
|
||
public List<BrowseElement> GetBrowseElements(string itemId = null)
|
||
{
|
||
return this.m_server?.Browse(itemId);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 获取服务状态
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public ServerStatus GetServerStatus()
|
||
{
|
||
return this.m_server?.GetServerStatus();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 初始化设置
|
||
/// </summary>
|
||
/// <param name="node"></param>
|
||
public void Init(OPCNode node)
|
||
{
|
||
if (node != null)
|
||
OPCNode = node;
|
||
checkTimer?.Stop();
|
||
checkTimer?.Dispose();
|
||
checkTimer = new Timer(OPCNode.CheckRate * 60 * 1000);
|
||
checkTimer.Elapsed += CheckTimer_Elapsed;
|
||
checkTimer.Start();
|
||
try
|
||
{
|
||
m_server?.Dispose();
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logAction?.Invoke(3, this, $"连接释放失败:{ex.Message}", ex);
|
||
}
|
||
m_server = new OpcServer(OPCNode.OPCName, OPCNode.OPCIP);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 按OPC组读取组内变量,结果会在订阅事件中返回
|
||
/// </summary>
|
||
/// <param name="groupName">组名称,值为null时读取全部组</param>
|
||
/// <returns></returns>
|
||
public void ReadItemsWithGroup(string groupName = null)
|
||
{
|
||
PrivateConnect();
|
||
{
|
||
var groups = groupName != null ? Groups.Where(a => a.Name == groupName) : Groups;
|
||
foreach (var group in groups)
|
||
{
|
||
if (group.OpcItems.Count > 0)
|
||
{
|
||
group.ReadAsync();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/// <summary>
|
||
/// 移除节点
|
||
/// </summary>
|
||
/// <param name="items"></param>
|
||
public void RemoveItems(List<string> items)
|
||
{
|
||
foreach (var item in items)
|
||
{
|
||
if (IsExit == 1) return;
|
||
var opcGroups = Groups.Where(it => it.OpcItems.Any(a => a.ItemID == item));
|
||
foreach (var opcGroup in opcGroups)
|
||
{
|
||
var tag = opcGroup.OpcItems.Where(a => item == a.ItemID);
|
||
var result = opcGroup.RemoveItem(tag.ToArray());
|
||
|
||
if (opcGroup.OpcItems.Count == 0)
|
||
{
|
||
opcGroup.OnDataChanged -= Subscription_OnDataChanged;
|
||
ItemDicts.Remove(opcGroup.Name);
|
||
m_server.RemoveGroup(opcGroup);
|
||
}
|
||
else
|
||
{
|
||
ItemDicts[opcGroup.Name].RemoveWhere(a => tag.Contains(a) && !result.Select(b => b.Item1).Contains(a));
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
}
|
||
/// <inheritdoc/>
|
||
public override string ToString()
|
||
{
|
||
return OPCNode?.ToString();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 批量写入值
|
||
/// </summary>
|
||
/// <returns></returns>
|
||
public Dictionary<string, Tuple<bool, string>> WriteItem(Dictionary<string, object> writeInfos)
|
||
{
|
||
PrivateConnect();
|
||
Dictionary<string, Tuple<bool, string>> results = new();
|
||
|
||
var valueGroup = writeInfos.GroupBy(itemId =>
|
||
{
|
||
var group = Groups.FirstOrDefault(it => it.OpcItems.Any(a => a.ItemID == itemId.Key));
|
||
return group;
|
||
}).ToList();
|
||
|
||
foreach (var item1 in valueGroup)
|
||
{
|
||
try
|
||
{
|
||
if (item1.Key == null)
|
||
{
|
||
foreach (var item2 in item1)
|
||
{
|
||
results.AddOrUpdate(item2.Key, Tuple.Create(true, $"不存在该变量{item2.Key}"));
|
||
}
|
||
}
|
||
else
|
||
{
|
||
List<int> serverHandles = new();
|
||
Dictionary<int, OpcItem> handleItems = new();
|
||
List<object> values = new();
|
||
foreach (var item2 in item1)
|
||
{
|
||
var opcItem = item1.Key.OpcItems.Where(it => it.ItemID == item2.Key).FirstOrDefault();
|
||
serverHandles.Add(opcItem.ServerHandle);
|
||
handleItems.AddOrUpdate(opcItem.ServerHandle, opcItem);
|
||
var rawWriteValue = item2.Value;
|
||
values.Add(rawWriteValue);
|
||
}
|
||
|
||
var result = item1.Key.Write(values.ToArray(), serverHandles.ToArray());
|
||
var data = item1.ToList();
|
||
foreach (var item2 in result)
|
||
{
|
||
results.AddOrUpdate(handleItems[item2.Item1].ItemID, Tuple.Create(true, $"错误代码{item2.Item2}"));
|
||
}
|
||
}
|
||
foreach (var item2 in item1)
|
||
{
|
||
results.AddOrUpdate(item2.Key, Tuple.Create(false, $"成功"));
|
||
}
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
var keys = writeInfos.Keys.ToList();
|
||
foreach (var item in keys)
|
||
{
|
||
results.AddOrUpdate(item, Tuple.Create(true, ex.Message));
|
||
}
|
||
return results;
|
||
}
|
||
}
|
||
return results;
|
||
}
|
||
private void CheckTimer_Elapsed(object sender, ElapsedEventArgs e)
|
||
{
|
||
lock (checkLock)
|
||
{
|
||
|
||
if (IsExit == 0)
|
||
{
|
||
try
|
||
{
|
||
var status = m_server.GetServerStatus();
|
||
}
|
||
catch
|
||
{
|
||
if (IsExit == 0 && publicConnect)
|
||
{
|
||
try
|
||
{
|
||
PrivateConnect();
|
||
_logAction?.Invoke(1, this, $"重新链接成功", null);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logAction?.Invoke(3, this, $"重新链接失败:{ex.Message}", ex);
|
||
}
|
||
}
|
||
}
|
||
}
|
||
else
|
||
{
|
||
var timeer = sender as Timer;
|
||
timeer.Enabled = false;
|
||
timeer.Stop();
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
private void PrivateAddItems()
|
||
{
|
||
try
|
||
{
|
||
AddItems(ItemDicts);
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logAction?.Invoke(3, this, $"添加点位失败:{ex.Message}", ex);
|
||
}
|
||
}
|
||
private void PrivateConnect()
|
||
{
|
||
lock (this)
|
||
{
|
||
|
||
if (m_server?.IsConnected == true)
|
||
{
|
||
try
|
||
{
|
||
var status = m_server.GetServerStatus();
|
||
}
|
||
catch
|
||
{
|
||
try
|
||
{
|
||
var status1 = m_server.GetServerStatus();
|
||
}
|
||
catch
|
||
{
|
||
|
||
Init(OPCNode);
|
||
m_server?.Connect();
|
||
_logAction?.Invoke(1, this, $"{m_server.Host} - {m_server.Name} - 连接成功", null);
|
||
PrivateAddItems();
|
||
}
|
||
}
|
||
|
||
|
||
}
|
||
else
|
||
{
|
||
Init(OPCNode);
|
||
m_server?.Connect();
|
||
_logAction?.Invoke(1, this, $"{m_server.Host} - {m_server.Name} - 连接成功", null);
|
||
PrivateAddItems();
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
private void PrivateDisconnect()
|
||
{
|
||
lock (this)
|
||
{
|
||
if (IsConnected)
|
||
_logAction?.Invoke(1, this, $"{m_server.Host} - {m_server.Name} - 断开连接", null);
|
||
if (checkTimer != null)
|
||
{
|
||
checkTimer.Enabled = false;
|
||
checkTimer.Stop();
|
||
}
|
||
|
||
try
|
||
{
|
||
m_server?.Dispose();
|
||
m_server = null;
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logAction?.Invoke(3, this, $"连接释放失败:{ex.Message}", ex);
|
||
}
|
||
}
|
||
}
|
||
private void Subscription_OnDataChanged(List<ItemReadResult> values)
|
||
{
|
||
DataChangedHandler?.Invoke(values);
|
||
}
|
||
|
||
} |