完成OpcUaService服务
This commit is contained in:
@@ -1,62 +1,24 @@
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using DMS.Infrastructure.Models;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DMS.Infrastructure.Interfaces.Services
|
||||
{
|
||||
public interface IOpcUaService
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// 连接到 OPC UA 服务器(异步)
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken">取消令牌</param>
|
||||
/// <returns></returns>
|
||||
public Task ConnectAsync(string opcUaServerUrl,CancellationToken stoppingToken = default);
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 断开 OPC UA 服务器连接
|
||||
/// </summary>
|
||||
public void Disconnect();
|
||||
|
||||
/// <summary>
|
||||
/// 添加订阅
|
||||
/// </summary>
|
||||
/// <param name="subscriptionName">订阅名称</param>
|
||||
/// <returns>创建的订阅</returns>
|
||||
public Subscription AddSubscription(string subscriptionName);
|
||||
|
||||
/// <summary>
|
||||
/// 浏览节点
|
||||
/// </summary>
|
||||
/// <param name="nodeId">起始节点ID</param>
|
||||
/// <returns>节点引用列表</returns>
|
||||
public IList<ReferenceDescription> BrowseNodes(NodeId nodeId);
|
||||
|
||||
/// <summary>
|
||||
/// 读取节点值
|
||||
/// </summary>
|
||||
/// <param name="nodeId">节点ID</param>
|
||||
/// <returns>节点值</returns>
|
||||
public DataValue ReadValue(NodeId nodeId);
|
||||
|
||||
/// <summary>
|
||||
/// 写入节点值
|
||||
/// </summary>
|
||||
/// <param name="nodeId">节点ID</param>
|
||||
/// <param name="value">要写入的值</param>
|
||||
/// <returns>写入结果</returns>
|
||||
public StatusCode WriteValue(NodeId nodeId, object value);
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否已连接
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsConnected();
|
||||
bool IsConnected { get; }
|
||||
Task ConnectAsync(string serviceURL);
|
||||
Task DisconnectAsync();
|
||||
Task<List<OpcUaNode>> BrowseNode(OpcUaNode? nodeToBrowse);
|
||||
void SubscribeToNode(OpcUaNode node, Action<OpcUaNode> onDataChange, int publishingInterval = 1000, int samplingInterval = 500);
|
||||
void SubscribeToNode(List<OpcUaNode> nodes, Action<OpcUaNode> onDataChange, int publishingInterval = 1000, int samplingInterval = 500);
|
||||
void UnsubscribeFromNode(OpcUaNode node);
|
||||
void UnsubscribeFromNode(List<OpcUaNode> nodes);
|
||||
List<OpcUaNode> GetSubscribedNodes();
|
||||
Task ReadNodeValueAsync(OpcUaNode node);
|
||||
Task ReadNodeValuesAsync(List<OpcUaNode> nodes);
|
||||
Task<bool> WriteNodeValueAsync(OpcUaNode node, object value);
|
||||
Task<bool> WriteNodeValuesAsync(Dictionary<OpcUaNode, object> nodesToWrite);
|
||||
}
|
||||
}
|
||||
49
DMS.Infrastructure/Models/OpcUaNode.cs
Normal file
49
DMS.Infrastructure/Models/OpcUaNode.cs
Normal file
@@ -0,0 +1,49 @@
|
||||
using Opc.Ua;
|
||||
|
||||
namespace DMS.Infrastructure.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 封装OPC UA节点的基本信息。
|
||||
/// </summary>
|
||||
public class OpcUaNode
|
||||
{
|
||||
/// <summary>
|
||||
/// 节点的唯一标识符。
|
||||
/// </summary>
|
||||
public NodeId? NodeId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点的显示名称。
|
||||
/// </summary>
|
||||
public string? DisplayName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点的类型(如对象、变量等)。
|
||||
/// </summary>
|
||||
public NodeClass NodeClass { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 节点的值。仅当节点是变量类型时有效。
|
||||
/// </summary>
|
||||
public object? Value { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父节点
|
||||
/// </summary>
|
||||
public OpcUaNode? ParentNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 子节点列表
|
||||
/// </summary>
|
||||
public List<OpcUaNode> Children { get; set; } = new List<OpcUaNode>();
|
||||
|
||||
/// <summary>
|
||||
/// 返回节点的字符串表示形式。
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
string valueString = Value != null ? $", Value: {Value}" : "";
|
||||
return $"- {DisplayName} ({NodeClass}, {NodeId}{valueString})";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,291 +1,509 @@
|
||||
using DMS.Infrastructure.Interfaces.Services;
|
||||
using DMS.Infrastructure.Helper;
|
||||
using DMS.Infrastructure.Interfaces.Services;
|
||||
using DMS.Infrastructure.Models;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
using Opc.Ua.Configuration;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace DMS.Infrastructure.Services
|
||||
{
|
||||
public class OpcUaService : IOpcUaService
|
||||
{
|
||||
private Session? _session;
|
||||
private readonly ApplicationConfiguration _config;
|
||||
private string _serverUrl;
|
||||
private Session _session;
|
||||
private Subscription _subscription;
|
||||
private readonly Dictionary<NodeId, OpcUaNode> _subscribedNodes = new Dictionary<NodeId, OpcUaNode>();
|
||||
|
||||
/// <summary>
|
||||
/// 创建 OPC UA 会话
|
||||
/// </summary>
|
||||
/// <param name="opcUaServerUrl">OPC UA 服务器地址</param>
|
||||
/// <param name="stoppingToken">取消令牌</param>
|
||||
/// <returns></returns>
|
||||
public async Task CreateSession( CancellationToken stoppingToken = default)
|
||||
public bool IsConnected => _session != null && _session.Connected;
|
||||
|
||||
public OpcUaService()
|
||||
{
|
||||
|
||||
_config = CreateApplicationConfiguration();
|
||||
}
|
||||
|
||||
public async Task ConnectAsync(string serverUrl)
|
||||
{
|
||||
try
|
||||
{
|
||||
_session = await OpcUaHelper.CreateOpcUaSessionAsync(_serverUrl, stoppingToken);
|
||||
_serverUrl = serverUrl.ToUpper();
|
||||
if (_serverUrl.IsNullOrEmpty() || !(_serverUrl.StartsWith("OPC.TCP://")))
|
||||
{
|
||||
throw new Exception($"serverUrl服务器地址无效,serverUrl:{_serverUrl}");
|
||||
}
|
||||
// 验证客户端应用程序配置的有效性。
|
||||
await _config.Validate(ApplicationType.Client);
|
||||
|
||||
// 如果配置为自动接受不受信任的证书,则设置证书验证回调。
|
||||
// 这在开发和测试中很方便,但在生产环境中应谨慎使用。
|
||||
if (_config.SecurityConfiguration.AutoAcceptUntrustedCertificates)
|
||||
{
|
||||
_config.CertificateValidator.CertificateValidation += (s, e) => { e.Accept = e.Error.StatusCode == StatusCodes.BadCertificateUntrusted; };
|
||||
}
|
||||
|
||||
// 创建一个应用程序实例,它代表了客户端应用程序。
|
||||
var application = new ApplicationInstance(_config);
|
||||
|
||||
// 检查应用程序实例证书是否存在且有效,如果不存在则会尝试创建。
|
||||
bool haveAppCertificate = await application.CheckApplicationInstanceCertificate(false, 2048);
|
||||
if (!haveAppCertificate)
|
||||
{
|
||||
throw new Exception("应用程序实例证书无效!");
|
||||
}
|
||||
|
||||
// 从给定的URL发现并选择一个合适的服务器终结点(Endpoint)。
|
||||
var selectedEndpoint = CoreClientUtils.SelectEndpoint(_config, _serverUrl, useSecurity: false);
|
||||
|
||||
// 创建到服务器的会话。会话管理客户端和服务器之间的所有通信。
|
||||
_session = await Session.Create(
|
||||
_config, // 应用程序配置
|
||||
new ConfiguredEndpoint(null, selectedEndpoint, EndpointConfiguration.Create(_config)), // 要连接的已配置端点
|
||||
false, // 不更新服务器端点
|
||||
"OpcUaDemo Session", // 会话名称
|
||||
60000, // 会话超时时间(毫秒)
|
||||
null, // 用户身份验证令牌,此处为匿名
|
||||
null // 首选区域设置
|
||||
);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to create OPC UA session: {ex.Message}", ex);
|
||||
Console.WriteLine($"连接服务器时发生错误: {ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接到 OPC UA 服务器
|
||||
/// </summary>
|
||||
public async Task ConnectAsync(string opcUaServerUrl, CancellationToken stoppingToken = default)
|
||||
public async Task DisconnectAsync()
|
||||
{
|
||||
_serverUrl = opcUaServerUrl;
|
||||
if (string.IsNullOrEmpty(opcUaServerUrl))
|
||||
if (_session != null)
|
||||
{
|
||||
throw new ArgumentException("OPC UA server URL cannot be null or empty.", nameof(opcUaServerUrl));
|
||||
await _session.CloseAsync();
|
||||
_session = null;
|
||||
}
|
||||
if (string.IsNullOrEmpty(_serverUrl))
|
||||
{
|
||||
throw new InvalidOperationException("Server URL is not set. Please call CreateSession first.");
|
||||
}
|
||||
|
||||
// 如果已经连接,直接返回
|
||||
if (_session?.Connected == true)
|
||||
public async Task<List<OpcUaNode>> BrowseNode(OpcUaNode nodeToBrowse)
|
||||
{
|
||||
if (!IsConnected)
|
||||
{
|
||||
throw new InvalidOperationException("会话未连接。请在浏览节点前调用ConnectAsync方法。");
|
||||
}
|
||||
var nodes = new List<OpcUaNode>();
|
||||
try
|
||||
{
|
||||
// 存放浏览结果的集合
|
||||
ReferenceDescriptionCollection references;
|
||||
// 用于处理分页的延续点,如果一次浏览无法返回所有结果,服务器会返回此值
|
||||
byte[] continuationPoint;
|
||||
|
||||
// 调用会话的Browse方法来发现服务器地址空间中的节点
|
||||
_session.Browse(
|
||||
null, // requestHeader: 使用默认值
|
||||
null, // view: 不指定视图,即在整个地址空间中浏览
|
||||
nodeToBrowse.NodeId, // nodeId: 要浏览的起始节点ID
|
||||
0u, // maxResultsToReturn: 0表示返回所有结果(如果服务器支持)
|
||||
BrowseDirection.Forward, // browseDirection: 向前浏览(子节点)
|
||||
ReferenceTypeIds.HierarchicalReferences, // referenceTypeId: 只获取层级引用,这是最常用的引用类型
|
||||
true, // includeSubtypes: 包含子类型
|
||||
(uint)NodeClass.Object | (uint)NodeClass.Variable, // nodeClassMask: 只返回对象和变量类型的节点
|
||||
out continuationPoint, // continuationPoint: 输出参数,用于处理分页
|
||||
out references); // references: 输出参数,浏览到的节点引用集合
|
||||
|
||||
if (references != null)
|
||||
{
|
||||
// 遍历第一次返回的结果
|
||||
foreach (var rd in references)
|
||||
{
|
||||
nodes.Add(new OpcUaNode
|
||||
{
|
||||
ParentNode = nodeToBrowse,
|
||||
NodeId = (NodeId)rd.NodeId,
|
||||
DisplayName = rd.DisplayName.Text,
|
||||
NodeClass = rd.NodeClass
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 如果continuationPoint不为null,说明服务器还有数据未返回,需要循环调用BrowseNext获取
|
||||
while (continuationPoint != null)
|
||||
{
|
||||
// 调用BrowseNext获取下一批数据
|
||||
_session.BrowseNext(null, false, continuationPoint, out continuationPoint, out references);
|
||||
|
||||
if (references != null)
|
||||
{
|
||||
// 遍历后续批次返回的结果
|
||||
foreach (var rd in references)
|
||||
{
|
||||
nodes.Add(new OpcUaNode
|
||||
{
|
||||
ParentNode = nodeToBrowse,
|
||||
NodeId = (NodeId)rd.NodeId,
|
||||
DisplayName = rd.DisplayName.Text,
|
||||
NodeClass = rd.NodeClass
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// 将找到的子节点列表关联到父节点
|
||||
nodeToBrowse.Children = nodes;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"浏览节点 '{nodeToBrowse.DisplayName}' 时发生错误: {ex.Message}");
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
public void SubscribeToNode(OpcUaNode node, Action<OpcUaNode> onDataChange, int publishingInterval = 1000, int samplingInterval = 500)
|
||||
{
|
||||
SubscribeToNode(new List<OpcUaNode> { node }, onDataChange, publishingInterval, samplingInterval);
|
||||
}
|
||||
|
||||
public void SubscribeToNode(List<OpcUaNode> nodes, Action<OpcUaNode> onDataChange, int publishingInterval = 1000, int samplingInterval = 500)
|
||||
{
|
||||
// 检查会话是否已连接
|
||||
if (!IsConnected)
|
||||
{
|
||||
throw new InvalidOperationException("会话未连接。请在订阅节点前调用ConnectAsync方法。");
|
||||
}
|
||||
// 检查节点列表是否有效
|
||||
if (nodes == null || !nodes.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
// 如果还没有订阅对象,则基于会话的默认设置创建一个新的订阅
|
||||
if (_subscription == null)
|
||||
{
|
||||
_subscription = new Subscription(_session.DefaultSubscription)
|
||||
{
|
||||
// 设置服务器向客户端发送通知的速率(毫秒)
|
||||
PublishingInterval = publishingInterval
|
||||
};
|
||||
// 在会话中添加订阅
|
||||
_session.AddSubscription(_subscription);
|
||||
// 在服务器上创建订阅
|
||||
_subscription.Create();
|
||||
}
|
||||
// 如果客户端请求的发布间隔与现有订阅不同,则修改订阅
|
||||
else if (_subscription.PublishingInterval != publishingInterval)
|
||||
{
|
||||
_subscription.PublishingInterval = publishingInterval;
|
||||
}
|
||||
|
||||
// 创建一个用于存放待添加监视项的列表
|
||||
var itemsToAdd = new List<MonitoredItem>();
|
||||
// 遍历所有请求订阅的节点
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
// 如果节点已经存在于我们的跟踪列表中,则跳过,避免重复订阅
|
||||
if (_subscribedNodes.ContainsKey(node.NodeId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// 为每个节点创建一个监视项
|
||||
var monitoredItem = new MonitoredItem(_subscription.DefaultItem)
|
||||
{
|
||||
DisplayName = node.DisplayName,
|
||||
StartNodeId = node.NodeId,
|
||||
AttributeId = Attributes.Value, // 我们关心的是节点的值属性
|
||||
SamplingInterval = samplingInterval // 服务器采样节点值的速率(毫秒)
|
||||
};
|
||||
// 设置数据变化通知的回调函数
|
||||
monitoredItem.Notification += (item, e) =>
|
||||
{
|
||||
// 将通知事件参数转换为MonitoredItemNotification
|
||||
var notification = e.NotificationValue as MonitoredItemNotification;
|
||||
if (notification != null)
|
||||
{
|
||||
// 通过StartNodeId从我们的跟踪字典中找到对应的OpcUaNode对象
|
||||
if (_subscribedNodes.TryGetValue(item.StartNodeId, out var changedNode))
|
||||
{
|
||||
// 更新节点对象的值
|
||||
changedNode.Value = notification.Value.Value;
|
||||
// 调用用户提供的回调函数,并传入更新后的节点
|
||||
onDataChange?.Invoke(changedNode);
|
||||
}
|
||||
}
|
||||
};
|
||||
// 将创建的监视项添加到待添加列表
|
||||
itemsToAdd.Add(monitoredItem);
|
||||
// 将节点添加到我们的跟踪字典中
|
||||
_subscribedNodes[node.NodeId] = node;
|
||||
}
|
||||
|
||||
// 如果有新的监视项要添加
|
||||
if (itemsToAdd.Any())
|
||||
{
|
||||
// 将所有新的监视项批量添加到订阅中
|
||||
_subscription.AddItems(itemsToAdd);
|
||||
// 将所有挂起的更改(包括订阅属性修改和添加新项)应用到服务器
|
||||
_subscription.ApplyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
public void UnsubscribeFromNode(OpcUaNode node)
|
||||
{
|
||||
UnsubscribeFromNode(new List<OpcUaNode> { node });
|
||||
}
|
||||
|
||||
public void UnsubscribeFromNode(List<OpcUaNode> nodes)
|
||||
{
|
||||
// 检查订阅对象和节点列表是否有效
|
||||
if (_subscription == null || nodes == null || !nodes.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 重新创建会话
|
||||
await CreateSession( stoppingToken);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 断开 OPC UA 服务器连接
|
||||
/// </summary>
|
||||
public void Disconnect()
|
||||
var itemsToRemove = new List<MonitoredItem>();
|
||||
// 遍历所有请求取消订阅的节点
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (_session != null)
|
||||
// 在当前订阅中查找与节点ID匹配的监视项
|
||||
var item = _subscription.MonitoredItems.FirstOrDefault(m => m.StartNodeId.Equals(node.NodeId));
|
||||
if (item != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
_session.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 记录日志但不抛出异常,确保清理工作完成
|
||||
System.Diagnostics.Debug.WriteLine($"Error closing OPC UA session: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_session = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 添加订阅
|
||||
/// </summary>
|
||||
/// <param name="subscriptionName">订阅名称</param>
|
||||
/// <returns>创建的订阅</returns>
|
||||
public Subscription AddSubscription(string subscriptionName)
|
||||
{
|
||||
if (_session == null)
|
||||
{
|
||||
throw new InvalidOperationException("Session is not created. Please call CreateSession first.");
|
||||
}
|
||||
|
||||
if (!_session.Connected)
|
||||
{
|
||||
throw new InvalidOperationException("Session is not connected. Please call Connect first.");
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(subscriptionName))
|
||||
{
|
||||
throw new ArgumentException("Subscription name cannot be null or empty.", nameof(subscriptionName));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var subscription = new Subscription(_session.DefaultSubscription)
|
||||
{
|
||||
DisplayName = subscriptionName,
|
||||
PublishingInterval = 1000,
|
||||
LifetimeCount = 0,
|
||||
MaxNotificationsPerPublish = 0,
|
||||
PublishingEnabled = true,
|
||||
Priority = 0
|
||||
};
|
||||
|
||||
_session.AddSubscription(subscription);
|
||||
subscription.Create();
|
||||
|
||||
return subscription;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to add subscription '{subscriptionName}': {ex.Message}", ex);
|
||||
// 如果找到,则添加到待移除列表
|
||||
itemsToRemove.Add(item);
|
||||
// 从我们的跟踪字典中移除该节点
|
||||
_subscribedNodes.Remove(node.NodeId);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 浏览节点
|
||||
/// </summary>
|
||||
/// <param name="nodeId">起始节点ID</param>
|
||||
/// <returns>节点引用列表</returns>
|
||||
public IList<ReferenceDescription> BrowseNodes(NodeId nodeId)
|
||||
// 如果有需要移除的监视项
|
||||
if (itemsToRemove.Any())
|
||||
{
|
||||
if (_session == null)
|
||||
{
|
||||
throw new InvalidOperationException("Session is not created. Please call CreateSession first.");
|
||||
}
|
||||
|
||||
if (!_session.Connected)
|
||||
{
|
||||
throw new InvalidOperationException("Session is not connected. Please call Connect first.");
|
||||
}
|
||||
|
||||
if (nodeId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nodeId));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 使用会话的浏览方法
|
||||
var response = _session.Browse(
|
||||
null,
|
||||
null,
|
||||
nodeId,
|
||||
0u,
|
||||
BrowseDirection.Forward,
|
||||
ReferenceTypeIds.HierarchicalReferences,
|
||||
true,
|
||||
(uint)NodeClass.Unspecified,
|
||||
out var continuationPoint,
|
||||
out var references);
|
||||
|
||||
return references;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to browse nodes: {ex.Message}", ex);
|
||||
// 从订阅中批量移除监视项
|
||||
_subscription.RemoveItems(itemsToRemove);
|
||||
// 将更改应用到服务器
|
||||
_subscription.ApplyChanges();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取节点值
|
||||
/// </summary>
|
||||
/// <param name="nodeId">节点ID</param>
|
||||
/// <returns>节点值</returns>
|
||||
public DataValue ReadValue(NodeId nodeId)
|
||||
public List<OpcUaNode> GetSubscribedNodes()
|
||||
{
|
||||
if (_session == null)
|
||||
{
|
||||
throw new InvalidOperationException("Session is not created. Please call CreateSession first.");
|
||||
return _subscribedNodes.Values.ToList();
|
||||
}
|
||||
|
||||
if (!_session.Connected)
|
||||
public Task ReadNodeValueAsync(OpcUaNode node)
|
||||
{
|
||||
throw new InvalidOperationException("Session is not connected. Please call Connect first.");
|
||||
return ReadNodeValuesAsync(new List<OpcUaNode> { node });
|
||||
}
|
||||
|
||||
if (nodeId == null)
|
||||
public async Task ReadNodeValuesAsync(List<OpcUaNode> nodes)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nodeId));
|
||||
if (!IsConnected || nodes == null || !nodes.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
// 创建一个用于存放读取请求的集合
|
||||
var nodesToRead = new ReadValueIdCollection();
|
||||
// 创建一个列表,用于在收到响应后按顺序查找对应的OpcUaNode
|
||||
var nodeListForLookup = new List<OpcUaNode>();
|
||||
// 遍历所有请求读取的节点
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
// 创建读取值集合
|
||||
var nodesToRead = new ReadValueIdCollection
|
||||
// 只处理变量类型的节点,因为只有变量才有值
|
||||
if (node.NodeClass == NodeClass.Variable)
|
||||
{
|
||||
new ReadValueId
|
||||
// 创建一个ReadValueId,指定要读取的节点ID和属性(值)
|
||||
nodesToRead.Add(new ReadValueId
|
||||
{
|
||||
NodeId = nodeId,
|
||||
NodeId = node.NodeId,
|
||||
AttributeId = Attributes.Value
|
||||
}
|
||||
};
|
||||
|
||||
// 执行读取操作
|
||||
_session.Read(
|
||||
null,
|
||||
0,
|
||||
TimestampsToReturn.Neither,
|
||||
nodesToRead,
|
||||
out var results,
|
||||
out var diagnosticInfos);
|
||||
|
||||
return results[0];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to read value from node '{nodeId}': {ex.Message}", ex);
|
||||
});
|
||||
// 将节点添加到查找列表中
|
||||
nodeListForLookup.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入节点值
|
||||
/// </summary>
|
||||
/// <param name="nodeId">节点ID</param>
|
||||
/// <param name="value">要写入的值</param>
|
||||
/// <returns>写入结果</returns>
|
||||
public StatusCode WriteValue(NodeId nodeId, object value)
|
||||
// 如果没有需要读取的变量节点,则直接返回
|
||||
if (nodesToRead.Count == 0)
|
||||
{
|
||||
if (_session == null)
|
||||
{
|
||||
throw new InvalidOperationException("Session is not created. Please call CreateSession first.");
|
||||
}
|
||||
|
||||
if (!_session.Connected)
|
||||
{
|
||||
throw new InvalidOperationException("Session is not connected. Please call Connect first.");
|
||||
}
|
||||
|
||||
if (nodeId == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(nodeId));
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 创建写入值集合
|
||||
var nodesToWrite = new WriteValueCollection
|
||||
// 异步调用Read方法,批量读取所有节点的值
|
||||
var response = await _session.ReadAsync(
|
||||
null, // RequestHeader, 使用默认值
|
||||
0, // maxAge, 0表示直接从设备读取最新值,而不是从缓存读取
|
||||
TimestampsToReturn.Neither, // TimestampsToReturn, 表示我们不关心值的时间戳
|
||||
nodesToRead, // ReadValueIdCollection, 要读取的节点和属性的集合
|
||||
default // CancellationToken
|
||||
);
|
||||
|
||||
// 获取响应中的结果和诊断信息
|
||||
var results = response.Results;
|
||||
var diagnosticInfos = response.DiagnosticInfos;
|
||||
|
||||
// 验证响应,确保请求成功
|
||||
ClientBase.ValidateResponse(results, nodesToRead);
|
||||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
|
||||
|
||||
// 遍历返回的结果
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
new WriteValue
|
||||
// 根据索引找到对应的OpcUaNode
|
||||
var node = nodeListForLookup[i];
|
||||
// 检查状态码,确保读取成功
|
||||
if (StatusCode.IsGood(results[i].StatusCode))
|
||||
{
|
||||
NodeId = nodeId,
|
||||
AttributeId = Attributes.Value,
|
||||
Value = new DataValue(new Variant(value))
|
||||
// 更新节点的值
|
||||
node.Value = results[i].Value;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 如果读取失败,则将状态码作为值,方便调试
|
||||
node.Value = $"({results[i].StatusCode})";
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 执行写入操作
|
||||
_session.Write(
|
||||
null,
|
||||
nodesToWrite,
|
||||
out var results,
|
||||
out var diagnosticInfos);
|
||||
|
||||
return results[0];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to write value to node '{nodeId}': {ex.Message}", ex);
|
||||
Console.WriteLine($"读取节点值时发生错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否已连接
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsConnected()
|
||||
public Task<bool> WriteNodeValueAsync(OpcUaNode node, object value)
|
||||
{
|
||||
return _session?.Connected == true;
|
||||
var nodesToWrite = new Dictionary<OpcUaNode, object> { { node, value } };
|
||||
return WriteNodeValuesAsync(nodesToWrite);
|
||||
}
|
||||
|
||||
public async Task<bool> WriteNodeValuesAsync(Dictionary<OpcUaNode, object> nodesToWrite)
|
||||
{
|
||||
// 检查会话是否连接,以及待写入的节点字典是否有效
|
||||
if (!IsConnected || nodesToWrite == null || !nodesToWrite.Any())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// 创建一个用于存放写入请求的集合
|
||||
var writeValues = new WriteValueCollection();
|
||||
// 创建一个列表,用于在收到响应后按顺序查找对应的OpcUaNode,以进行错误报告
|
||||
var nodeListForLookup = new List<OpcUaNode>();
|
||||
|
||||
// 遍历所有请求写入的节点和值
|
||||
foreach (var entry in nodesToWrite)
|
||||
{
|
||||
var node = entry.Key;
|
||||
var value = entry.Value;
|
||||
|
||||
// 只能向变量类型的节点写入值
|
||||
if (node.NodeClass != NodeClass.Variable)
|
||||
{
|
||||
Console.WriteLine($"节点 '{node.DisplayName}' 不是变量类型,无法写入。");
|
||||
continue; // 跳过非变量节点
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 创建一个WriteValue对象,它封装了写入操作的所有信息
|
||||
var writeValue = new WriteValue
|
||||
{
|
||||
NodeId = node.NodeId, // 指定要写入的节点ID
|
||||
AttributeId = Attributes.Value, // 指定要写入的是节点的Value属性
|
||||
// 将要写入的值封装在DataValue和Variant中
|
||||
// Variant可以处理各种数据类型
|
||||
Value = new DataValue(new Variant(value))
|
||||
};
|
||||
writeValues.Add(writeValue);
|
||||
// 将节点添加到查找列表中,保持与writeValues集合的顺序一致
|
||||
nodeListForLookup.Add(node);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 处理在创建写入值时可能发生的异常(例如,值类型不兼容)
|
||||
Console.WriteLine($"为节点 '{node.DisplayName}' 创建写入值时发生错误: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有有效的写入请求,则直接返回
|
||||
if (writeValues.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// 异步调用Write方法,将所有写入请求批量发送到服务器
|
||||
var response = await _session.WriteAsync(
|
||||
null, // RequestHeader, 使用默认值
|
||||
writeValues, // WriteValueCollection, 要写入的节点和值的集合
|
||||
default // CancellationToken
|
||||
);
|
||||
|
||||
// 获取响应中的结果和诊断信息
|
||||
var results = response.Results;
|
||||
var diagnosticInfos = response.DiagnosticInfos;
|
||||
|
||||
// 验证响应,确保请求被服务器正确处理
|
||||
ClientBase.ValidateResponse(results, writeValues);
|
||||
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, writeValues);
|
||||
|
||||
bool allSuccess = true;
|
||||
// 遍历返回的结果状态码
|
||||
for (int i = 0; i < results.Count; i++)
|
||||
{
|
||||
// 如果返回的状态码表示失败
|
||||
if (StatusCode.IsBad(results[i]))
|
||||
{
|
||||
allSuccess = false;
|
||||
// 根据索引找到写入失败的节点
|
||||
var failedNode = nodeListForLookup[i];
|
||||
Console.WriteLine($"写入节点 '{failedNode.DisplayName}' 失败: {results[i]}");
|
||||
}
|
||||
}
|
||||
|
||||
return allSuccess;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.WriteLine($"写入节点值时发生错误: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private ApplicationConfiguration CreateApplicationConfiguration()
|
||||
{
|
||||
return new ApplicationConfiguration()
|
||||
{
|
||||
// 应用程序的名称,会显示在服务器端
|
||||
ApplicationName = "OpcUaDemoClient",
|
||||
// 应用程序的唯一标识符URI
|
||||
ApplicationUri = Utils.Format("urn:{0}:OpcUaDemoClient", System.Net.Dns.GetHostName()),
|
||||
// 应用程序类型为客户端
|
||||
ApplicationType = ApplicationType.Client,
|
||||
// 安全相关的配置
|
||||
SecurityConfiguration = new SecurityConfiguration
|
||||
{
|
||||
// 应用程序实例证书的存储位置和主题名称
|
||||
ApplicationCertificate = new CertificateIdentifier { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\MachineDefault", SubjectName = "OpcUaDemoClient" },
|
||||
// 受信任的证书颁发机构的证书存储
|
||||
TrustedIssuerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Certificate Authorities" },
|
||||
// 受信任的对等(服务器)证书的存储
|
||||
TrustedPeerCertificates = new CertificateTrustList { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\UA Applications" },
|
||||
// 被拒绝的证书的存储
|
||||
RejectedCertificateStore = new CertificateStoreIdentifier { StoreType = @"Directory", StorePath = @"%CommonApplicationData%\OPC Foundation\CertificateStores\RejectedCertificates" },
|
||||
// 自动接受不受信任的证书(仅建议在开发环境中使用)
|
||||
AutoAcceptUntrustedCertificates = true
|
||||
},
|
||||
// 传输配置(例如,缓冲区大小)
|
||||
TransportConfigurations = new TransportConfigurationCollection(),
|
||||
// 传输配额(例如,操作超时时间)
|
||||
TransportQuotas = new TransportQuotas { OperationTimeout = 15000 },
|
||||
// 客户端特定的配置(例如,默认会话超时)
|
||||
ClientConfiguration = new ClientConfiguration { DefaultSessionTimeout = 60000 },
|
||||
// 跟踪和日志记录的配置
|
||||
TraceConfiguration = new TraceConfiguration()
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ public partial class App : System.Windows.Application
|
||||
services.AddSingleton<IRepositoryManager, RepositoryManager>();
|
||||
services.AddSingleton<IExcelService, ExcelService>();
|
||||
|
||||
services.AddSingleton<IOpcUaService, OpcUaService>();
|
||||
services.AddTransient<IOpcUaService, OpcUaService>();
|
||||
|
||||
|
||||
// 注册App服务
|
||||
|
||||
@@ -3,6 +3,7 @@ using CommunityToolkit.Mvvm.Input;
|
||||
using DMS.Core.Models;
|
||||
using DMS.Helper;
|
||||
using DMS.Infrastructure.Interfaces.Services;
|
||||
using DMS.Infrastructure.Models;
|
||||
using DMS.WPF.ViewModels.Items;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Client;
|
||||
@@ -15,8 +16,8 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
|
||||
[ObservableProperty]
|
||||
private string _endpointUrl = "opc.tcp://127.0.0.1:4855"; // 默认值
|
||||
|
||||
//[ObservableProperty]
|
||||
//private ObservableCollection<OpcUaNode> _opcUaNodes;
|
||||
[ObservableProperty]
|
||||
private OpcUaNode _rootOpcUaNode;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<Variable> _selectedNodeVariables;
|
||||
@@ -46,7 +47,7 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
|
||||
//OpcUaNodes = new ObservableCollection<OpcUaNode>();
|
||||
SelectedNodeVariables = new ObservableCollection<Variable>();
|
||||
this._opcUaService = opcUaService;
|
||||
|
||||
RootOpcUaNode = new OpcUaNode() { DisplayName = "根节点", NodeId = Objects.ObjectsFolder };
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
}
|
||||
@@ -57,13 +58,12 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
|
||||
try
|
||||
{
|
||||
// 断开现有连接
|
||||
if (!_opcUaService.IsConnected())
|
||||
if (!_opcUaService.IsConnected)
|
||||
{
|
||||
await _opcUaService.ConnectAsync(EndpointUrl, _cancellationTokenSource.Token);
|
||||
await _opcUaService.ConnectAsync(EndpointUrl);
|
||||
}
|
||||
|
||||
IsConnected= _opcUaService.IsConnected();
|
||||
if (IsConnected)
|
||||
if (_opcUaService.IsConnected)
|
||||
{
|
||||
ConnectButtonText = "已连接";
|
||||
IsConnectButtonEnabled = false;
|
||||
@@ -71,9 +71,10 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase<List<Varia
|
||||
|
||||
|
||||
// 浏览根节点
|
||||
var rootNodeId = new NodeId(ObjectIds.ObjectsFolder);
|
||||
var list=_opcUaService.BrowseNodes(rootNodeId);
|
||||
//await BrowseNodes(OpcUaNodes, ObjectIds.ObjectsFolder);
|
||||
|
||||
await _opcUaService.BrowseNode(RootOpcUaNode);
|
||||
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
50
DMS.WPF/ViewModels/Items/OpcUaNodeViewModel.cs
Normal file
50
DMS.WPF/ViewModels/Items/OpcUaNodeViewModel.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Opc.Ua;
|
||||
using System.Collections.ObjectModel;
|
||||
|
||||
namespace DMS.WPF.ViewModels.Items
|
||||
{
|
||||
public partial class OpcUaNodeViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _displayName;
|
||||
|
||||
[ObservableProperty]
|
||||
private NodeId _nodeId;
|
||||
|
||||
[ObservableProperty]
|
||||
private NodeType _nodeType;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isExpanded;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoading;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isLoaded;
|
||||
|
||||
public ObservableCollection<OpcUaNodeViewModel> Children { get; set; }
|
||||
|
||||
public OpcUaNodeViewModel(string displayName, NodeId nodeId, NodeType nodeType)
|
||||
{
|
||||
DisplayName = displayName;
|
||||
NodeId = nodeId;
|
||||
NodeType = nodeType;
|
||||
Children = new ObservableCollection<OpcUaNodeViewModel>();
|
||||
|
||||
// 如果是文件夹或对象,添加一个虚拟子节点,用于懒加载
|
||||
if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
|
||||
{
|
||||
Children.Add(new OpcUaNodeViewModel("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public enum NodeType
|
||||
{
|
||||
Folder,
|
||||
Object,
|
||||
Variable
|
||||
}
|
||||
}
|
||||
@@ -50,7 +50,7 @@
|
||||
Grid.Row="1"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,10,0"
|
||||
ItemsSource="{Binding OpcUaNodes}"
|
||||
ItemsSource="{Binding RootOpcUaNode.Children}"
|
||||
SelectedItemChanged="TreeView_SelectedItemChanged">
|
||||
<TreeView.Style>
|
||||
<Style BasedOn="{StaticResource {x:Type TreeView}}" TargetType="TreeView">
|
||||
|
||||
Reference in New Issue
Block a user