diff --git a/DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs b/DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs
index b365ba3..05e9f6e 100644
--- a/DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs
+++ b/DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs
@@ -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
{
-
- ///
- /// 连接到 OPC UA 服务器(异步)
- ///
- /// 取消令牌
- ///
- public Task ConnectAsync(string opcUaServerUrl,CancellationToken stoppingToken = default);
-
-
-
- ///
- /// 断开 OPC UA 服务器连接
- ///
- public void Disconnect();
-
- ///
- /// 添加订阅
- ///
- /// 订阅名称
- /// 创建的订阅
- public Subscription AddSubscription(string subscriptionName);
-
- ///
- /// 浏览节点
- ///
- /// 起始节点ID
- /// 节点引用列表
- public IList BrowseNodes(NodeId nodeId);
-
- ///
- /// 读取节点值
- ///
- /// 节点ID
- /// 节点值
- public DataValue ReadValue(NodeId nodeId);
-
- ///
- /// 写入节点值
- ///
- /// 节点ID
- /// 要写入的值
- /// 写入结果
- public StatusCode WriteValue(NodeId nodeId, object value);
-
- ///
- /// 检查是否已连接
- ///
- ///
- public bool IsConnected();
+ bool IsConnected { get; }
+ Task ConnectAsync(string serviceURL);
+ Task DisconnectAsync();
+ Task> BrowseNode(OpcUaNode? nodeToBrowse);
+ void SubscribeToNode(OpcUaNode node, Action onDataChange, int publishingInterval = 1000, int samplingInterval = 500);
+ void SubscribeToNode(List nodes, Action onDataChange, int publishingInterval = 1000, int samplingInterval = 500);
+ void UnsubscribeFromNode(OpcUaNode node);
+ void UnsubscribeFromNode(List nodes);
+ List GetSubscribedNodes();
+ Task ReadNodeValueAsync(OpcUaNode node);
+ Task ReadNodeValuesAsync(List nodes);
+ Task WriteNodeValueAsync(OpcUaNode node, object value);
+ Task WriteNodeValuesAsync(Dictionary nodesToWrite);
}
-}
\ No newline at end of file
+}
diff --git a/DMS.Infrastructure/Models/OpcUaNode.cs b/DMS.Infrastructure/Models/OpcUaNode.cs
new file mode 100644
index 0000000..c79079c
--- /dev/null
+++ b/DMS.Infrastructure/Models/OpcUaNode.cs
@@ -0,0 +1,49 @@
+using Opc.Ua;
+
+namespace DMS.Infrastructure.Models
+{
+ ///
+ /// 封装OPC UA节点的基本信息。
+ ///
+ public class OpcUaNode
+ {
+ ///
+ /// 节点的唯一标识符。
+ ///
+ public NodeId? NodeId { get; set; }
+
+ ///
+ /// 节点的显示名称。
+ ///
+ public string? DisplayName { get; set; }
+
+ ///
+ /// 节点的类型(如对象、变量等)。
+ ///
+ public NodeClass NodeClass { get; set; }
+
+ ///
+ /// 节点的值。仅当节点是变量类型时有效。
+ ///
+ public object? Value { get; set; }
+
+ ///
+ /// 父节点
+ ///
+ public OpcUaNode? ParentNode { get; set; }
+
+ ///
+ /// 子节点列表
+ ///
+ public List Children { get; set; } = new List();
+
+ ///
+ /// 返回节点的字符串表示形式。
+ ///
+ public override string ToString()
+ {
+ string valueString = Value != null ? $", Value: {Value}" : "";
+ return $"- {DisplayName} ({NodeClass}, {NodeId}{valueString})";
+ }
+ }
+}
\ No newline at end of file
diff --git a/DMS.Infrastructure/Services/OpcUaService.cs b/DMS.Infrastructure/Services/OpcUaService.cs
index 8f95a9d..d3f193b 100644
--- a/DMS.Infrastructure/Services/OpcUaService.cs
+++ b/DMS.Infrastructure/Services/OpcUaService.cs
@@ -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 string _serverUrl;
+ private readonly ApplicationConfiguration _config;
+ private string _serverUrl;
+ private Session _session;
+ private Subscription _subscription;
+ private readonly Dictionary _subscribedNodes = new Dictionary();
- ///
- /// 创建 OPC UA 会话
- ///
- /// OPC UA 服务器地址
- /// 取消令牌
- ///
- 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;
}
}
- ///
- /// 连接到 OPC UA 服务器
- ///
- 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))
+ }
+
+ public async Task> BrowseNode(OpcUaNode nodeToBrowse)
+ {
+ if (!IsConnected)
{
- throw new InvalidOperationException("Server URL is not set. Please call CreateSession first.");
+ throw new InvalidOperationException("会话未连接。请在浏览节点前调用ConnectAsync方法。");
}
-
- // 如果已经连接,直接返回
- if (_session?.Connected == true)
+ var nodes = new List();
+ 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 onDataChange, int publishingInterval = 1000, int samplingInterval = 500)
+ {
+ SubscribeToNode(new List { node }, onDataChange, publishingInterval, samplingInterval);
+ }
+
+ public void SubscribeToNode(List nodes, Action 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();
+ // 遍历所有请求订阅的节点
+ 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 { node });
+ }
+
+ public void UnsubscribeFromNode(List nodes)
+ {
+ // 检查订阅对象和节点列表是否有效
+ if (_subscription == null || nodes == null || !nodes.Any())
{
return;
}
- // 重新创建会话
- await CreateSession( stoppingToken);
+ var itemsToRemove = new List();
+ // 遍历所有请求取消订阅的节点
+ foreach (var node in nodes)
+ {
+ // 在当前订阅中查找与节点ID匹配的监视项
+ var item = _subscription.MonitoredItems.FirstOrDefault(m => m.StartNodeId.Equals(node.NodeId));
+ if (item != null)
+ {
+ // 如果找到,则添加到待移除列表
+ itemsToRemove.Add(item);
+ // 从我们的跟踪字典中移除该节点
+ _subscribedNodes.Remove(node.NodeId);
+ }
+ }
+
+ // 如果有需要移除的监视项
+ if (itemsToRemove.Any())
+ {
+ // 从订阅中批量移除监视项
+ _subscription.RemoveItems(itemsToRemove);
+ // 将更改应用到服务器
+ _subscription.ApplyChanges();
+ }
}
-
-
- ///
- /// 断开 OPC UA 服务器连接
- ///
- public void Disconnect()
+ public List GetSubscribedNodes()
{
- if (_session != null)
+ return _subscribedNodes.Values.ToList();
+ }
+
+ public Task ReadNodeValueAsync(OpcUaNode node)
+ {
+ return ReadNodeValuesAsync(new List { node });
+ }
+
+ public async Task ReadNodeValuesAsync(List nodes)
+ {
+ if (!IsConnected || nodes == null || !nodes.Any())
{
+ return;
+ }
+
+ // 创建一个用于存放读取请求的集合
+ var nodesToRead = new ReadValueIdCollection();
+ // 创建一个列表,用于在收到响应后按顺序查找对应的OpcUaNode
+ var nodeListForLookup = new List();
+ // 遍历所有请求读取的节点
+ foreach (var node in nodes)
+ {
+ // 只处理变量类型的节点,因为只有变量才有值
+ if (node.NodeClass == NodeClass.Variable)
+ {
+ // 创建一个ReadValueId,指定要读取的节点ID和属性(值)
+ nodesToRead.Add(new ReadValueId
+ {
+ NodeId = node.NodeId,
+ AttributeId = Attributes.Value
+ });
+ // 将节点添加到查找列表中
+ nodeListForLookup.Add(node);
+ }
+ }
+
+ // 如果没有需要读取的变量节点,则直接返回
+ if (nodesToRead.Count == 0)
+ {
+ return;
+ }
+
+ try
+ {
+ // 异步调用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++)
+ {
+ // 根据索引找到对应的OpcUaNode
+ var node = nodeListForLookup[i];
+ // 检查状态码,确保读取成功
+ if (StatusCode.IsGood(results[i].StatusCode))
+ {
+ // 更新节点的值
+ node.Value = results[i].Value;
+ }
+ else
+ {
+ // 如果读取失败,则将状态码作为值,方便调试
+ node.Value = $"({results[i].StatusCode})";
+ }
+ }
+ }
+ catch (Exception ex)
+ {
+ Console.WriteLine($"读取节点值时发生错误: {ex.Message}");
+ }
+ }
+
+ public Task WriteNodeValueAsync(OpcUaNode node, object value)
+ {
+ var nodesToWrite = new Dictionary { { node, value } };
+ return WriteNodeValuesAsync(nodesToWrite);
+ }
+
+ public async Task WriteNodeValuesAsync(Dictionary nodesToWrite)
+ {
+ // 检查会话是否连接,以及待写入的节点字典是否有效
+ if (!IsConnected || nodesToWrite == null || !nodesToWrite.Any())
+ {
+ return false;
+ }
+
+ // 创建一个用于存放写入请求的集合
+ var writeValues = new WriteValueCollection();
+ // 创建一个列表,用于在收到响应后按顺序查找对应的OpcUaNode,以进行错误报告
+ var nodeListForLookup = new List();
+
+ // 遍历所有请求写入的节点和值
+ foreach (var entry in nodesToWrite)
+ {
+ var node = entry.Key;
+ var value = entry.Value;
+
+ // 只能向变量类型的节点写入值
+ if (node.NodeClass != NodeClass.Variable)
+ {
+ Console.WriteLine($"节点 '{node.DisplayName}' 不是变量类型,无法写入。");
+ continue; // 跳过非变量节点
+ }
+
try
{
- _session.Close();
+ // 创建一个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)
{
- // 记录日志但不抛出异常,确保清理工作完成
- System.Diagnostics.Debug.WriteLine($"Error closing OPC UA session: {ex.Message}");
- }
- finally
- {
- _session = null;
+ // 处理在创建写入值时可能发生的异常(例如,值类型不兼容)
+ Console.WriteLine($"为节点 '{node.DisplayName}' 创建写入值时发生错误: {ex.Message}");
}
}
- }
- ///
- /// 添加订阅
- ///
- /// 订阅名称
- /// 创建的订阅
- public Subscription AddSubscription(string subscriptionName)
- {
- if (_session == null)
+ // 如果没有有效的写入请求,则直接返回
+ if (writeValues.Count == 0)
{
- 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));
+ return false;
}
try
{
- var subscription = new Subscription(_session.DefaultSubscription)
+ // 异步调用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++)
{
- 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);
- }
- }
-
- ///
- /// 浏览节点
- ///
- /// 起始节点ID
- /// 节点引用列表
- public IList BrowseNodes(NodeId nodeId)
- {
- 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);
- }
- }
-
- ///
- /// 读取节点值
- ///
- /// 节点ID
- /// 节点值
- public DataValue ReadValue(NodeId nodeId)
- {
- 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 nodesToRead = new ReadValueIdCollection
- {
- new ReadValueId
+ // 如果返回的状态码表示失败
+ if (StatusCode.IsBad(results[i]))
{
- NodeId = nodeId,
- AttributeId = Attributes.Value
+ allSuccess = false;
+ // 根据索引找到写入失败的节点
+ var failedNode = nodeListForLookup[i];
+ Console.WriteLine($"写入节点 '{failedNode.DisplayName}' 失败: {results[i]}");
}
- };
+ }
- // 执行读取操作
- _session.Read(
- null,
- 0,
- TimestampsToReturn.Neither,
- nodesToRead,
- out var results,
- out var diagnosticInfos);
-
- return results[0];
+ return allSuccess;
}
catch (Exception ex)
{
- throw new InvalidOperationException($"Failed to read value from node '{nodeId}': {ex.Message}", ex);
+ Console.WriteLine($"写入节点值时发生错误: {ex.Message}");
+ return false;
}
}
- ///
- /// 写入节点值
- ///
- /// 节点ID
- /// 要写入的值
- /// 写入结果
- public StatusCode WriteValue(NodeId nodeId, object value)
+ private ApplicationConfiguration CreateApplicationConfiguration()
{
- if (_session == null)
+ return new ApplicationConfiguration()
{
- 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 nodesToWrite = new WriteValueCollection
+ // 应用程序的名称,会显示在服务器端
+ ApplicationName = "OpcUaDemoClient",
+ // 应用程序的唯一标识符URI
+ ApplicationUri = Utils.Format("urn:{0}:OpcUaDemoClient", System.Net.Dns.GetHostName()),
+ // 应用程序类型为客户端
+ ApplicationType = ApplicationType.Client,
+ // 安全相关的配置
+ SecurityConfiguration = new SecurityConfiguration
{
- new WriteValue
- {
- NodeId = nodeId,
- AttributeId = Attributes.Value,
- Value = new DataValue(new Variant(value))
- }
- };
-
- // 执行写入操作
- _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);
- }
- }
-
- ///
- /// 检查是否已连接
- ///
- ///
- public bool IsConnected()
- {
- return _session?.Connected == true;
+ // 应用程序实例证书的存储位置和主题名称
+ 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()
+ };
}
}
}
diff --git a/DMS.WPF/App.xaml.cs b/DMS.WPF/App.xaml.cs
index b09bd42..21724e5 100644
--- a/DMS.WPF/App.xaml.cs
+++ b/DMS.WPF/App.xaml.cs
@@ -159,7 +159,7 @@ public partial class App : System.Windows.Application
services.AddSingleton();
services.AddSingleton();
- services.AddSingleton();
+ services.AddTransient();
// 注册App服务
diff --git a/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs b/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs
index 328ca5b..93e44a8 100644
--- a/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs
+++ b/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs
@@ -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 _opcUaNodes;
+ [ObservableProperty]
+ private OpcUaNode _rootOpcUaNode;
[ObservableProperty]
private ObservableCollection _selectedNodeVariables;
@@ -30,7 +31,7 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase();
SelectedNodeVariables = new ObservableCollection();
this._opcUaService = opcUaService;
-
- _cancellationTokenSource=new CancellationTokenSource();
+ RootOpcUaNode = new OpcUaNode() { DisplayName = "根节点", NodeId = Objects.ObjectsFolder };
+ _cancellationTokenSource = new CancellationTokenSource();
}
@@ -57,13 +58,12 @@ public partial class ImportOpcUaDialogViewModel : DialogViewModelBase Children { get; set; }
+
+ public OpcUaNodeViewModel(string displayName, NodeId nodeId, NodeType nodeType)
+ {
+ DisplayName = displayName;
+ NodeId = nodeId;
+ NodeType = nodeType;
+ Children = new ObservableCollection();
+
+ // 如果是文件夹或对象,添加一个虚拟子节点,用于懒加载
+ if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
+ {
+ Children.Add(new OpcUaNodeViewModel("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
+ }
+ }
+ }
+
+ public enum NodeType
+ {
+ Folder,
+ Object,
+ Variable
+ }
+}
\ No newline at end of file
diff --git a/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml b/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml
index edd265a..330d1f3 100644
--- a/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml
+++ b/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml
@@ -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">