You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

356 lines
12 KiB
C#

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

#region << 版 本 注 释 >>
/*--------------------------------------------------------------------
* 版权所有 (c) 2025 WenJY 保留所有权利。
* CLR版本4.0.30319.42000
* 机器名称Mr.Wen's MacBook Pro
* 命名空间Sln.Imm.Daemon.Opc.Impl
* 唯一标识AAD72EB2-53B5-43B2-AD71-77FE72AC816E
*
* 创建者WenJY
* 电子邮箱:
* 创建时间2025-09-22 17:24:43
* 版本V1.0.0
* 描述:
*
*--------------------------------------------------------------------
* 修改人:
* 时间:
* 修改说明:
*
* 版本V1.0.0
*--------------------------------------------------------------------*/
#endregion << 版 本 注 释 >>
using Opc.Ua;
using Opc.Ua.Client;
using Sln.Imm.Daemon.Model.dto;
using System.Threading;
namespace Sln.Imm.Daemon.Opc.Impl;
public class OpcUaService : IOpcService, IDisposable
{
private ApplicationConfiguration _config;
private Session _session;
private bool _disposed = false;
private static readonly TimeSpan DefaultDisconnectTimeout = TimeSpan.FromSeconds(5);
// 全局限制:同时对 OPC UA 服务器建立的 Session 数量(避免 BadMaxConnectionsReached
// 如果你的 OPC UA 服务器限制更低/更高,可以调整这个数(建议小于服务器 MaxConnections/MaxSessions
private static readonly SemaphoreSlim UaConnectionSemaphore = new SemaphoreSlim(8, 8);
private bool _holdsUaConnectionSlot;
public OpcUaService()
{
_config = new ApplicationConfiguration()
{
ApplicationName = "OPC UA Client",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration
{
// 使用最小化的证书配置
ApplicationCertificate = new CertificateIdentifier
{
StoreType = "Directory",
StorePath = "./pki/own" // 使用相对路径
},
TrustedPeerCertificates = new CertificateTrustList
{
StoreType = "Directory",
StorePath = "./pki/trusted" // 使用相对路径
},
TrustedIssuerCertificates = new CertificateTrustList
{
StoreType = "Directory",
StorePath = "./pki/issuers" // 使用相对路径
},
RejectedCertificateStore = new CertificateTrustList
{
StoreType = "Directory",
StorePath = "./pki/rejected" // 使用相对路径
},
AutoAcceptUntrustedCertificates = true // 自动接受证书(仅测试环境)
},
TransportConfigurations = new TransportConfigurationCollection(),
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 }
};
_config.Validate(ApplicationType.Client).Wait();
}
public async Task<bool> ConnectAsync(string serverUrl)
{
// 先拿到“连接名额”,避免瞬时并发把服务器连接打满
await UaConnectionSemaphore.WaitAsync().ConfigureAwait(false);
_holdsUaConnectionSlot = true;
try
{
const int maxAttempts = 2;
for (int attempt = 1; attempt <= maxAttempts; attempt++)
{
try
{
var endpointDescription = CoreClientUtils.SelectEndpoint(serverUrl, false);
var endpointConfiguration = EndpointConfiguration.Create(_config);
var endpoint = new ConfiguredEndpoint(null, endpointDescription, endpointConfiguration);
_session = await Session.Create(
_config,
endpoint,
false,
false,
_config.ApplicationName,
60000,
new UserIdentity(),
null).ConfigureAwait(false);
return _session != null && _session.Connected;
}
catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes.BadMaxConnectionsReached)
{
// 服务器连接数达到上限:做一次短暂退避重试
if (attempt >= maxAttempts)
throw;
await Task.Delay(500 * attempt).ConfigureAwait(false);
}
catch (Exception ex) when (ex.Message != null && ex.Message.Contains("BadMaxConnectionsReached", StringComparison.OrdinalIgnoreCase))
{
if (attempt >= maxAttempts)
throw;
await Task.Delay(500 * attempt).ConfigureAwait(false);
}
}
return false;
}
catch (Exception ex)
{
// 连接失败:释放名额
ReleaseUaConnectionSlotIfHeld();
throw new InvalidOperationException($"连接到 OPC UA 服务器失败: {ex.Message}");
}
}
public async Task DisconnectAsync()
{
var session = _session;
_session = null;
if (session == null)
{
ReleaseUaConnectionSlotIfHeld();
return;
}
try
{
// 断开时可能会卡在 Close()/Dispose()(网络/通道问题),放到线程池并加超时保护
var closeTask = Task.Run(() =>
{
try
{
if (session.Connected)
{
// 使用带超时的 Close毫秒
session.Close((int)DefaultDisconnectTimeout.TotalMilliseconds);
}
}
finally
{
session.Dispose();
}
});
var finished = await Task.WhenAny(closeTask, Task.Delay(DefaultDisconnectTimeout)).ConfigureAwait(false);
if (finished != closeTask)
{
// 超时:不阻塞调用方,避免采集线程被永久卡死
// closeTask 仍可能在后台继续清理(或被底层卡住)
return;
}
// 传播 closeTask 的异常(如果有)
await closeTask.ConfigureAwait(false);
}
catch
{
// 断开失败不应影响上层循环
//(上层会记录日志/继续)
}
finally
{
ReleaseUaConnectionSlotIfHeld();
}
}
private void ReleaseUaConnectionSlotIfHeld()
{
if (_holdsUaConnectionSlot)
{
_holdsUaConnectionSlot = false;
try { UaConnectionSemaphore.Release(); } catch { /* ignore */ }
}
}
public async Task<List<OpcNode>> ReadNodeAsync(List<string> nodeId)
{
if (_session == null || !_session.Connected)
throw new Exception("未连接到 OPC UA 服务器");
try
{
ReadValueIdCollection nodesToRead = new ReadValueIdCollection();
nodeId.ForEach(x =>
{
ReadValueId nodeToRead = new ReadValueId
{
NodeId = new NodeId(x),
AttributeId = Attributes.Value
};
nodesToRead.Add(nodeToRead);
});
_session.Read(
null,
0,
TimestampsToReturn.Both,
nodesToRead,
out DataValueCollection results,
out DiagnosticInfoCollection diagnosticInfos);
if (results != null && results.Count > 0 && StatusCode.IsGood(results[0].StatusCode))
{
var indexedNodeIds = nodeId.Select((value,index)=>new {Index = index, Value = value});
List<OpcNode> result = new List<OpcNode>();
foreach (var item in indexedNodeIds)
{
var dataValue = results[item.Index];
result.Add(new OpcNode()
{
NodeId = item.Value,
DisplayName = item.Value,
Value = dataValue.Value,
SourceTimestamp = dataValue.SourceTimestamp,
DataType = dataValue.WrappedValue.TypeInfo?.BuiltInType.ToString() ?? "Unknown"
});
}
return result;
}
throw new Exception($"读取节点失败: {StatusCode.LookupSymbolicId((uint)results[0].StatusCode)}");
}
catch (Exception ex)
{
throw new InvalidOperationException($"OPC UA 读取节点时出错:{ex.Message}");
}
}
public async Task WriteNodeAsync(string nodeId, object value)
{
if (_session == null || !_session.Connected)
throw new Exception("未连接到 OPC UA 服务器");
try
{
WriteValueCollection nodesToWrite = new WriteValueCollection();
WriteValue nodeToWrite = new WriteValue
{
NodeId = new NodeId(nodeId),
AttributeId = Attributes.Value,
Value = new DataValue(new Variant(value))
};
nodesToWrite.Add(nodeToWrite);
_session.Write(
null,
nodesToWrite,
out StatusCodeCollection results,
out DiagnosticInfoCollection diagnosticInfos);
if (results == null || results.Count == 0 || !StatusCode.IsGood(results[0]))
{
throw new Exception($"写入节点失败: {StatusCode.LookupSymbolicId((uint)results[0])}");
}
}
catch (Exception ex)
{
throw new InvalidOperationException($"写入节点时出错:{ex.Message}");
}
}
public async Task<List<OpcNode>> BrowseNodesAsync(string startingNodeId = null)
{
if (_session == null || !_session.Connected)
throw new Exception("未连接到 OPC UA 服务器");
var nodes = new List<OpcNode>();
NodeId startingNode = startingNodeId != null ? new NodeId(startingNodeId) : ObjectIds.ObjectsFolder;
try
{
BrowseDescriptionCollection nodesToBrowse = new BrowseDescriptionCollection();
BrowseDescription nodeToBrowse = new BrowseDescription
{
NodeId = startingNode,
BrowseDirection = BrowseDirection.Forward,
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
IncludeSubtypes = true,
NodeClassMask = (uint)(NodeClass.Variable | NodeClass.Object),
ResultMask = (uint)BrowseResultMask.All
};
nodesToBrowse.Add(nodeToBrowse);
_session.Browse(
null,
null,
0,
nodesToBrowse,
out BrowseResultCollection results,
out DiagnosticInfoCollection diagnosticInfos);
if (results != null && results.Count > 0)
{
foreach (var reference in results[0].References)
{
nodes.Add(new OpcNode
{
NodeId = reference.NodeId.ToString(),
DisplayName = reference.DisplayName.Text,
DataType = reference.NodeClass.ToString()
});
}
}
return nodes;
}
catch (Exception ex)
{
Console.WriteLine($"浏览节点时出错: {ex.Message}");
throw;
}
}
public void Dispose()
{
if (!_disposed)
{
try
{
// Dispose 里不要 .Wait() 无限阻塞;最多等待一次默认超时
DisconnectAsync().GetAwaiter().GetResult();
}
catch
{
// swallow
}
_disposed = true;
}
}
}