This commit is contained in:
2248356998 qq.com
2025-09-23 12:02:17 +08:00
parent cf6e8b58f0
commit f9cc1cbb05
27 changed files with 337 additions and 364 deletions

View File

@@ -5,7 +5,7 @@
<ItemGroup>
<ProjectReference Include="..\ThingsGateway.Admin.Application\ThingsGateway.Admin.Application.csproj" />
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.0" />
<PackageReference Include="BootstrapBlazor.Chart" Version="9.0.1" />
<PackageReference Include="BootstrapBlazor.UniverSheet" Version="9.0.5" />
</ItemGroup>

View File

@@ -1,9 +1,9 @@
<Project>
<PropertyGroup>
<PluginVersion>10.11.64</PluginVersion>
<ProPluginVersion>10.11.64</ProPluginVersion>
<DefaultVersion>10.11.64</DefaultVersion>
<PluginVersion>10.11.65</PluginVersion>
<ProPluginVersion>10.11.65</ProPluginVersion>
<DefaultVersion>10.11.65</DefaultVersion>
<AuthenticationVersion>10.11.6</AuthenticationVersion>
<SourceGeneratorVersion>10.11.6</SourceGeneratorVersion>
<NET8Version>8.0.20</NET8Version>

View File

@@ -10,7 +10,7 @@
namespace ThingsGateway.Gateway.Application;
public interface IMemoryVariableRuntime
public interface IMemoryVariableRpc
{
OperResult MemoryVariableRpc(string value, CancellationToken cancellationToken = default);
}

View File

@@ -58,27 +58,27 @@ internal sealed class RpcService : IRpcService
deviceDatas.ForEach(a => results.TryAdd(a.Key, new()));
// 对每个要操作的变量进行检查和处理(内存变量)
foreach (var item in deviceDatas.Where(a => a.Key.IsNullOrEmpty()).SelectMany(a => a.Value))
foreach (var item in deviceDatas.Where(a => a.Key.IsNullOrEmpty() || a.Key.Equals("Memory", StringComparison.OrdinalIgnoreCase)).SelectMany(a => a.Value))
{
// 查找变量是否存在
if (!(GlobalData.MemoryVariables.TryGetValue(item.Key, out var tag) &&
tag is IMemoryVariableRuntime memoryVariableRuntime))
tag is IMemoryVariableRpc memoryVariableRuntime))
{
// 如果变量不存在,则添加错误信息到结果中并继续下一个变量的处理
results[string.Empty].TryAdd(item.Key, new OperResult(Localizer["VariableNotNull", item.Key]));
results["Memory"].TryAdd(item.Key, new OperResult(Localizer["VariableNotNull", item.Key]));
continue;
}
// 检查变量的保护类型和远程写入权限
if (tag.ProtectType == ProtectTypeEnum.ReadOnly)
{
results[string.Empty].TryAdd(item.Key, new OperResult(Localizer["VariableReadOnly", item.Key]));
results["Memory"].TryAdd(item.Key, new OperResult(Localizer["VariableReadOnly", item.Key]));
continue;
}
if (!tag.RpcWriteEnable)
{
results[string.Empty].TryAdd(item.Key, new OperResult(Localizer["VariableWriteDisable", item.Key]));
results["Memory"].TryAdd(item.Key, new OperResult(Localizer["VariableWriteDisable", item.Key]));
continue;
}
@@ -90,7 +90,7 @@ internal sealed class RpcService : IRpcService
var end = DateTime.Now;
string operObj = tag.Name;
string parJson = deviceDatas[string.Empty][tag.Name];
string parJson = deviceDatas["Memory"][tag.Name];
if (!variableResult.IsSuccess || _rpcLogOptions.SuccessLog)
{
@@ -118,19 +118,19 @@ internal sealed class RpcService : IRpcService
variableResult = result1;
}
results[string.Empty].Add(tag.Name, variableResult);
results["Memory"].Add(tag.Name, variableResult);
}
catch (Exception ex)
{
// 将异常信息添加到结果字典中
results[string.Empty].Add(tag.Name, new OperResult(ex));
results["Memory"].Add(tag.Name, new OperResult(ex));
}
}
var deviceDict = GlobalData.Devices;
// 对每个要操作的变量进行检查和处理(设备变量)
foreach (var deviceData in deviceDatas.Where(a => !a.Key.IsNullOrEmpty()))
foreach (var deviceData in deviceDatas.Where(a => (!a.Key.IsNullOrEmpty() && !a.Key.Equals("Memory", StringComparison.OrdinalIgnoreCase))))
{
// 查找设备是否存在
if (!deviceDict.TryGetValue(deviceData.Key, out var device))

View File

@@ -23,7 +23,7 @@
LogLevel=a;
if(LogLevelChanged!=null)
{
await LogLevelChanged?.Invoke(a);
await LogLevelChanged.Invoke(a);
}
}" LogPath=@LogPath HeaderText=@HeaderText></ThingsGateway.Debug.LogConsole>

View File

@@ -47,7 +47,7 @@ public partial class RedundancyOptionsPage
LogLevel = logLevel;
if (LogLevelChanged != null)
{
await LogLevelChanged?.Invoke(LogLevel);
await LogLevelChanged.Invoke(LogLevel);
}
await InvokeAsync(StateHasChanged);
}

View File

