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);
 | ||
|     }
 | ||
| 
 | ||
| } | 
