diff --git a/.gitignore b/.gitignore
index ee323ee..ed4a86a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -364,4 +364,6 @@ FodyWeavers.xsd
# Rider/JetBrains IDEs
.idea/
-/OpcUaTestApp/OpcUaTestApp.csproj
+/OpcUaTestApp/
+/OpcUaExample/
+.gitignore
diff --git a/DMS.Core/DMS.Core.csproj b/DMS.Core/DMS.Core.csproj
index 4bbeeee..e0c93f7 100644
--- a/DMS.Core/DMS.Core.csproj
+++ b/DMS.Core/DMS.Core.csproj
@@ -23,10 +23,6 @@
-
-
-
-
diff --git a/DMS.Core/Interfaces/IExcelService.cs b/DMS.Core/Interfaces/Services/IExcelService.cs
similarity index 89%
rename from DMS.Core/Interfaces/IExcelService.cs
rename to DMS.Core/Interfaces/Services/IExcelService.cs
index f653e48..c94d676 100644
--- a/DMS.Core/Interfaces/IExcelService.cs
+++ b/DMS.Core/Interfaces/Services/IExcelService.cs
@@ -1,6 +1,6 @@
using DMS.Core.Models;
-namespace DMS.Core.Interfaces;
+namespace DMS.Core.Interfaces.Services;
public interface IExcelService
{
diff --git a/DMS.Infrastructure.UnitTests/Services/OpcUaServiceTest.cs b/DMS.Infrastructure.UnitTests/Services/OpcUaServiceTest.cs
new file mode 100644
index 0000000..c6ab823
--- /dev/null
+++ b/DMS.Infrastructure.UnitTests/Services/OpcUaServiceTest.cs
@@ -0,0 +1,159 @@
+using DMS.Infrastructure.Interfaces.Services;
+using DMS.Infrastructure.Services;
+using Opc.Ua;
+using System;
+using System.Threading.Tasks;
+using Xunit;
+
+namespace DMS.Infrastructure.UnitTests.Services
+{
+ public class OpcUaServiceTest
+ {
+ [Fact]
+ public async Task TestOpcUaService_CreateSession_WithValidUrl_ShouldCreateSession()
+ {
+ // Arrange
+ var service = new OpcUaService();
+ var opcUaServerUrl = "opc.tcp://localhost:4840"; // 示例URL,实际测试时需要真实的OPC UA服务器
+
+ // Act & Assert
+ // 注意:这个测试需要真实的OPC UA服务器才能通过
+ // 在实际测试环境中,您需要启动一个OPC UA服务器
+ try
+ {
+ await service.CreateSession(opcUaServerUrl);
+ // 如果没有异常,则认为会话创建成功
+ Assert.True(true);
+ }
+ catch (Exception ex)
+ {
+ // 在没有真实服务器的情况下,我们期望出现连接异常
+ Assert.NotNull(ex);
+ }
+ }
+
+ [Fact]
+ public async Task TestOpcUaService_CreateSession_WithNullUrl_ShouldThrowException()
+ {
+ // Arrange
+ var service = new OpcUaService();
+ string opcUaServerUrl = null;
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await service.CreateSession(opcUaServerUrl);
+ });
+ }
+
+ [Fact]
+ public async Task TestOpcUaService_CreateSession_WithEmptyUrl_ShouldThrowException()
+ {
+ // Arrange
+ var service = new OpcUaService();
+ var opcUaServerUrl = "";
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await service.CreateSession(opcUaServerUrl);
+ });
+ }
+
+ [Fact]
+ public void TestOpcUaService_IsConnected_WithoutSession_ShouldReturnFalse()
+ {
+ // Arrange
+ var service = new OpcUaService();
+
+ // Act
+ var isConnected = service.IsConnected();
+
+ // Assert
+ Assert.False(isConnected);
+ }
+
+ [Fact]
+ public async Task TestOpcUaService_ConnectAsync_WithoutSession_ShouldThrowException()
+ {
+ // Arrange
+ var service = new OpcUaService();
+
+ // Act & Assert
+ await Assert.ThrowsAsync(async () =>
+ {
+ await service.ConnectAsync();
+ });
+ }
+
+ [Fact]
+ public void TestOpcUaService_Connect_WithoutSession_ShouldThrowException()
+ {
+ // Arrange
+ var service = new OpcUaService();
+
+ // Act & Assert
+ Assert.Throws(() =>
+ {
+ service.Connect();
+ });
+ }
+
+ [Fact]
+ public void TestOpcUaService_AddSubscription_WithoutSession_ShouldThrowException()
+ {
+ // Arrange
+ var service = new OpcUaService();
+ var subscriptionName = "TestSubscription";
+
+ // Act & Assert
+ Assert.Throws(() =>
+ {
+ service.AddSubscription(subscriptionName);
+ });
+ }
+
+ [Fact]
+ public void TestOpcUaService_BrowseNodes_WithoutSession_ShouldThrowException()
+ {
+ // Arrange
+ var service = new OpcUaService();
+ var nodeId = NodeId.Null;
+
+ // Act & Assert
+ Assert.Throws(() =>
+ {
+ service.BrowseNodes(nodeId);
+ });
+ }
+
+ [Fact]
+ public void TestOpcUaService_ReadValue_WithoutSession_ShouldThrowException()
+ {
+ // Arrange
+ var service = new OpcUaService();
+ var nodeId = NodeId.Null;
+
+ // Act & Assert
+ Assert.Throws(() =>
+ {
+ service.ReadValue(nodeId);
+ });
+ }
+
+ [Fact]
+ public void TestOpcUaService_WriteValue_WithoutSession_ShouldThrowException()
+ {
+ // Arrange
+ var service = new OpcUaService();
+ var nodeId = NodeId.Null;
+ var value = "test";
+
+ // Act & Assert
+ Assert.Throws(() =>
+ {
+ service.WriteValue(nodeId, value);
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/DMS.Infrastructure/Services/OpcUaHelper.cs b/DMS.Infrastructure/Helper/OpcUaHelper.cs
similarity index 98%
rename from DMS.Infrastructure/Services/OpcUaHelper.cs
rename to DMS.Infrastructure/Helper/OpcUaHelper.cs
index b896adf..8427285 100644
--- a/DMS.Infrastructure/Services/OpcUaHelper.cs
+++ b/DMS.Infrastructure/Helper/OpcUaHelper.cs
@@ -5,7 +5,7 @@ using System;
using System.Threading;
using System.Threading.Tasks;
-namespace DMS.Infrastructure.Services;
+namespace DMS.Infrastructure.Helper;
public static class OpcUaHelper
{
diff --git a/DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs b/DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs
new file mode 100644
index 0000000..1b83d11
--- /dev/null
+++ b/DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs
@@ -0,0 +1,72 @@
+using Opc.Ua;
+using Opc.Ua.Client;
+using System;
+using System.Collections.Generic;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace DMS.Infrastructure.Interfaces.Services
+{
+ public interface IOpcUaService
+ {
+ ///
+ /// 创建 OPC UA 会话
+ ///
+ /// OPC UA 服务器地址
+ /// 取消令牌
+ ///
+ public Task CreateSession(string opcUaServerUrl, CancellationToken stoppingToken = default);
+
+ ///
+ /// 连接到 OPC UA 服务器(异步)
+ ///
+ /// 取消令牌
+ ///
+ public Task ConnectAsync(CancellationToken stoppingToken = default);
+
+ ///
+ /// 连接到 OPC UA 服务器(同步)
+ ///
+ public void Connect();
+
+ ///
+ /// 断开 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();
+ }
+}
\ No newline at end of file
diff --git a/DMS.Infrastructure/Services/ExcelService.cs b/DMS.Infrastructure/Services/ExcelService.cs
index 2280e7a..fa0fa0d 100644
--- a/DMS.Infrastructure/Services/ExcelService.cs
+++ b/DMS.Infrastructure/Services/ExcelService.cs
@@ -1,7 +1,7 @@
using System.Data;
using System.Reflection;
using DMS.Core.Enums;
-using DMS.Core.Interfaces;
+using DMS.Core.Interfaces.Services;
using DMS.Core.Models;
using DMS.Infrastructure.Helper;
using NPOI.SS.UserModel;
diff --git a/DMS.Infrastructure/Services/OpcUaBackgroundService.cs b/DMS.Infrastructure/Services/OpcUaBackgroundService.cs
index e8e8ca8..87da528 100644
--- a/DMS.Infrastructure/Services/OpcUaBackgroundService.cs
+++ b/DMS.Infrastructure/Services/OpcUaBackgroundService.cs
@@ -7,6 +7,7 @@ using Opc.Ua.Client;
using Microsoft.Extensions.Logging;
using DMS.Application.Interfaces;
using DMS.Core.Interfaces;
+using DMS.Infrastructure.Helper;
namespace DMS.Infrastructure.Services;
diff --git a/DMS.Infrastructure/Services/OpcUaService.cs b/DMS.Infrastructure/Services/OpcUaService.cs
new file mode 100644
index 0000000..b469ac1
--- /dev/null
+++ b/DMS.Infrastructure/Services/OpcUaService.cs
@@ -0,0 +1,322 @@
+using DMS.Infrastructure.Interfaces.Services;
+using DMS.Infrastructure.Helper;
+using Opc.Ua;
+using Opc.Ua.Client;
+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;
+
+ ///
+ /// 创建 OPC UA 会话
+ ///
+ /// OPC UA 服务器地址
+ /// 取消令牌
+ ///
+ public async Task CreateSession(string opcUaServerUrl, CancellationToken stoppingToken = default)
+ {
+ if (string.IsNullOrEmpty(opcUaServerUrl))
+ {
+ throw new ArgumentException("OPC UA server URL cannot be null or empty.", nameof(opcUaServerUrl));
+ }
+
+ try
+ {
+ _session = await OpcUaHelper.CreateOpcUaSessionAsync(opcUaServerUrl, stoppingToken);
+ _serverUrl = opcUaServerUrl;
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed to create OPC UA session: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// 连接到 OPC UA 服务器
+ ///
+ public async Task ConnectAsync(CancellationToken stoppingToken = default)
+ {
+ if (string.IsNullOrEmpty(_serverUrl))
+ {
+ throw new InvalidOperationException("Server URL is not set. Please call CreateSession first.");
+ }
+
+ // 如果已经连接,直接返回
+ if (_session?.Connected == true)
+ {
+ return;
+ }
+
+ // 重新创建会话
+ await CreateSession(_serverUrl, stoppingToken);
+ }
+
+ ///
+ /// 连接到 OPC UA 服务器(同步版本,用于向后兼容)
+ ///
+ public void Connect()
+ {
+ if (string.IsNullOrEmpty(_serverUrl))
+ {
+ throw new InvalidOperationException("Server URL is not set. Please call CreateSession first.");
+ }
+
+ // 如果已经连接,直接返回
+ if (_session?.Connected == true)
+ {
+ return;
+ }
+
+ // 检查会话是否存在但未连接
+ if (_session != null)
+ {
+ // 尝试重新激活会话
+ try
+ {
+ _session.Reconnect();
+ return;
+ }
+ catch
+ {
+ // 如果重新连接失败,继续到重新创建会话的步骤
+ }
+ }
+
+ // 如果没有会话或重新连接失败,抛出异常提示需要调用 CreateSession
+ throw new InvalidOperationException("Session is not created or connection lost. Please call CreateSession first.");
+ }
+
+ ///
+ /// 断开 OPC UA 服务器连接
+ ///
+ public void Disconnect()
+ {
+ if (_session != null)
+ {
+ try
+ {
+ _session.Close();
+ }
+ catch (Exception ex)
+ {
+ // 记录日志但不抛出异常,确保清理工作完成
+ System.Diagnostics.Debug.WriteLine($"Error closing OPC UA session: {ex.Message}");
+ }
+ finally
+ {
+ _session = null;
+ }
+ }
+ }
+
+ ///
+ /// 添加订阅
+ ///
+ /// 订阅名称
+ /// 创建的订阅
+ 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);
+ }
+ }
+
+ ///
+ /// 浏览节点
+ ///
+ /// 起始节点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
+ {
+ NodeId = 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);
+ }
+ }
+
+ ///
+ /// 写入节点值
+ ///
+ /// 节点ID
+ /// 要写入的值
+ /// 写入结果
+ public StatusCode WriteValue(NodeId nodeId, object value)
+ {
+ 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 nodesToWrite = new WriteValueCollection
+ {
+ 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;
+ }
+ }
+}
diff --git a/DMS.WPF/App.xaml.cs b/DMS.WPF/App.xaml.cs
index c9364b4..b09bd42 100644
--- a/DMS.WPF/App.xaml.cs
+++ b/DMS.WPF/App.xaml.cs
@@ -26,6 +26,8 @@ using DMS.WPF.ViewModels.Dialogs;
using DataProcessingService = DMS.Services.DataProcessingService;
using IDataProcessingService = DMS.Services.IDataProcessingService;
using LogLevel = Microsoft.Extensions.Logging.LogLevel;
+using DMS.Core.Interfaces.Services;
+using DMS.Infrastructure.Interfaces.Services;
namespace DMS;
@@ -156,6 +158,8 @@ public partial class App : System.Windows.Application
services.AddSingleton();
services.AddSingleton();
services.AddSingleton();
+
+ services.AddSingleton();
// 注册App服务
@@ -182,6 +186,7 @@ public partial class App : System.Windows.Application
services.AddSingleton();
// 注册对话框模型
services.AddTransient();
+ services.AddTransient();
services.AddTransient();
// 注册对话框
services.AddSingleton();
diff --git a/DMS.WPF/DMS.WPF.csproj b/DMS.WPF/DMS.WPF.csproj
index bdae7c5..3833ec2 100644
--- a/DMS.WPF/DMS.WPF.csproj
+++ b/DMS.WPF/DMS.WPF.csproj
@@ -66,7 +66,7 @@
Wpf
Designer
-
+
MSBuild:Compile
Wpf
Designer
diff --git a/DMS.WPF/ViewModels/Dialogs/ImportExcelDialogViewModel.cs b/DMS.WPF/ViewModels/Dialogs/ImportExcelDialogViewModel.cs
index 1e7ba43..1a2822a 100644
--- a/DMS.WPF/ViewModels/Dialogs/ImportExcelDialogViewModel.cs
+++ b/DMS.WPF/ViewModels/Dialogs/ImportExcelDialogViewModel.cs
@@ -4,7 +4,7 @@ using System.Linq;
using AutoMapper;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
-using DMS.Core.Interfaces;
+using DMS.Core.Interfaces.Services;
using DMS.Core.Models;
using DMS.Helper;
using DMS.WPF.ViewModels.Items;
diff --git a/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs b/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs
new file mode 100644
index 0000000..b15d7bb
--- /dev/null
+++ b/DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs
@@ -0,0 +1,256 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using DMS.Core.Models;
+using DMS.Helper;
+using DMS.WPF.ViewModels.Items;
+using Opc.Ua.Client;
+using System.Collections.ObjectModel;
+
+namespace DMS.WPF.ViewModels.Dialogs;
+
+public partial class ImportOpcUaDialogViewModel : DialogViewModelBase>
+{
+ [ObservableProperty]
+ private string _endpointUrl = "opc.tcp://127.0.0.1:4855"; // 默认值
+
+ //[ObservableProperty]
+ //private ObservableCollection _opcUaNodes;
+
+ [ObservableProperty]
+ private ObservableCollection _selectedNodeVariables;
+
+ public List SelectedVariables { get; set; } = new List();
+
+ [ObservableProperty]
+ private bool _selectAllVariables;
+
+ [ObservableProperty]
+ private bool _isConnected;
+
+ private Session _session;
+
+ public ImportOpcUaDialogViewModel()
+ {
+ //OpcUaNodes = new ObservableCollection();
+ SelectedNodeVariables = new ObservableCollection();
+ // Automatically connect when the ViewModel is created
+ //ConnectC.Execute(null);
+
+ }
+
+ [RelayCommand]
+ private async Task Connect()
+ {
+ try
+ {
+ // 断开现有连接
+ if (_session != null && _session.Connected)
+ {
+ await _session.CloseAsync();
+ _session.Dispose();
+ _session = null;
+ }
+
+ IsConnected = false;
+ SelectedNodeVariables.Clear();
+
+ //_session = await ServiceHelper.CreateOpcUaSessionAsync(EndpointUrl);
+
+ NotificationHelper.ShowSuccess($"已连接到 OPC UA 服务器: {EndpointUrl}");
+ IsConnected = true;
+
+ // 浏览根节点
+ //await BrowseNodes(OpcUaNodes, ObjectIds.ObjectsFolder);
+ }
+ catch (Exception ex)
+ {
+ IsConnected = false;
+ NotificationHelper.ShowError($"连接 OPC UA 服务器失败: {EndpointUrl} - {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// 处理来自服务器的数据变化通知
+ ///
+ private static void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e)
+ {
+ foreach (var value in item.DequeueValues())
+ {
+ Console.WriteLine(
+ $"[通知] {item.DisplayName}: {value.Value} | 时间戳: {value.SourceTimestamp.ToLocalTime()} | 状态: {value.StatusCode}");
+ }
+ }
+
+ //private async Task BrowseNodes(ObservableCollection nodes, NodeId parentNodeId)
+ //{
+ // try
+ // {
+ // Opc.Ua.ReferenceDescriptionCollection references;
+ // byte[] continuationPoint = null;
+
+ // _session.Browse(
+ // null, // RequestHeader
+ // new ViewDescription(),
+ // parentNodeId,
+ // 0u,
+ // BrowseDirection.Forward,
+ // Opc.Ua.ReferenceTypeIds.HierarchicalReferences,
+ // true,
+ // (uint)Opc.Ua.NodeClass.Object | (uint)Opc.Ua.NodeClass.Variable,
+ // out continuationPoint,
+ // out references
+ // );
+
+ // foreach (var rd in references)
+ // {
+ // NodeType nodeType = NodeType.Folder; // 默认是文件夹
+ // if ((rd.NodeClass & NodeClass.Variable) != 0)
+ // {
+ // nodeType = NodeType.Variable;
+ // }
+ // else if ((rd.NodeClass & NodeClass.Object) != 0)
+ // {
+ // nodeType = NodeType.Object;
+ // }
+
+ // var opcUaNode = new OpcUaNode(rd.DisplayName.Text, (NodeId)rd.NodeId, nodeType);
+ // nodes.Add(opcUaNode);
+
+ // // 如果是文件夹或对象,添加一个虚拟子节点,用于懒加载
+ // if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
+ // {
+ // opcUaNode.Children.Add(new OpcUaNode("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
+ // }
+ // }
+ // }
+ // catch (Exception ex)
+ // {
+ // NlogHelper.Error($"浏览 OPC UA 节点失败: {parentNodeId} - {ex.Message}", ex);
+ // NotificationHelper.ShowError($"浏览 OPC UA 节点失败: {parentNodeId} - {ex.Message}", ex);
+ // }
+ //}
+
+ //public async Task LoadNodeVariables(OpcUaNode node)
+ //{
+ //if (node.NodeType == NodeType.Variable)
+ //{
+ // // 如果是变量节点,直接显示它
+ // SelectedNodeVariables.Clear();
+ // SelectedNodeVariables.Add(new Variable
+ // {
+ // Name = node.DisplayName,
+ // NodeId = node.NodeId.ToString(),
+ // OpcUaNodeId = node.NodeId.ToString(),
+ // ProtocolType = ProtocolType.OpcUA,
+ // IsActive = true // 默认选中
+ // });
+ // return;
+ //}
+
+ //if (node.IsLoaded || node.IsLoading)
+ //{
+ // return; // 已经加载或正在加载
+ //}
+
+ //node.IsLoading = true;
+ //node.Children.Clear(); // 清除虚拟节点
+
+ //try
+ //{
+ // Opc.Ua.ReferenceDescriptionCollection references;
+ // byte[] continuationPoint = null;
+
+ // _session.Browse(
+ // null, // RequestHeader
+ // new ViewDescription(),
+ // node.NodeId,
+ // 0u,
+ // BrowseDirection.Forward,
+ // Opc.Ua.ReferenceTypeIds.HierarchicalReferences,
+ // true,
+ // (uint)Opc.Ua.NodeClass.Object | (uint)Opc.Ua.NodeClass.Variable,
+ // out continuationPoint,
+ // out references
+ // );
+
+ // foreach (var rd in references)
+ // {
+ // NodeType nodeType = NodeType.Folder;
+ // if ((rd.NodeClass & NodeClass.Variable) != 0)
+ // {
+ // nodeType = NodeType.Variable;
+ // }
+ // else if ((rd.NodeClass & NodeClass.Object) != 0)
+ // {
+ // nodeType = NodeType.Object;
+ // }
+
+ // var opcUaNode = new OpcUaNode(rd.DisplayName.Text, (NodeId)rd.NodeId, nodeType);
+ // node.Children.Add(opcUaNode);
+
+ // if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
+ // {
+ // opcUaNode.Children.Add(new OpcUaNode("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
+ // }
+
+ // // 如果是变量,添加到右侧列表
+ // if (nodeType == NodeType.Variable)
+ // {
+ // // Read the DataType attribute
+ // ReadValueId readValueId = new ReadValueId
+ // {
+ // NodeId = opcUaNode.NodeId,
+ // AttributeId = Attributes.DataType,
+ // // You might need to specify IndexRange and DataEncoding if dealing with arrays or specific encodings
+ // };
+
+ // DataValueCollection results;
+ // DiagnosticInfoCollection diagnosticInfos;
+
+ // _session.Read(
+ // null, // RequestHeader
+ // 0, // MaxAge
+ // TimestampsToReturn.Source,
+ // new ReadValueIdCollection { readValueId },
+ // out results,
+ // out diagnosticInfos
+ // );
+
+ // string dataType = string.Empty;
+
+ // if (results != null && results.Count > 0 && results[0].Value != null)
+ // {
+ // // Convert the NodeId of the DataType to a readable string
+ // NodeId dataTypeNodeId = (NodeId)results[0].Value;
+ // dataType = _session.NodeCache.GetDisplayText(dataTypeNodeId);
+ // }
+
+ // SelectedNodeVariables.Add(new Variable
+ // {
+ // Name = opcUaNode.DisplayName,
+ // OpcUaNodeId = opcUaNode.NodeId.ToString(),
+ // ProtocolType = ProtocolType.OpcUA,
+ // IsActive = true, // Default selected
+ // DataType = dataType // Assign the read DataType
+ // });
+ // }
+ // }
+
+ // node.IsLoaded = true;
+ //}
+ //catch (Exception ex)
+ //{
+ // NlogHelper.Error($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex);
+ // NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex);
+ //}
+ //finally
+ //{
+ // node.IsLoading = false;
+ //}
+ //}
+
+ public ObservableCollection GetSelectedVariables()
+ {
+ return new ObservableCollection(SelectedVariables);
+ }
+}
\ No newline at end of file
diff --git a/DMS.WPF/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs b/DMS.WPF/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs
deleted file mode 100644
index 79294e1..0000000
--- a/DMS.WPF/ViewModels/Dialogs/OpcUaImportDialogViewModel.cs
+++ /dev/null
@@ -1,257 +0,0 @@
-using System.Collections.ObjectModel;
-using System.Linq;
-using System.Threading.Tasks;
-using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
-using DMS.Core.Enums;
-using DMS.Helper;
-
-namespace DMS.WPF.ViewModels.Dialogs;
-
-public partial class OpcUaImportDialogViewModel : ObservableObject
-{
- // [ObservableProperty]
- // private string _endpointUrl = "opc.tcp://127.0.0.1:4855"; // 默认值
- //
- // [ObservableProperty]
- // private ObservableCollection _opcUaNodes;
- //
- // [ObservableProperty]
- // private ObservableCollection _selectedNodeVariables;
- //
- // public List SelectedVariables { get; set; }=new List();
- //
- // [ObservableProperty]
- // private bool _selectAllVariables;
- //
- // [ObservableProperty]
- // private bool _isConnected;
- //
- // private Session _session;
- //
- // public OpcUaImportDialogViewModel()
- // {
- // OpcUaNodes = new ObservableCollection();
- // SelectedNodeVariables = new ObservableCollection();
- // // Automatically connect when the ViewModel is created
- // ConnectCommand.Execute(null);
- //
- // }
- //
- // [RelayCommand]
- // private async Task Connect()
- // {
- // try
- // {
- // // 断开现有连接
- // if (_session != null && _session.Connected)
- // {
- // await _session.CloseAsync();
- // _session.Dispose();
- // _session = null;
- // }
- //
- // IsConnected = false;
- // OpcUaNodes.Clear();
- // SelectedNodeVariables.Clear();
- //
- // _session = await ServiceHelper.CreateOpcUaSessionAsync(EndpointUrl);
- //
- // NotificationHelper.ShowSuccess($"已连接到 OPC UA 服务器: {EndpointUrl}");
- // IsConnected = true;
- //
- // // 浏览根节点
- // await BrowseNodes(OpcUaNodes, ObjectIds.ObjectsFolder);
- // }
- // catch (Exception ex)
- // {
- // IsConnected = false;
- // NotificationHelper.ShowError($"连接 OPC UA 服务器失败: {EndpointUrl} - {ex.Message}", ex);
- // }
- // }
- //
- // ///
- // /// 处理来自服务器的数据变化通知
- // ///
- // private static void OnNotification(MonitoredItem item, MonitoredItemNotificationEventArgs e)
- // {
- // foreach (var value in item.DequeueValues())
- // {
- // Console.WriteLine(
- // $"[通知] {item.DisplayName}: {value.Value} | 时间戳: {value.SourceTimestamp.ToLocalTime()} | 状态: {value.StatusCode}");
- // }
- // }
- //
- // private async Task BrowseNodes(ObservableCollection nodes, NodeId parentNodeId)
- // {
- // try
- // {
- // Opc.Ua.ReferenceDescriptionCollection references;
- // byte[] continuationPoint = null;
- //
- // _session.Browse(
- // null, // RequestHeader
- // new ViewDescription(),
- // parentNodeId,
- // 0u,
- // BrowseDirection.Forward,
- // Opc.Ua.ReferenceTypeIds.HierarchicalReferences,
- // true,
- // (uint)Opc.Ua.NodeClass.Object | (uint)Opc.Ua.NodeClass.Variable,
- // out continuationPoint,
- // out references
- // );
- //
- // foreach (var rd in references)
- // {
- // NodeType nodeType = NodeType.Folder; // 默认是文件夹
- // if ((rd.NodeClass & NodeClass.Variable) != 0)
- // {
- // nodeType = NodeType.Variable;
- // }
- // else if ((rd.NodeClass & NodeClass.Object) != 0)
- // {
- // nodeType = NodeType.Object;
- // }
- //
- // var opcUaNode = new OpcUaNode(rd.DisplayName.Text, (NodeId)rd.NodeId, nodeType);
- // nodes.Add(opcUaNode);
- //
- // // 如果是文件夹或对象,添加一个虚拟子节点,用于懒加载
- // if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
- // {
- // opcUaNode.Children.Add(new OpcUaNode("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
- // }
- // }
- // }
- // catch (Exception ex)
- // {
- // NlogHelper.Error($"浏览 OPC UA 节点失败: {parentNodeId} - {ex.Message}", ex);
- // NotificationHelper.ShowError($"浏览 OPC UA 节点失败: {parentNodeId} - {ex.Message}", ex);
- // }
- // }
- //
- // public async Task LoadNodeVariables(OpcUaNode node)
- // {
- // if (node.NodeType == NodeType.Variable)
- // {
- // // 如果是变量节点,直接显示它
- // SelectedNodeVariables.Clear();
- // SelectedNodeVariables.Add(new Variable
- // {
- // Name = node.DisplayName,
- // NodeId = node.NodeId.ToString(),
- // OpcUaNodeId = node.NodeId.ToString(),
- // ProtocolType = ProtocolType.OpcUA,
- // IsActive = true // 默认选中
- // });
- // return;
- // }
- //
- // if (node.IsLoaded || node.IsLoading)
- // {
- // return; // 已经加载或正在加载
- // }
- //
- // node.IsLoading = true;
- // node.Children.Clear(); // 清除虚拟节点
- //
- // try
- // {
- // Opc.Ua.ReferenceDescriptionCollection references;
- // byte[] continuationPoint = null;
- //
- // _session.Browse(
- // null, // RequestHeader
- // new ViewDescription(),
- // node.NodeId,
- // 0u,
- // BrowseDirection.Forward,
- // Opc.Ua.ReferenceTypeIds.HierarchicalReferences,
- // true,
- // (uint)Opc.Ua.NodeClass.Object | (uint)Opc.Ua.NodeClass.Variable,
- // out continuationPoint,
- // out references
- // );
- //
- // foreach (var rd in references)
- // {
- // NodeType nodeType = NodeType.Folder;
- // if ((rd.NodeClass & NodeClass.Variable) != 0)
- // {
- // nodeType = NodeType.Variable;
- // }
- // else if ((rd.NodeClass & NodeClass.Object) != 0)
- // {
- // nodeType = NodeType.Object;
- // }
- //
- // var opcUaNode = new OpcUaNode(rd.DisplayName.Text, (NodeId)rd.NodeId, nodeType);
- // node.Children.Add(opcUaNode);
- //
- // if (nodeType == NodeType.Folder || nodeType == NodeType.Object)
- // {
- // opcUaNode.Children.Add(new OpcUaNode("Loading...", NodeId.Null, NodeType.Folder)); // 虚拟节点
- // }
- //
- // // 如果是变量,添加到右侧列表
- // if (nodeType == NodeType.Variable)
- // {
- // // Read the DataType attribute
- // ReadValueId readValueId = new ReadValueId
- // {
- // NodeId = opcUaNode.NodeId,
- // AttributeId = Attributes.DataType,
- // // You might need to specify IndexRange and DataEncoding if dealing with arrays or specific encodings
- // };
- //
- // DataValueCollection results;
- // DiagnosticInfoCollection diagnosticInfos;
- //
- // _session.Read(
- // null, // RequestHeader
- // 0, // MaxAge
- // TimestampsToReturn.Source,
- // new ReadValueIdCollection { readValueId },
- // out results,
- // out diagnosticInfos
- // );
- //
- // string dataType = string.Empty;
- //
- // if (results != null && results.Count > 0 && results[0].Value != null)
- // {
- // // Convert the NodeId of the DataType to a readable string
- // NodeId dataTypeNodeId = (NodeId)results[0].Value;
- // dataType = _session.NodeCache.GetDisplayText(dataTypeNodeId);
- // }
- //
- // SelectedNodeVariables.Add(new Variable
- // {
- // Name = opcUaNode.DisplayName,
- // OpcUaNodeId = opcUaNode.NodeId.ToString(),
- // ProtocolType = ProtocolType.OpcUA,
- // IsActive = true, // Default selected
- // DataType = dataType // Assign the read DataType
- // });
- // }
- // }
- //
- // node.IsLoaded = true;
- // }
- // catch (Exception ex)
- // {
- // NlogHelper.Error($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex);
- // NotificationHelper.ShowError($"加载 OPC UA 节点变量失败: {node.NodeId} - {ex.Message}", ex);
- // }
- // finally
- // {
- // node.IsLoading = false;
- // }
- // }
- //
- // public ObservableCollection GetSelectedVariables()
- // {
- // return new ObservableCollection(SelectedVariables);
- // }
-}
\ No newline at end of file
diff --git a/DMS.WPF/ViewModels/VariableTableViewModel.cs b/DMS.WPF/ViewModels/VariableTableViewModel.cs
index 9f40e37..b74ae32 100644
--- a/DMS.WPF/ViewModels/VariableTableViewModel.cs
+++ b/DMS.WPF/ViewModels/VariableTableViewModel.cs
@@ -273,86 +273,66 @@ partial class VariableTableViewModel : ViewModelBase, INavigatable
/// 此命令通常绑定到UI中的“从OPC UA导入”按钮。
///
[RelayCommand]
- private async Task ImportFromOpcUaServer()
+ private async void ImportFromOpcUaServer()
{
- // ContentDialog processingDialog = null; // 用于显示处理中的对话框
- // try
- // {
- // // 检查OPC UA Endpoint URL是否已设置
- // string opcUaEndpointUrl = VariableTable?.Device?.OpcUaEndpointUrl;
- // if (string.IsNullOrEmpty(opcUaEndpointUrl))
- // {
- // NotificationHelper.ShowError("OPC UA Endpoint URL 未设置。请在设备详情中配置。");
- // return;
- // }
- //
- // // 显示OPC UA导入对话框,让用户选择要导入的变量
- // var importedVariables = await _dialogService.ShowOpcUaImportDialog(opcUaEndpointUrl);
- // if (importedVariables == null || !importedVariables.Any())
- // {
- // return; // 用户取消或没有选择任何变量
- // }
- //
- // // 显示处理中的对话框
- // processingDialog = _dialogService.ShowProcessingDialog("正在处理...", "正在导入OPC UA变量,请稍等片刻....");
- //
- // // 在进行重复检查之前,先刷新 Variables 集合,确保其包含所有最新数据
- // await RefreshDataView();
- //
- // List newVariables = new List();
- // List importedVariableNames = new List();
- // List existingVariableNames = new List();
- //
- // foreach (var variableData in importedVariables)
- // {
- // // 判断是否存在重复变量,仅在当前 VariableTable 的 Variables 中查找
- // bool isDuplicate = Variables.Any(existingVar =>
- // (existingVar.Name == variableData.Name) ||
- // (!string.IsNullOrEmpty(variableData.NodeId) &&
- // existingVar.NodeId == variableData.NodeId) ||
- // (!string.IsNullOrEmpty(variableData.OpcUaNodeId) &&
- // existingVar.OpcUaNodeId == variableData.OpcUaNodeId)
- // );
- //
- // if (isDuplicate)
- // {
- // existingVariableNames.Add(variableData.Name);
- // }
- // else
- // {
- // variableData.CreateTime = DateTime.Now;
- // variableData.VariableTableId = VariableTable.Id;
- // variableData.ProtocolType = ProtocolType.OpcUA; // 确保协议类型正确
- // variableData.IsModified = false;
- // newVariables.Add(variableData);
- // importedVariableNames.Add(variableData.Name);
- // }
- // }
- //
- // if (newVariables.Any())
- // {
- // // 批量插入新变量数据到数据库
- // var resVarDataCount = await _varDataRepository.AddAsync(newVariables);
- // NlogHelper.Info($"成功导入OPC UA变量:{resVarDataCount}个。");
- // }
- //
- // // 再次刷新 Variables 集合,以反映新添加的数据
- // await RefreshDataView();
- //
- // processingDialog?.Hide(); // 隐藏处理中的对话框
- //
- // // 显示导入结果对话框
- // await _dialogService.ShowImportResultDialog(importedVariableNames, existingVariableNames);
- // }
- // catch (Exception e)
- // {
- // // 捕获并显示错误通知
- // NotificationHelper.ShowError($"从OPC UA服务器导入变量的过程中发生了不可预期的错误:{e.Message}", e);
- // }
- // finally
- // {
- // processingDialog?.Hide(); // 确保在任何情况下都隐藏对话框
- // }
+ try
+ {
+ // 检查OPC UA Endpoint URL是否已设置
+ string opcUaEndpointUrl = CurrentVariableTable.Device.OpcUaServerUrl;
+ if (string.IsNullOrEmpty(opcUaEndpointUrl))
+ {
+ NotificationHelper.ShowError("OPC UA Endpoint URL 未设置。请在设备详情中配置。");
+ return;
+ }
+
+ // 显示OPC UA导入对话框,让用户选择要导入的变量
+ ImportOpcUaDialogViewModel importOpcUaDialogViewModel = new ImportOpcUaDialogViewModel();
+ var importedVariables = await _dialogService.ShowDialogAsync(importOpcUaDialogViewModel);
+ if (importedVariables == null || !importedVariables.Any())
+ {
+ return; // 用户取消或没有选择任何变量
+ }
+
+ //var importedVariableDtos = _mapper.Map>(importedVariables);
+ //foreach (var variableDto in importedVariableDtos)
+ //{
+ // variableDto.CreatedAt = DateTime.Now;
+ // variableDto.UpdatedAt = DateTime.Now;
+ // variableDto.VariableTableId = CurrentVariableTable.Id;
+ // variableDto.Protocol = ProtocolType.OpcUa; // 确保协议类型正确
+ //}
+
+ //var existList = await _variableAppService.FindExistingVariablesAsync(importedVariableDtos);
+ //if (existList.Count > 0)
+ //{
+ // // 拼接要删除的变量名称,用于确认提示
+ // var existNames = string.Join("、", existList.Select(v => v.Name));
+ // var confrimDialogViewModel
+ // = new ConfirmDialogViewModel("存在已经添加的变量", $"变量名称:{existNames},已经存在,是否跳过继续添加其他的变量。取消则不添加任何变量", "继续");
+ // var res = await _dialogService.ShowDialogAsync(confrimDialogViewModel);
+ // if (!res) return;
+ // // 从导入列表中删除已经存在的变量
+ // importedVariableDtos.RemoveAll(variableDto => existList.Contains(variableDto));
+ //}
+
+ //if (importedVariableDtos.Count != 0)
+ //{
+ // var isSuccess = await _variableAppService.BatchImportVariablesAsync(importedVariableDtos);
+ // if (isSuccess)
+ // {
+ // _variableItemList.AddRange(_mapper.Map>(importedVariableDtos));
+ // NotificationHelper.ShowSuccess($"从OPC UA服务器导入变量成功,共导入变量:{importedVariableDtos.Count}个");
+ // }
+ //}
+ //else
+ //{
+ // NotificationHelper.ShowSuccess($"列表中没有要添加的变量了。");
+ //}
+ }
+ catch (Exception e)
+ {
+ NotificationHelper.ShowError($"从OPC UA服务器导入变量的过程中发生了不可预期的错误:{e.Message}", e);
+ }
}
diff --git a/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml b/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml
new file mode 100644
index 0000000..3acf79a
--- /dev/null
+++ b/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/DMS.WPF/Views/Dialogs/OpcUaImportDialog.xaml.cs b/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml.cs
similarity index 83%
rename from DMS.WPF/Views/Dialogs/OpcUaImportDialog.xaml.cs
rename to DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml.cs
index 5614461..ee81622 100644
--- a/DMS.WPF/Views/Dialogs/OpcUaImportDialog.xaml.cs
+++ b/DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml.cs
@@ -6,17 +6,17 @@ using iNKORE.UI.WPF.Modern.Controls;
namespace DMS.WPF.Views.Dialogs;
///
-/// OpcUaImportDialog.xaml 的交互逻辑
+/// ImportOpcUaDialog.xaml 的交互逻辑
///
-public partial class OpcUaImportDialog : ContentDialog
+public partial class ImportOpcUaDialog : ContentDialog
{
- public OpcUaImportDialogViewModel ViewModel
+ public ImportOpcUaDialogViewModel ViewModel
{
- get => (OpcUaImportDialogViewModel)DataContext;
+ get => (ImportOpcUaDialogViewModel)DataContext;
set => DataContext = value;
}
- public OpcUaImportDialog(OpcUaImportDialogViewModel viewModel)
+ public ImportOpcUaDialog(ImportOpcUaDialogViewModel viewModel)
{
InitializeComponent();
ViewModel = viewModel;
diff --git a/DMS.WPF/Views/Dialogs/OpcUaImportDialog.xaml b/DMS.WPF/Views/Dialogs/OpcUaImportDialog.xaml
deleted file mode 100644
index 4b2bc3a..0000000
--- a/DMS.WPF/Views/Dialogs/OpcUaImportDialog.xaml
+++ /dev/null
@@ -1,96 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/DMS.sln b/DMS.sln
index 0562014..b62cc28 100644
--- a/DMS.sln
+++ b/DMS.sln
@@ -24,6 +24,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "DMS.Infrastructure.UnitTest
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DMS.WPF.UnitTests", "DMS.WPF.UnitTests\DMS.WPF.UnitTests.csproj", "{C15E6B39-211C-417A-BC3F-551AD17C8905}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpcUaTestApp", "OpcUaTestApp\OpcUaTestApp.csproj", "{FD58DDF1-340B-4946-28B7-5B905832858E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -54,6 +56,10 @@ Global
{C15E6B39-211C-417A-BC3F-551AD17C8905}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C15E6B39-211C-417A-BC3F-551AD17C8905}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C15E6B39-211C-417A-BC3F-551AD17C8905}.Release|Any CPU.Build.0 = Release|Any CPU
+ {FD58DDF1-340B-4946-28B7-5B905832858E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {FD58DDF1-340B-4946-28B7-5B905832858E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {FD58DDF1-340B-4946-28B7-5B905832858E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {FD58DDF1-340B-4946-28B7-5B905832858E}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/README.md b/README.md
index 6e61f91..1ecd62e 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,45 @@
-# PMSWPF
\ No newline at end of file
+# PMSWPF
+
+## OPC UA Service
+
+This project includes an OPC UA service implementation that provides the following functionalities:
+
+### Features
+- Connect to OPC UA servers
+- Browse nodes in the OPC UA address space
+- Read and write values from/to OPC UA nodes
+- Add subscriptions for monitoring node changes
+
+### Usage
+
+```csharp
+// Create an instance of the OPC UA service
+var opcUaService = new OpcUaService();
+
+// Connect to an OPC UA server
+await opcUaService.CreateSession("opc.tcp://localhost:4840");
+
+// Check connection status
+if (opcUaService.IsConnected())
+{
+ // Browse nodes
+ var rootNodeId = ObjectIds.RootFolder;
+ var references = opcUaService.BrowseNodes(rootNodeId);
+
+ // Read a value
+ var value = opcUaService.ReadValue(someNodeId);
+
+ // Write a value
+ opcUaService.WriteValue(someNodeId, newValue);
+
+ // Add a subscription
+ var subscription = opcUaService.AddSubscription("MySubscription");
+}
+
+// Disconnect when done
+opcUaService.Disconnect();
+```
+
+### Testing
+
+Unit tests for the OPC UA service are included in the `DMS.Infrastructure.UnitTests` project. Run them using your preferred test runner.
\ No newline at end of file