临时提交
This commit is contained in:
4
.gitignore
vendored
4
.gitignore
vendored
@@ -364,4 +364,6 @@ FodyWeavers.xsd
|
||||
|
||||
# Rider/JetBrains IDEs
|
||||
.idea/
|
||||
/OpcUaTestApp/OpcUaTestApp.csproj
|
||||
/OpcUaTestApp/
|
||||
/OpcUaExample/
|
||||
.gitignore
|
||||
|
||||
@@ -23,10 +23,6 @@
|
||||
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Interfaces\Services\" />
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
using DMS.Core.Models;
|
||||
|
||||
namespace DMS.Core.Interfaces;
|
||||
namespace DMS.Core.Interfaces.Services;
|
||||
|
||||
public interface IExcelService
|
||||
{
|
||||
159
DMS.Infrastructure.UnitTests/Services/OpcUaServiceTest.cs
Normal file
159
DMS.Infrastructure.UnitTests/Services/OpcUaServiceTest.cs
Normal file
@@ -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<ArgumentException>(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<ArgumentException>(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<InvalidOperationException>(async () =>
|
||||
{
|
||||
await service.ConnectAsync();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestOpcUaService_Connect_WithoutSession_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var service = new OpcUaService();
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
service.Connect();
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestOpcUaService_AddSubscription_WithoutSession_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var service = new OpcUaService();
|
||||
var subscriptionName = "TestSubscription";
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
service.AddSubscription(subscriptionName);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestOpcUaService_BrowseNodes_WithoutSession_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var service = new OpcUaService();
|
||||
var nodeId = NodeId.Null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
service.BrowseNodes(nodeId);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void TestOpcUaService_ReadValue_WithoutSession_ShouldThrowException()
|
||||
{
|
||||
// Arrange
|
||||
var service = new OpcUaService();
|
||||
var nodeId = NodeId.Null;
|
||||
|
||||
// Act & Assert
|
||||
Assert.Throws<InvalidOperationException>(() =>
|
||||
{
|
||||
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<InvalidOperationException>(() =>
|
||||
{
|
||||
service.WriteValue(nodeId, value);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
{
|
||||
72
DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs
Normal file
72
DMS.Infrastructure/Interfaces/Services/IOpcUaService.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 创建 OPC UA 会话
|
||||
/// </summary>
|
||||
/// <param name="opcUaServerUrl">OPC UA 服务器地址</param>
|
||||
/// <param name="stoppingToken">取消令牌</param>
|
||||
/// <returns></returns>
|
||||
public Task CreateSession(string opcUaServerUrl, CancellationToken stoppingToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 连接到 OPC UA 服务器(异步)
|
||||
/// </summary>
|
||||
/// <param name="stoppingToken">取消令牌</param>
|
||||
/// <returns></returns>
|
||||
public Task ConnectAsync(CancellationToken stoppingToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 连接到 OPC UA 服务器(同步)
|
||||
/// </summary>
|
||||
public void Connect();
|
||||
|
||||
/// <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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
322
DMS.Infrastructure/Services/OpcUaService.cs
Normal file
322
DMS.Infrastructure/Services/OpcUaService.cs
Normal file
@@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// 创建 OPC UA 会话
|
||||
/// </summary>
|
||||
/// <param name="opcUaServerUrl">OPC UA 服务器地址</param>
|
||||
/// <param name="stoppingToken">取消令牌</param>
|
||||
/// <returns></returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接到 OPC UA 服务器
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 连接到 OPC UA 服务器(同步版本,用于向后兼容)
|
||||
/// </summary>
|
||||
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.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 断开 OPC UA 服务器连接
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 浏览节点
|
||||
/// </summary>
|
||||
/// <param name="nodeId">起始节点ID</param>
|
||||
/// <returns>节点引用列表</returns>
|
||||
public IList<ReferenceDescription> 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 读取节点值
|
||||
/// </summary>
|
||||
/// <param name="nodeId">节点ID</param>
|
||||
/// <returns>节点值</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 写入节点值
|
||||
/// </summary>
|
||||
/// <param name="nodeId">节点ID</param>
|
||||
/// <param name="value">要写入的值</param>
|
||||
/// <returns>写入结果</returns>
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查是否已连接
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool IsConnected()
|
||||
{
|
||||
return _session?.Connected == true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -157,6 +159,8 @@ public partial class App : System.Windows.Application
|
||||
services.AddSingleton<IRepositoryManager, RepositoryManager>();
|
||||
services.AddSingleton<IExcelService, ExcelService>();
|
||||
|
||||
services.AddSingleton<IOpcUaService, OpcUaService>();
|
||||
|
||||
|
||||
// 注册App服务
|
||||
services.AddSingleton<IInitializeService,InitializeService>();
|
||||
@@ -182,6 +186,7 @@ public partial class App : System.Windows.Application
|
||||
services.AddSingleton<MqttsViewModel>();
|
||||
// 注册对话框模型
|
||||
services.AddTransient<ImportExcelDialogViewModel>();
|
||||
services.AddTransient<ImportOpcUaDialogViewModel>();
|
||||
services.AddTransient<VariableDialogViewModel>();
|
||||
// 注册对话框
|
||||
services.AddSingleton<DevicesView>();
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
<XamlRuntime>Wpf</XamlRuntime>
|
||||
<SubType>Designer</SubType>
|
||||
</Page>
|
||||
<Page Update="Views\Dialogs\OpcUaImportDialog.xaml">
|
||||
<Page Update="Views\Dialogs\ImportOpcUaDialog.xaml">
|
||||
<Generator>MSBuild:Compile</Generator>
|
||||
<XamlRuntime>Wpf</XamlRuntime>
|
||||
<SubType>Designer</SubType>
|
||||
|
||||
@@ -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;
|
||||
|
||||
256
DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs
Normal file
256
DMS.WPF/ViewModels/Dialogs/ImportOpcUaDialogViewModel.cs
Normal file
@@ -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<List<VariableItemViewModel>>
|
||||
{
|
||||
[ObservableProperty]
|
||||
private string _endpointUrl = "opc.tcp://127.0.0.1:4855"; // 默认值
|
||||
|
||||
//[ObservableProperty]
|
||||
//private ObservableCollection<OpcUaNode> _opcUaNodes;
|
||||
|
||||
[ObservableProperty]
|
||||
private ObservableCollection<Variable> _selectedNodeVariables;
|
||||
|
||||
public List<Variable> SelectedVariables { get; set; } = new List<Variable>();
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _selectAllVariables;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isConnected;
|
||||
|
||||
private Session _session;
|
||||
|
||||
public ImportOpcUaDialogViewModel()
|
||||
{
|
||||
//OpcUaNodes = new ObservableCollection<OpcUaNode>();
|
||||
SelectedNodeVariables = new ObservableCollection<Variable>();
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理来自服务器的数据变化通知
|
||||
/// </summary>
|
||||
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<OpcUaNode> 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<Variable> GetSelectedVariables()
|
||||
{
|
||||
return new ObservableCollection<Variable>(SelectedVariables);
|
||||
}
|
||||
}
|
||||
@@ -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<OpcUaNode> _opcUaNodes;
|
||||
//
|
||||
// [ObservableProperty]
|
||||
// private ObservableCollection<Variable> _selectedNodeVariables;
|
||||
//
|
||||
// public List<Variable> SelectedVariables { get; set; }=new List<Variable>();
|
||||
//
|
||||
// [ObservableProperty]
|
||||
// private bool _selectAllVariables;
|
||||
//
|
||||
// [ObservableProperty]
|
||||
// private bool _isConnected;
|
||||
//
|
||||
// private Session _session;
|
||||
//
|
||||
// public OpcUaImportDialogViewModel()
|
||||
// {
|
||||
// OpcUaNodes = new ObservableCollection<OpcUaNode>();
|
||||
// SelectedNodeVariables = new ObservableCollection<Variable>();
|
||||
// // 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);
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// /// <summary>
|
||||
// /// 处理来自服务器的数据变化通知
|
||||
// /// </summary>
|
||||
// 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<OpcUaNode> 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<Variable> GetSelectedVariables()
|
||||
// {
|
||||
// return new ObservableCollection<Variable>(SelectedVariables);
|
||||
// }
|
||||
}
|
||||
@@ -273,86 +273,66 @@ partial class VariableTableViewModel : ViewModelBase, INavigatable
|
||||
/// 此命令通常绑定到UI中的“从OPC UA导入”按钮。
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task ImportFromOpcUaServer()
|
||||
private async void ImportFromOpcUaServer()
|
||||
{
|
||||
// ContentDialog processingDialog = null; // 用于显示处理中的对话框
|
||||
// try
|
||||
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<List<VariableDto>>(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)
|
||||
// {
|
||||
// // 检查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<Variable> newVariables = new List<Variable>();
|
||||
// List<string> importedVariableNames = new List<string>();
|
||||
// List<string> existingVariableNames = new List<string>();
|
||||
//
|
||||
// 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(); // 确保在任何情况下都隐藏对话框
|
||||
// _variableItemList.AddRange(_mapper.Map<List<VariableItemViewModel>>(importedVariableDtos));
|
||||
// NotificationHelper.ShowSuccess($"从OPC UA服务器导入变量成功,共导入变量:{importedVariableDtos.Count}个");
|
||||
// }
|
||||
//}
|
||||
//else
|
||||
//{
|
||||
// NotificationHelper.ShowSuccess($"列表中没有要添加的变量了。");
|
||||
//}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
NotificationHelper.ShowError($"从OPC UA服务器导入变量的过程中发生了不可预期的错误:{e.Message}", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
86
DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml
Normal file
86
DMS.WPF/Views/Dialogs/ImportOpcUaDialog.xaml
Normal file
@@ -0,0 +1,86 @@
|
||||
<controls:ContentDialog
|
||||
x:Class="DMS.WPF.Views.Dialogs.ImportOpcUaDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:controls="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:vm="clr-namespace:DMS.WPF.ViewModels.Dialogs"
|
||||
Title="从OPC UA服务器导入变量"
|
||||
d:DataContext="{d:DesignInstance vm:ImportOpcUaDialogViewModel}"
|
||||
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
|
||||
PrimaryButtonText="导入"
|
||||
SecondaryButtonClick="ContentDialog_SecondaryButtonClick"
|
||||
SecondaryButtonText="取消"
|
||||
mc:Ignorable="d">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="2*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 节点树 -->
|
||||
<TreeView
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Margin="0,0,10,0"
|
||||
ItemsSource="{Binding OpcUaNodes}"
|
||||
SelectedItemChanged="TreeView_SelectedItemChanged">
|
||||
<TreeView.Style>
|
||||
<Style BasedOn="{StaticResource {x:Type TreeView}}" TargetType="TreeView">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TreeView.Style>
|
||||
<TreeView.ItemTemplate>
|
||||
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
|
||||
<TextBlock Text="{Binding DisplayName}" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
|
||||
<!-- 变量列表 -->
|
||||
<DataGrid
|
||||
Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True"
|
||||
ItemsSource="{Binding SelectedNodeVariables}"
|
||||
SelectionChanged="Selector_OnSelectionChanged">
|
||||
<DataGrid.Style>
|
||||
<Style BasedOn="{StaticResource {x:Type DataGrid}}" TargetType="DataGrid">
|
||||
<Setter Property="Visibility" Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}" Value="True">
|
||||
<Setter Property="Visibility" Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.Style>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding DataContext.SelectAllVariables, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.HeaderTemplate>
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelect, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Binding="{Binding Name}" Header="名称" />
|
||||
<DataGridTextColumn Binding="{Binding OpcUaNodeId}" Header="节点ID" />
|
||||
<DataGridTextColumn Binding="{Binding SignalType}" Header="数据类型" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</controls:ContentDialog>
|
||||
@@ -6,17 +6,17 @@ using iNKORE.UI.WPF.Modern.Controls;
|
||||
namespace DMS.WPF.Views.Dialogs;
|
||||
|
||||
/// <summary>
|
||||
/// OpcUaImportDialog.xaml 的交互逻辑
|
||||
/// ImportOpcUaDialog.xaml 的交互逻辑
|
||||
/// </summary>
|
||||
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;
|
||||
@@ -1,96 +0,0 @@
|
||||
<controls:ContentDialog x:Class="DMS.WPF.Views.Dialogs.OpcUaImportDialog"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:controls="http://schemas.inkore.net/lib/ui/wpf/modern"
|
||||
xmlns:vm="clr-namespace:DMS.WPF.ViewModels.Dialogs"
|
||||
mc:Ignorable="d"
|
||||
d:DataContext="{d:DesignInstance vm:OpcUaImportDialogViewModel}"
|
||||
Title="从OPC UA服务器导入变量"
|
||||
PrimaryButtonText="导入"
|
||||
SecondaryButtonText="取消"
|
||||
PrimaryButtonClick="ContentDialog_PrimaryButtonClick"
|
||||
SecondaryButtonClick="ContentDialog_SecondaryButtonClick"
|
||||
>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="2*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- 节点树 -->
|
||||
<TreeView Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
ItemsSource="{Binding OpcUaNodes}"
|
||||
Margin="0,0,10,0"
|
||||
SelectedItemChanged="TreeView_SelectedItemChanged">
|
||||
<TreeView.Style>
|
||||
<Style TargetType="TreeView"
|
||||
BasedOn="{StaticResource {x:Type TreeView}}">
|
||||
<Setter Property="Visibility"
|
||||
Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}"
|
||||
Value="True">
|
||||
<Setter Property="Visibility"
|
||||
Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</TreeView.Style>
|
||||
<TreeView.ItemTemplate>
|
||||
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
|
||||
<TextBlock Text="{Binding DisplayName}" />
|
||||
</HierarchicalDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
|
||||
<!-- 变量列表 -->
|
||||
<DataGrid Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
ItemsSource="{Binding SelectedNodeVariables}"
|
||||
SelectionChanged="Selector_OnSelectionChanged"
|
||||
AutoGenerateColumns="False"
|
||||
IsReadOnly="True">
|
||||
<DataGrid.Style>
|
||||
<Style TargetType="DataGrid"
|
||||
BasedOn="{StaticResource {x:Type DataGrid}}">
|
||||
<Setter Property="Visibility"
|
||||
Value="Collapsed" />
|
||||
<Style.Triggers>
|
||||
<DataTrigger Binding="{Binding IsConnected}"
|
||||
Value="True">
|
||||
<Setter Property="Visibility"
|
||||
Value="Visible" />
|
||||
</DataTrigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</DataGrid.Style>
|
||||
<DataGrid.Columns>
|
||||
<DataGridTemplateColumn>
|
||||
<DataGridTemplateColumn.HeaderTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox
|
||||
IsChecked="{Binding DataContext.SelectAllVariables, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.HeaderTemplate>
|
||||
<DataGridTemplateColumn.CellTemplate>
|
||||
<DataTemplate>
|
||||
<CheckBox IsChecked="{Binding IsSelect, UpdateSourceTrigger=PropertyChanged}" />
|
||||
</DataTemplate>
|
||||
</DataGridTemplateColumn.CellTemplate>
|
||||
</DataGridTemplateColumn>
|
||||
<DataGridTextColumn Header="名称"
|
||||
Binding="{Binding Name}" />
|
||||
<DataGridTextColumn Header="节点ID"
|
||||
Binding="{Binding OpcUaNodeId}" />
|
||||
<DataGridTextColumn Header="数据类型"
|
||||
Binding="{Binding SignalType}" />
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
</Grid>
|
||||
</controls:ContentDialog>
|
||||
6
DMS.sln
6
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
|
||||
|
||||
44
README.md
44
README.md
@@ -1 +1,45 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user