@@ -11,6 +11,9 @@
//修改自https://github.com/dathlin/OpcUaHelper 与OPC基金会net库
using System.Collections.Concurrent;
#if NET8_0_OR_GREATER
using System.Collections.Frozen;
#endif
namespace ThingsGateway.Foundation.OpcUa;
@@ -28,7 +31,7 @@ public delegate void LogEventHandler(byte level, object sender, string message,
/// <summary>
/// OpcUaMaster
/// </summary>
public class OpcUaMaster : IDisposable, IAsyncDisposable
public class OpcUaMaster : IAsyncDisposable
{
#region
@@ -157,9 +160,9 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
},
};
certificateValidator.Update(m_configuration);
Task.Run(() => certificateValidator.UpdateAsync(m_configuration)).GetAwaiter().GetResult();
m_configuration.Validate(ApplicationType.Client);
Task.Run(() => m_configuration.ValidateAsync(ApplicationType.Client)).GetAwaiter().GetResult();
m_application.ApplicationConfiguration = m_configuration;
}
@@ -367,19 +370,21 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
/// </summary>
/// <param name="tagParent">方法的父节点tag</param>
/// <param name="tag">方法的节点tag</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <param name="args">传递的参数</param>
/// <returns>输出的结果值</returns>
public object[] CallMethodByNodeId(string tagParent, string tag, params object[] args)
public async Task<IList<object>> CallMethodByNodeIdAsync(string tagParent, string tag, CancellationToken cancellationToken, params object[] args)
{
if (m_session == null)
{
return null;
}
IList<object> outputArguments = m_session.Call(
IList<object> outputArguments = await m_session.CallAsync(
new NodeId(tagParent),
new NodeId(tag),
args);
cancellationToken,
args).ConfigureAwait(false);
return outputArguments.ToArray();
}
@@ -392,19 +397,6 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
await ConnectAsync(OpcUaProperty.OpcUrl, cancellationToken).ConfigureAwait(false);
}
/// <summary>
/// 断开连接。
/// </summary>
public void Disconnect()
{
PrivateDisconnect();
// disconnect any existing session.
if (m_session != null)
{
m_session = null;
}
}
/// <summary>
/// 断开连接。
/// </summary>
@@ -417,14 +409,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
m_session = null;
}
}
/// <inheritdoc/>
public void Dispose()
{
Disconnect();
_variableDicts?.Clear();
_subscriptionDicts?.Clear();
waitLock?.Dispose();
}
public async ValueTask DisposeAsync()
{
await DisconnectAsync().ConfigureAwait(false);
@@ -573,8 +558,9 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
/// 读取一个节点的所有属性
/// </summary>
/// <param name="tag">节点信息</param>
/// <param name="cancellationToken">cancellationToken</param>
/// <returns>节点的特性值</returns>
public OPCNodeAttribute[] ReadNoteAttributes(string tag)
public async Task<OPCNodeAttribute[]> ReadNoteAttributesAsync(string tag, CancellationToken cancellationToken)
{
NodeId sourceId = new(tag);
ReadValueIdCollection nodesToRead = new();
@@ -608,7 +594,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
};
// fetch property references from the server.
ReferenceDescriptionCollection references = OpcUaUtils.Browse(m_session, nodesToBrowse, false);
ReferenceDescriptionCollection references = await OpcUaUtils.BrowseAsync(m_session, nodesToBrowse, false, cancellationToken).ConfigureAwait(false);
if (references == null)
{
@@ -631,15 +617,15 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
nodesToRead.Add(nodeToRead);
}
// read all values.
m_session.Read(
null,
0,
TimestampsToReturn.Neither,
nodesToRead,
out DataValueCollection results,
out DiagnosticInfoCollection diagnosticInfos);
// 读取当前的值
var result = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
nodesToRead,
cancellationToken).ConfigureAwait(false);
var results = result.Results;
var diagnosticInfos = result.DiagnosticInfos;
ClientBase.ValidateResponse(results, nodesToRead);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToRead);
@@ -661,7 +647,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
}
// get the name of the attribute.
item.Name = Attributes.GetBrowseName(nodesToRead[ii].AttributeId);
item.Name = GetBrowseName(nodesToRead[ii].AttributeId);
// display any unexpected error.
if (StatusCode.IsBad(results[ii].StatusCode))
@@ -726,7 +712,34 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
return nodeAttribute.ToArray();
}
/// <summary>
/// Returns the browse name for the attribute.
/// </summary>
public static string GetBrowseName(uint identifier)
{
return s_attributesIdToName.Value.TryGetValue(identifier, out string name)
? name : string.Empty;
}
/// <summary>
/// Creates a dictionary of identifiers to browse names for the attributes.
/// </summary>
private static readonly Lazy<IReadOnlyDictionary<long, string>> s_attributesIdToName =
new(() =>
{
System.Reflection.FieldInfo[] fields = typeof(Attributes).GetFields(
System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
var keyValuePairs = new Dictionary<long, string>();
foreach (System.Reflection.FieldInfo field in fields)
{
keyValuePairs.TryAdd(Convert.ToInt64(field.GetValue(typeof(Attributes))), field.Name);
}
#if NET8_0_OR_GREATER
return keyValuePairs.ToFrozenDictionary();
#else
return new System.Collections.ObjectModel.ReadOnlyDictionary<long, string>(keyValuePairs);
#endif
});
/// <summary>
/// 移除所有的订阅消息
/// </summary>
@@ -825,7 +838,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
{
foreach (var value in monitoreditem.DequeueValues())
{
var variableNode = ReadNode(monitoreditem.StartNodeId.ToString(), false, StatusCode.IsGood(value.StatusCode));
var variableNode = ReadNode(monitoreditem.StartNodeId.ToString(), false, StatusCode.IsGood(value.StatusCode)).GetAwaiter().GetResult();
if (value.Value != null)
{
@@ -861,7 +874,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
/// <returns></returns>
public async Task CheckApplicationInstanceCertificate()
{
await m_application.CheckApplicationInstanceCertificates(true, 1200).ConfigureAwait(false);
await m_application.CheckApplicationInstanceCertificatesAsync(true, 1200).ConfigureAwait(false);
}
SemaphoreSlim waitLock = new(1, 1);
@@ -910,11 +923,12 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
}
//创建本地证书
if (useSecurity)
await m_application.CheckApplicationInstanceCertificates(true, 1200, cancellationToken).ConfigureAwait(false);
m_session = await Opc.Ua.Client.Session.Create(
await m_application.CheckApplicationInstanceCertificatesAsync(true, 1200, cancellationToken).ConfigureAwait(false);
m_session = await Opc.Ua.Client.Session.CreateAsync(
DefaultSessionFactory.Instance,
m_configuration,
(ITransportWaitingConnection)null,
endpoint,
false,
OpcUaProperty.CheckDomain,
@@ -940,29 +954,6 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
}
}
private void PrivateDisconnect()
{
bool state = m_session?.Connected == true;
if (m_reConnectHandler != null)
{
try { m_reConnectHandler.Dispose(); } catch { }
m_reConnectHandler = null;
}
if (m_session != null)
{
m_session.KeepAlive -= Session_KeepAlive;
m_session.Close(10000);
m_session.Dispose();
m_session = null;
}
if (state)
{
Log(2, null, "Disconnected");
DoConnectComplete(false);
}
}
private async Task PrivateDisconnectAsync()
{
bool state = m_session?.Connected == true;
@@ -1035,7 +1026,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
/// <summary>
/// 从服务器或缓存读取节点
/// </summary>
private VariableNode ReadNode(string nodeIdStr, bool isOnlyServer = true, bool cache = true)
private async Task<VariableNode> ReadNode(string nodeIdStr, bool isOnlyServer = true, bool cache = true, CancellationToken cancellationToken = default)
{
if (!isOnlyServer)
{
@@ -1061,16 +1052,17 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
}
// read from server.
DataValueCollection values = null;
DiagnosticInfoCollection diagnosticInfos = null;
ResponseHeader responseHeader = m_session.Read(
null,
0,
TimestampsToReturn.Neither,
itemsToRead,
out values,
out diagnosticInfos);
var result = await m_session.ReadAsync(
null,
0,
TimestampsToReturn.Neither,
itemsToRead,
cancellationToken).ConfigureAwait(false);
var values = result.Results;
var diagnosticInfos = result.DiagnosticInfos;
var responseHeader = result.ResponseHeader;
VariableNode variableNode = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader).FirstOrDefault();
@@ -1121,7 +1113,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
VariableNode variableNode = GetVariableNodes(itemsToRead, values, diagnosticInfos, responseHeader).FirstOrDefault();
if (OpcUaProperty.LoadType && variableNode.DataType != NodeId.Null && (await TypeInfo.GetBuiltInTypeAsync(variableNode.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false)) == BuiltInType.ExtensionObject)
await typeSystem.LoadType(variableNode.DataType, ct: cancellationToken).ConfigureAwait(false);
await typeSystem.LoadTypeAsync(variableNode.DataType, ct: cancellationToken).ConfigureAwait(false);
if (cache)
_variableDicts.AddOrUpdate(nodeIdStr, a => variableNode, (a, b) => variableNode);
@@ -1222,7 +1214,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
_variableDicts.AddOrUpdate(nodeIdStrs[i], a => node, (a, b) => node);
if (node.DataType != NodeId.Null && (await TypeInfo.GetBuiltInTypeAsync(node.DataType, m_session.SystemContext.TypeTable, cancellationToken).ConfigureAwait(false)) == BuiltInType.ExtensionObject)
{
await typeSystem.LoadType(node.DataType, ct: cancellationToken).ConfigureAwait(false);
await typeSystem.LoadTypeAsync(node.DataType, ct: cancellationToken).ConfigureAwait(false);
}
}
result.Add(node);
@@ -1314,7 +1306,7 @@ public class OpcUaMaster : IDisposable, IAsyncDisposable
continue;
}
item.Name = Attributes.GetBrowseName(nodesToRead[ii].AttributeId);
item.Name = GetBrowseName(nodesToRead[ii].AttributeId);
if (StatusCode.IsBad(nodeValue.StatusCode))
{
item.Type = Utils.Format("{0}", Attributes.GetDataTypeId(nodesToRead[ii].AttributeId));

View File

@@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.4" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes" Version="1.5.376.244" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes" Version="1.5.377.21" />
</ItemGroup>
<ItemGroup>

View File

@@ -56,133 +56,6 @@ public static class OpcUaUtils
}
}
/// <summary>
/// Browses the address space and returns the references found.
/// </summary>
/// <param name="session">The session.</param>
/// <param name="nodesToBrowse">The set of browse operations to perform.</param>
/// <param name="throwOnError">if set to <c>true</c> a exception will be thrown on an error.</param>
/// <returns>
/// The references found. Null if an error occurred.
/// </returns>
public static ReferenceDescriptionCollection Browse(ISession session, BrowseDescriptionCollection nodesToBrowse, bool throwOnError)
{
try
{
ReferenceDescriptionCollection references = new();
BrowseDescriptionCollection unprocessedOperations = new();
while (nodesToBrowse.Count > 0)
{
// start the browse operation.
session.Browse(
null,
null,
0,
nodesToBrowse,
out BrowseResultCollection results,
out DiagnosticInfoCollection diagnosticInfos);
ClientBase.ValidateResponse(results, nodesToBrowse);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, nodesToBrowse);
ByteStringCollection continuationPoints = new();
for (int ii = 0; ii < nodesToBrowse.Count; ii++)
{
// check for error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
// this error indicates that the server does not have enough simultaneously active
// continuation points. This request will need to be resent after the other operations
// have been completed and their continuation points released.
if (results[ii].StatusCode == StatusCodes.BadNoContinuationPoints)
{
unprocessedOperations.Add(nodesToBrowse[ii]);
}
continue;
}
// check if all references have been fetched.
if (results[ii].References.Count == 0)
{
continue;
}
// save results.
references.AddRange(results[ii].References);
// check for continuation point.
if (results[ii].ContinuationPoint != null)
{
continuationPoints.Add(results[ii].ContinuationPoint);
}
}
// process continuation points.
ByteStringCollection revisedContiuationPoints = new();
while (continuationPoints.Count > 0)
{
// continue browse operation.
session.BrowseNext(
null,
true,
continuationPoints,
out results,
out diagnosticInfos);
ClientBase.ValidateResponse(results, continuationPoints);
ClientBase.ValidateDiagnosticInfos(diagnosticInfos, continuationPoints);
for (int ii = 0; ii < continuationPoints.Count; ii++)
{
// check for error.
if (StatusCode.IsBad(results[ii].StatusCode))
{
continue;
}
// check if all references have been fetched.
if (results[ii].References.Count == 0)
{
continue;
}
// save results.
references.AddRange(results[ii].References);
// check for continuation point.
if (results[ii].ContinuationPoint != null)
{
revisedContiuationPoints.Add(results[ii].ContinuationPoint);
}
}
// check if browsing must continue;
revisedContiuationPoints = continuationPoints;
}
// check if unprocessed results exist.
nodesToBrowse = unprocessedOperations;
}
// return complete list.
return references;
}
catch (Exception exception)
{
if (throwOnError)
{
throw new ServiceResultException(exception, StatusCodes.BadUnexpectedError);
}
return null;
}
}
/// <summary>
/// 浏览地址空间
/// </summary>
@@ -570,7 +443,7 @@ public static class OpcUaUtils
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <returns>A list of server urls.</returns>
public static IList<string> DiscoverServers(ApplicationConfiguration configuration)
public static async Task<IList<string>> DiscoverServers(ApplicationConfiguration configuration)
{
List<string> serverUrls = new();
@@ -581,7 +454,7 @@ public static class OpcUaUtils
// Connect to the local discovery server and find the available servers.
using (DiscoveryClient client = DiscoveryClient.Create(new Uri("opc.tcp://localhost:4840"), endpointConfiguration))
{
ApplicationDescriptionCollection servers = client.FindServers(null);
ApplicationDescriptionCollection servers = await client.FindServersAsync(null).ConfigureAwait(false);
// populate the drop down list with the discovery URLs for the available servers.
for (int ii = 0; ii < servers.Count; ii++)
@@ -641,7 +514,7 @@ public static class OpcUaUtils
/// <summary>
/// 指定的属性的显示文本。
/// </summary>
public static string GetAttributeDisplayText(ISession session, uint attributeId, Variant value)
public static async Task<string> GetAttributeDisplayTextAsync(ISession session, uint attributeId, Variant value)
{
if (value == Variant.Null)
{
@@ -677,7 +550,7 @@ public static class OpcUaUtils
case Attributes.DataType:
{
return session.NodeCache.GetDisplayText(value.Value as NodeId);
return await session.NodeCache.GetDisplayTextAsync(value.Value as NodeId).ConfigureAwait(false);
}
case Attributes.ValueRank:
@@ -727,102 +600,6 @@ public static class OpcUaUtils
return value.ToString();
}
/// <summary>
/// Finds the endpoint that best matches the current settings.
/// </summary>
/// <param name="discoveryUrl">The discovery URL.</param>
/// <param name="useSecurity">if set to <c>true</c> select an endpoint that uses security.</param>
/// <returns>The best available endpoint.</returns>
public static EndpointDescription SelectEndpoint(string discoveryUrl, bool useSecurity)
{
// needs to add the '/discovery' back onto non-UA TCP URLs.
if (!discoveryUrl.StartsWith(Utils.UriSchemeOpcTcp))
{
if (!discoveryUrl.EndsWith("/discovery"))
{
discoveryUrl += "/discovery";
}
}
// parse the selected URL.
Uri uri = new(discoveryUrl);
// set a short timeout because this is happening in the drop down event.
EndpointConfiguration configuration = EndpointConfiguration.Create();
configuration.OperationTimeout = 5000;
EndpointDescription selectedEndpoint = null;
// Connect to the server's discovery endpoint and find the available configuration.
using (DiscoveryClient client = DiscoveryClient.Create(uri, configuration))
{
EndpointDescriptionCollection endpoints = client.GetEndpoints(null);
// select the best endpoint to use based on the selected URL and the UseSecurity checkbox.
for (int ii = 0; ii < endpoints.Count; ii++)
{
EndpointDescription endpoint = endpoints[ii];
// check for a match on the URL scheme.
if (endpoint.EndpointUrl.StartsWith(uri.Scheme))
{
// check if security was requested.
if (useSecurity)
{
if (endpoint.SecurityMode == MessageSecurityMode.None)
{
continue;
}
}
else
{
if (endpoint.SecurityMode != MessageSecurityMode.None)
{
continue;
}
}
// pick the first available endpoint by default.
selectedEndpoint ??= endpoint;
// The security level is a relative measure assigned by the server to the
// endpoints that it returns. Clients should always pick the highest level
// unless they have a reason not too.
if (endpoint.SecurityLevel > selectedEndpoint.SecurityLevel)
{
selectedEndpoint = endpoint;
}
}
}
// pick the first available endpoint by default.
if (selectedEndpoint == null && endpoints.Count > 0)
{
selectedEndpoint = endpoints[0];
}
}
// if a server is behind a firewall it may return URLs that are not accessible to the client.
// This problem can be avoided by assuming that the domain in the URL used to call
// GetEndpoints can be used to access any of the endpoints. This code makes that conversion.
// Note that the conversion only makes sense if discovery uses the same protocol as the endpoint.
Uri endpointUrl = Utils.ParseUri(selectedEndpoint.EndpointUrl);
if (endpointUrl != null && endpointUrl.Scheme == uri.Scheme)
{
UriBuilder builder = new(endpointUrl)
{
Host = uri.DnsSafeHost,
Port = uri.Port
};
selectedEndpoint.EndpointUrl = builder.ToString();
}
// return the selected endpoint.
return selectedEndpoint;
}
/// <summary>
/// 返回一组相对路径的节点id
/// </summary>

View File

@@ -31,6 +31,15 @@ public abstract class DynamicSQLBase
/// <returns></returns>
public virtual Task DBInsertable(ISqlSugarClient db, IEnumerable<object> datas, CancellationToken cancellationToken)
{
throw new NotSupportedException();
return Task.CompletedTask;
}
/// <summary>
/// 删除n天前数据
/// </summary>
/// <returns></returns>
public virtual Task<int> DBDeleteable(ISqlSugarClient db, int days, CancellationToken cancellationToken)
{
return Task.FromResult(0);
}
}

View File

@@ -33,6 +33,9 @@ public class RealDBProducerProperty : BusinessPropertyWithCacheInterval
[Required]
public string StringTableName { get; set; } = "historyStringValue";
[DynamicProperty]
public int SaveDays { get; set; } = 3650;
public string NumberTableNameLow => NumberTableName.ToLower();
public string StringTableNameLow => StringTableName.ToLower();

View File

@@ -1,6 +1,7 @@
{
"ThingsGateway.Plugin.DB.RealDBProducerProperty": {
"BigTextConnectStr": "ConnectString",
"SaveDays": "SaveDays",
"BigTextScriptHistoryTable": "DynamicScriptHistoryTable",
"RealTableBusinessInterval": "RealTableBusinessInterval",
"StringTableName": "StringTableName",
@@ -25,6 +26,7 @@
"Value": "Value"
},
"ThingsGateway.Plugin.SqlDB.SqlDBProducerProperty": {
"SaveDays": "SaveDays",
"BigTextConnectStr": "ConnectString",
"BigTextScriptHistoryTable": "DynamicScriptHistoryTable",
"BigTextScriptRealTable": "DynamicScriptRealTable",
@@ -87,6 +89,7 @@
"Remark5": "Remark5"
},
"ThingsGateway.Plugin.DB.SQLHistoryAlarmProperty": {
"SaveDays": "SaveDays",
"BigTextConnectStr": "ConnectString",
"DbType": "DbType",
"TableName": "TableName",

View File

@@ -1,6 +1,7 @@
{
"ThingsGateway.Plugin.DB.RealDBProducerProperty": {
"BigTextConnectStr": "连接字符串",
"SaveDays": "保留天数",
"BigTextScriptHistoryTable": "历史表动态脚本",
"RealTableBusinessInterval": "实时表定时上传间隔",
"NumberTableName": "数值变量历史表名称",
@@ -25,6 +26,7 @@
"Value": "变量值"
},
"ThingsGateway.Plugin.SqlDB.SqlDBProducerProperty": {
"SaveDays": "保留天数",
"BigTextConnectStr": "链接字符串",
"BigTextScriptHistoryTable": "历史表动态脚本",
"BigTextScriptRealTable": "实时表动态脚本",
@@ -87,6 +89,7 @@
"Remark5": "备用5"
},
"ThingsGateway.Plugin.DB.SQLHistoryAlarmProperty": {
"SaveDays": "保留天数",
"BigTextConnectStr": "链接字符串",
"DbType": "数据库类型",
"TableName": "表名称",

View File

@@ -16,6 +16,7 @@ using ThingsGateway.Debug;
using ThingsGateway.Foundation;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Extension;
using ThingsGateway.NewLife.Threading;
using ThingsGateway.Plugin.DB;
using ThingsGateway.SqlSugar;
@@ -69,6 +70,56 @@ public partial class QuestDBProducer : BusinessBaseWithCacheIntervalVariable
#if !Management
protected override List<IScheduledTask> ProtectedGetTasks(CancellationToken cancellationToken)
{
var list = base.ProtectedGetTasks(cancellationToken);
list.Add(ScheduledTaskHelper.GetTask("0 0 * * *", DeleteByDayAsync, null, LogMessage, cancellationToken));
return list;
}
private async Task DeleteByDayAsync(object? state, CancellationToken cancellationToken)
{
try
{
using var db = BusinessDatabaseUtil.GetDb(_driverPropertys.DbType, _driverPropertys.BigTextConnectStr);
if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
{
var hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
await hisModel.DBDeleteable(db, _driverPropertys.SaveDays, cancellationToken).ConfigureAwait(false);
}
else
{
{
var time = TimerX.Now - TimeSpan.FromDays(-_driverPropertys.SaveDays);
string sql = $"""
ALTER TABLE {_driverPropertys.NumberTableNameLow}
DROP PARTITION
WHERE createtime < to_timestamp('{time.ToString("yyyy-MM-dd:HH:mm:ss")}', 'yyyy-MM-dd:HH:mm:ss');
""";
await db.Ado.ExecuteCommandAsync("", default, cancellationToken: cancellationToken).ConfigureAwait(false);
sql = $"""
ALTER TABLE {_driverPropertys.StringTableNameLow}
DROP PARTITION
WHERE createtime < to_timestamp('{time.ToString("yyyy-MM-dd:HH:mm:ss")}', 'yyyy-MM-dd:HH:mm:ss');
""";
await db.Ado.ExecuteCommandAsync("", default, cancellationToken: cancellationToken).ConfigureAwait(false);
LogMessage?.LogInformation($"Clean up historical data from {_driverPropertys.SaveDays} days ago");
}
}
}
catch (Exception ex)
{
LogMessage?.LogWarning(ex, "Clearing historical data error");
}
}
protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
{
_db = BusinessDatabaseUtil.GetDb(_driverPropertys.DbType, _driverPropertys.BigTextConnectStr);

View File

@@ -18,6 +18,7 @@ using ThingsGateway.Foundation;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.DictionaryExtensions;
using ThingsGateway.NewLife.Extension;
using ThingsGateway.NewLife.Threading;
using ThingsGateway.Plugin.DB;
using ThingsGateway.SqlSugar;
@@ -70,7 +71,82 @@ public partial class SqlDBProducer : BusinessBaseWithCacheIntervalVariable
return $" {nameof(SqlDBProducer)}";
}
#if !Management
protected override List<IScheduledTask> ProtectedGetTasks(CancellationToken cancellationToken)
{
var list = base.ProtectedGetTasks(cancellationToken);
list.Add(ScheduledTaskHelper.GetTask("0/10 * * * * *", DeleteByDayAsync, null, LogMessage, cancellationToken));
return list;
}
private async Task DeleteByDayAsync(object? state, CancellationToken cancellationToken)
{
try
{
using var db = SqlDBBusinessDatabaseUtil.GetDb(_driverPropertys);
if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
{
var hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
if (_driverPropertys.IsHistoryDB)
{
await hisModel.DBDeleteable(db, _driverPropertys.SaveDays, cancellationToken).ConfigureAwait(false);
}
}
else
{
if (_driverPropertys.IsHistoryDB)
{
{
var time = TimerX.Now - TimeSpan.FromDays(-_driverPropertys.SaveDays);
var tableNames = db.SplitHelper<SQLHistoryValue>().GetTables();//根据时间获取表名
var filtered = tableNames.Where(a => a.Date < time).ToList();
// 去掉最后一个
var oldTable = filtered.Take(filtered.Count - 1);
foreach (var table in oldTable)
{
db.DbMaintenance.DropTable(table.TableName);
}
var deldata = filtered.LastOrDefault();
if (deldata != null)
{
await db.Deleteable<SQLHistoryValue>().AS(deldata.TableName).Where(a => a.CreateTime < time).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
}
}
{
var time = TimerX.Now - TimeSpan.FromDays(-_driverPropertys.SaveDays);
var tableNames = db.SplitHelper<SQLNumberHistoryValue>().GetTables();//根据时间获取表名
var filtered = tableNames.Where(a => a.Date < time).ToList();
// 去掉最后一个
var oldTable = filtered.Take(filtered.Count - 1);
foreach (var table in oldTable)
{
db.DbMaintenance.DropTable(table.TableName);
}
var deldata = filtered.LastOrDefault();
if (deldata != null)
{
await db.Deleteable<SQLNumberHistoryValue>().AS(deldata.TableName).Where(a => a.CreateTime < time).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
}
}
}
LogMessage?.LogInformation($"Clean up historical data from {_driverPropertys.SaveDays} days ago");
}
}
catch (Exception ex)
{
LogMessage?.LogWarning(ex, "Clearing historical data error");
}
}
public async Task<SqlSugarPagedList<IDBHistoryValue>> GetDBHistoryValuePagesAsync(DBHistoryValuePageInput input)
{

View File

@@ -58,6 +58,9 @@ public class SqlDBProducerProperty : BusinessPropertyWithCacheInterval
[DynamicProperty]
public DbType DbType { get; set; } = DbType.SqlServer;
[DynamicProperty]
public int SaveDays { get; set; } = 3650;
[DynamicProperty]
public SqlDBSplitType SqlDBSplitType { get; set; } = SqlDBSplitType.Week;

View File

@@ -16,6 +16,7 @@ using ThingsGateway.Debug;
using ThingsGateway.Foundation;
using ThingsGateway.NewLife;
using ThingsGateway.NewLife.Extension;
using ThingsGateway.NewLife.Threading;
using ThingsGateway.SqlSugar;
namespace ThingsGateway.Plugin.DB;
@@ -65,6 +66,44 @@ public partial class SqlHistoryAlarm : BusinessBaseWithCacheAlarm
}
#if !Management
protected override List<IScheduledTask> ProtectedGetTasks(CancellationToken cancellationToken)
{
var list = base.ProtectedGetTasks(cancellationToken);
list.Add(ScheduledTaskHelper.GetTask("0 0 * * *", DeleteByDayAsync, null, LogMessage, cancellationToken));
return list;
}
private async Task DeleteByDayAsync(object? state, CancellationToken cancellationToken)
{
try
{
using var db = BusinessDatabaseUtil.GetDb((DbType)_driverPropertys.DbType, _driverPropertys.BigTextConnectStr);
if (!_driverPropertys.BigTextScriptHistoryTable.IsNullOrEmpty())
{
var hisModel = CSharpScriptEngineExtension.Do<DynamicSQLBase>(_driverPropertys.BigTextScriptHistoryTable);
await hisModel.DBDeleteable(db, _driverPropertys.SaveDays, cancellationToken).ConfigureAwait(false);
}
else
{
{
var time = TimerX.Now - TimeSpan.FromDays(-_driverPropertys.SaveDays);
await db.Deleteable<HistoryAlarm>().Where(a => a.EventTime < time).ExecuteCommandAsync(cancellationToken).ConfigureAwait(false);
LogMessage?.LogInformation($"Clean up historical data from {_driverPropertys.SaveDays} days ago");
}
}
}
catch (Exception ex)
{
LogMessage?.LogWarning(ex, "Clearing historical data error");
}
}
protected override async Task InitChannelAsync(IChannel? channel, CancellationToken cancellationToken)
{

View File

@@ -23,6 +23,10 @@ public class SqlHistoryAlarmProperty : BusinessPropertyWithCache
{
[DynamicProperty]
public DbType DbType { get; set; } = DbType.SqlServer;
[DynamicProperty]
public int SaveDays { get; set; } = 3650;
[DynamicProperty]
[Required]
public string TableName { get; set; } = "historyAlarm";

View File

@@ -130,6 +130,12 @@ public partial class TDengineDBProducer : BusinessBaseWithCacheIntervalVariable
`value` DOUBLE ) TAGS(`devicename` VARCHAR(100) ,`name` VARCHAR(100))
""";
await _db.Ado.ExecuteCommandAsync(sql, default, cancellationToken: cancellationToken).ConfigureAwait(false);
await _db.Ado.ExecuteCommandAsync($"ALTER STABLE `{_driverPropertys.StringTableNameLow}` KEEP 10;", default, cancellationToken: cancellationToken).ConfigureAwait(false);
await _db.Ado.ExecuteCommandAsync($"ALTER STABLE `{_driverPropertys.NumberTableNameLow}` KEEP 10;", default, cancellationToken: cancellationToken).ConfigureAwait(false);
}
await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
}

View File

@@ -250,7 +250,7 @@ public class ThingsGatewayNodeManager : CustomNodeManager2
/// <param name="disposing"></param>
protected override void Dispose(bool disposing)
{
NodeIdTags.Clear();
NodeIdTags?.Clear();
base.Dispose(disposing);
}
@@ -972,7 +972,7 @@ public class ThingsGatewayNodeManager : CustomNodeManager2
{
var str = variableRuntime.GetPropertyValue(_businessBase.DeviceId, nameof(OpcUaServerVariableProperty.DataType)) ?? "";
Type tp;
if (Enum.TryParse(str, out DataTypeEnum result))
if (Enum.TryParse(str, out DataTypeEnum result) && result != DataTypeEnum.Object)
{
tp = result.GetSystemType();
return DataNodeType(tp);

View File

@@ -57,7 +57,7 @@ public partial class ThingsGatewayServer : StandardServer
/// <inheritdoc/>
protected override ResourceManager CreateResourceManager(IServerInternal server, ApplicationConfiguration configuration)
{
ResourceManager resourceManager = new(server, configuration);
ResourceManager resourceManager = new(configuration);
System.Reflection.FieldInfo[] fields = typeof(StatusCodes).GetFields(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static);
@@ -117,7 +117,6 @@ public partial class ThingsGatewayServer : StandardServer
// 由应用程序决定如何验证用户身份令牌。
// 此函数为 X509 身份令牌创建验证器。
CreateUserIdentityValidators(configuration);
}
/// <inheritdoc/>
@@ -127,7 +126,7 @@ public partial class ThingsGatewayServer : StandardServer
base.OnServerStopping();
}
private void CreateUserIdentityValidators(ApplicationConfiguration configuration)
public async Task CreateUserIdentityValidators(ApplicationConfiguration configuration)
{
for (int ii = 0; ii < configuration.ServerConfiguration.UserTokenPolicies.Count; ii++)
{
@@ -141,7 +140,7 @@ public partial class ThingsGatewayServer : StandardServer
configuration.SecurityConfiguration.UserIssuerCertificates != null)
{
CertificateValidator certificateValidator = new();
certificateValidator.Update(configuration).GetAwaiter().GetResult();
await certificateValidator.UpdateAsync(configuration).ConfigureAwait(false);
certificateValidator.Update(configuration.SecurityConfiguration.UserIssuerCertificates,
configuration.SecurityConfiguration.TrustedUserCertificates,
configuration.SecurityConfiguration.RejectedCertificateStore);
@@ -153,7 +152,7 @@ public partial class ThingsGatewayServer : StandardServer
}
}
private void SessionManager_ImpersonateUser(Session session, ImpersonateEventArgs args)
private void SessionManager_ImpersonateUser(ISession session, ImpersonateEventArgs args)
{
// check for a user name cancellationToken.
@@ -226,7 +225,7 @@ public partial class ThingsGatewayServer : StandardServer
"Security cancellationToken is not a valid username cancellationToken. An empty password is not accepted.");
}
var sysUserService = App.RootServices.GetService<ISysUserService>();
var userInfo = sysUserService.GetUserByAccountAsync(userName, null).ConfigureAwait(true).GetAwaiter().GetResult();//获取用户信息
var userInfo = sysUserService.GetUserByAccountAsync(userName, null).ConfigureAwait(false).GetAwaiter().GetResult();//获取用户信息
if (userInfo == null)
{
// construct translation object with default text.

View File

@@ -112,8 +112,8 @@ public partial class OpcUaServer : BusinessBase
//Utils.SetLogger(new OpcUaLogger(LogMessage)); //调试用途
m_application = new ApplicationInstance();
m_configuration = GetDefaultConfiguration();
await m_configuration.Validate(ApplicationType.Server).ConfigureAwait(false);
m_configuration = await GetDefaultConfigurationAsync().ConfigureAwait(false);
await m_configuration.ValidateAsync(ApplicationType.Server).ConfigureAwait(false);
m_application.ApplicationConfiguration = m_configuration;
if (m_configuration.SecurityConfiguration.AutoAcceptUntrustedCertificates)
{
@@ -121,6 +121,7 @@ public partial class OpcUaServer : BusinessBase
}
m_server = new(this);
await m_server.CreateUserIdentityValidators(m_configuration).ConfigureAwait(false);
}
private void UaDispose()
{
@@ -142,7 +143,7 @@ public partial class OpcUaServer : BusinessBase
typeof(EncodeableFactory).GetField("s_globalFactory", BindingFlags.NonPublic | BindingFlags.Static)?.SetValue(null, new EncodeableFactory());
typeof(ServiceMessageContext).GetField("s_globalContext", BindingFlags.NonPublic | BindingFlags.Static)?.SetValue(null, typeof(ServiceMessageContext).GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, new Type[] { typeof(bool) }, null).Invoke(new object[] { true }));
var listeners = m_server.GetValue("m_listeners") as List<ITransportListener>;
var listeners = m_server.GetValue("TransportListeners") as List<ITransportListener>;
if (listeners != null)
{
foreach (var item in listeners)
@@ -189,9 +190,9 @@ public partial class OpcUaServer : BusinessBase
protected override async Task ProtectedStartAsync(CancellationToken cancellationToken)
{
// 启动服务器。
await m_application.CheckApplicationInstanceCertificates(true, 1200, cancellationToken).ConfigureAwait(false);
await m_application.CheckApplicationInstanceCertificatesAsync(true, 1200, cancellationToken).ConfigureAwait(false);
await m_application.Start(m_server).ConfigureAwait(false);
await m_application.StartAsync(m_server).ConfigureAwait(false);
IdVariableRuntimes.ForEach(a => VariableValueChange(a.Value, a.Value.AdaptVariableBasicData()));
await base.ProtectedStartAsync(cancellationToken).ConfigureAwait(false);
}
@@ -205,8 +206,8 @@ public partial class OpcUaServer : BusinessBase
try
{
await Task.Delay(2000, cancellationToken).ConfigureAwait(false);
await m_application.CheckApplicationInstanceCertificates(true, 1200, cancellationToken).ConfigureAwait(false);
await m_application.Start(m_server).ConfigureAwait(false);
await m_application.CheckApplicationInstanceCertificatesAsync(true, 1200, cancellationToken).ConfigureAwait(false);
await m_application.StartAsync(m_server).ConfigureAwait(false);
connect_success = true;
await Task.Delay(2000, cancellationToken).ConfigureAwait(false);
IdVariableRuntimes.ForEach(a => VariableValueChange(a.Value, a.Value.AdaptVariableBasicData()));
@@ -249,7 +250,7 @@ public partial class OpcUaServer : BusinessBase
}
}
private ApplicationConfiguration GetDefaultConfiguration()
private async Task<ApplicationConfiguration> GetDefaultConfigurationAsync()
{
ApplicationConfiguration config = new();
var urls = _driverPropertys.OpcUaStringUrl.Split(separator, StringSplitOptions.RemoveEmptyEntries);
@@ -428,7 +429,7 @@ public partial class OpcUaServer : BusinessBase
config.TraceConfiguration = new TraceConfiguration();
config.CertificateValidator = new CertificateValidator();
config.CertificateValidator.Update(config).ConfigureAwait(false);
await config.CertificateValidator.UpdateAsync(config).ConfigureAwait(false);
config.Extensions = new XmlElementCollection();
return config;

View File

@@ -10,15 +10,7 @@
<div class="col-12 col-md-6 p-1" style="min-height:500px;max-height:80vh;overflow: auto;">
<TreeView TItem="OpcUaTagModel" Items="Items" ShowIcon="true" ShowCheckbox="true" AutoCheckParent="true" AutoCheckChildren="true" IsVirtualize="true"
OnExpandNodeAsync=OnExpandNodeAsync OnTreeItemChecked="OnTreeItemChecked" OnTreeItemClick=@(async a=>
{
if(a?.Value?.Tag?.NodeId!=null)
{
ClickItem=a;
NodeAttributes = Plc.ReadNoteAttributes(ClickItem.Value.NodeId.ToString());
}
await InvokeAsync(StateHasChanged);
}) ShowSkeleton=ShowSkeleton
OnExpandNodeAsync=OnExpandNodeAsync OnTreeItemChecked="OnTreeItemChecked" OnTreeItemClick=@(OnTreeItemClick) ShowSkeleton=ShowSkeleton
IsAccordion ClickToggleNode ModelEqualityComparer="OpcUaImportVariable.ModelEqualityComparer" />
</div>
<div class="col-12 col-md-6 p-2" style="min-height:500px;max-height:80vh;overflow: auto;">

View File

@@ -46,6 +46,8 @@ public partial class OpcUaImportVariable
private IEnumerable<OpcUaTagModel> Nodes;
private bool ShowSkeleton = true;
/// <summary>
/// Opc对象
/// </summary>
@@ -77,6 +79,17 @@ public partial class OpcUaImportVariable
await base.OnAfterRenderAsync(firstRender);
}
private async Task OnTreeItemClick(TreeViewItem<OpcUaTagModel> item)
{
if (item?.Value?.Tag?.NodeId != null && Plc != null)
{
ClickItem = item;
NodeAttributes = await Plc.ReadNoteAttributesAsync(ClickItem.Value.NodeId.ToString(), default).ConfigureAwait(false);
}
await InvokeAsync(StateHasChanged);
}
/// <summary>
/// 构建树节点,传入的列表已经是树结构
/// </summary>

View File

@@ -34,7 +34,7 @@
<Buttons>
<Button IsAsync Color="Color.Primary" OnClick="Connect">@OpcUaPropertyLocalizer["Connect"]</Button>
<Button IsAsync Color="Color.Warning" OnClick="Disconnect">@OpcUaPropertyLocalizer["Disconnect"]</Button>
<Button IsAsync Color="Color.Warning" OnClick="DisconnectAsync">@OpcUaPropertyLocalizer["Disconnect"]</Button>
<Button IsAsync Color="Color.Primary" OnClick="Export">@OpcUaPropertyLocalizer["ExportC"]</Button>
</Buttons>

View File

@@ -24,7 +24,7 @@ using TouchSocket.Core;
namespace ThingsGateway.Debug;
public partial class OpcUaMaster : IDisposable
public partial class OpcUaMaster : IAsyncDisposable
{
public LoggerGroup? LogMessage;
private readonly OpcUaProperty OpcUaProperty = new();
@@ -37,7 +37,8 @@ public partial class OpcUaMaster : IDisposable
/// <inheritdoc/>
~OpcUaMaster()
{
this.SafeDispose();
if (_plc != null)
_ = _plc.SafeDisposeAsync();
}
private DeviceComponent DeviceComponent { get; set; }
@@ -49,9 +50,10 @@ public partial class OpcUaMaster : IDisposable
private IStringLocalizer<OpcUaProperty> OpcUaPropertyLocalizer { get; set; }
/// <inheritdoc/>
public void Dispose()
public async ValueTask DisposeAsync()
{
_plc?.SafeDispose();
if (_plc != null)
await _plc.SafeDisposeAsync();
GC.SuppressFinalize(this);
}
[Inject]
@@ -129,11 +131,11 @@ public partial class OpcUaMaster : IDisposable
}
}
private void Disconnect()
private async Task DisconnectAsync()
{
try
{
_plc.Disconnect();
await _plc.DisconnectAsync().ConfigureAwait(false);
}
catch (Exception ex)
{

View File

@@ -25,27 +25,27 @@
</ProjectReference>
<ProjectReference Include="..\ThingsGateway.Foundation.OpcUa\ThingsGateway.Foundation.OpcUa.csproj" />
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Server" Version="1.5.376.244" GeneratePathProperty="true">
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Server" Version="1.5.377.21" GeneratePathProperty="true">
<PrivateAssets>contentFiles;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
</PackageReference>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes" Version="1.5.376.244" GeneratePathProperty="true">
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client.ComplexTypes" Version="1.5.377.21" GeneratePathProperty="true">
<PrivateAssets>contentFiles;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
</PackageReference>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.376.244" GeneratePathProperty="true">
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Client" Version="1.5.377.21" GeneratePathProperty="true">
<PrivateAssets>contentFiles;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
</PackageReference>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Core" Version="1.5.376.244" GeneratePathProperty="true">
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Core" Version="1.5.377.21" GeneratePathProperty="true">
<PrivateAssets>contentFiles;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
</PackageReference>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration" Version="1.5.376.244" GeneratePathProperty="true">
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Configuration" Version="1.5.377.21" GeneratePathProperty="true">
<PrivateAssets>contentFiles;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
</PackageReference>
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Security.Certificates" Version="1.5.376.244" GeneratePathProperty="true">
<PackageReference Include="OPCFoundation.NetStandard.Opc.Ua.Security.Certificates" Version="1.5.377.21" GeneratePathProperty="true">
<PrivateAssets>contentFiles;build;buildMultitargeting;buildTransitive;analyzers;</PrivateAssets>
</PackageReference>