mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 14:43:08 +08:00
d525bc3f28
# Description This PR fixes bounded ManagedSession reconnect cleanup and reorganizes the sample applications into domain-focused folders. ## Reconnect cleanup - Bounds reconnect integration cleanup with independent 30-second close and dispose limits. - Sends `CloseSession` through the awaited transport request path during `Session.DisposeAsync`. - Preserves explicit `Session.CloseAsync` behavior that waits for or cancels outstanding publish requests. - Covers the disposal request path with a regression test. ## Sample organization - `samples/OpenUsd`: `GeneratorServer`, `SiteCompositionServer` - `samples/Redundancy`: `RedundantClient`, `RedundantServer`, `RedundantPubSub` - `samples/Isa95`: `MinimalIsa95Server` - `samples/MinimalApi`: `MinimalBoilerServer`, `MinimalCalcServer`, `MinimalClient` - `samples/DI`: `PumpDeviceIntegrationServer` - `samples/Lds`: `ConsoleLdsServer` - `samples/Reference`: `ConsoleReferenceClient`, `ConsoleReferenceServer` - `samples/PubSub`: `ConsoleReferencePubSubClient` `UA.slnx` now exposes the same requested domain groups. Project references, tests, runtime sample probes, CI workflows, Dockerfiles, Compose files, helper scripts, and documentation were updated to the new paths. `samples/Robotics/MinimalRobotServer` remains under `Robotics`. ## Validation - `dotnet build UA.slnx` - 3 Redundancy short-haul sample tests - 70 DI sample tests - 914 OpenUSD tests - 1 WotCon model-materialization test - 137 AOT tests - `docker compose config` for all 6 moved Redundancy Compose files ## Related Issues - Fixes #4273 ## Checklist - [ ] I have signed the [CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf) and read the [CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md) doc. - [x] I have added tests that prove my fix is effective or that my feature works and increased code coverage. - [x] I have added all necessary documentation. - [x] I have verified that my changes do not introduce new build or analyzer warnings. - [x] I built the complete `UA.slnx` solution locally. - [ ] I ran all tests locally against at least .NET Framework and .NET 10. - [ ] I fixed all failing and flaky tests in the CI pipelines and all CodeQL warnings. - [x] I have addressed all PR feedback received. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: marcschier <11168470+marcschier@users.noreply.github.com> Co-authored-by: Marc Schier <mschier@microsoft.com> Co-authored-by: Marc Schier <marcschier@users.noreply.github.com> Copilot-Session: a2c2838f-74ae-4997-a1d4-50574d9e0636
2103 lines
88 KiB
C#
2103 lines
88 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.Json;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Xml;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.Client;
|
|
|
|
namespace Quickstarts
|
|
{
|
|
/// <summary>
|
|
/// A client interface which holds an active session.
|
|
/// The client handler may reconnect and the Session
|
|
/// property may be updated during operation.
|
|
/// </summary>
|
|
public interface IUAClient
|
|
{
|
|
/// <summary>
|
|
/// The session to use.
|
|
/// </summary>
|
|
ISession Session { get; }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sample Session calls based on the reference server node model.
|
|
/// </summary>
|
|
public class ClientSamples
|
|
{
|
|
private const int kMaxSearchDepth = 128;
|
|
|
|
public ClientSamples(
|
|
ITelemetryContext telemetry,
|
|
Action<Array, Array>? validateResponse,
|
|
ManualResetEvent? quitEvent = null,
|
|
bool verbose = false)
|
|
{
|
|
m_telemetry = telemetry;
|
|
m_logger = telemetry.CreateLogger<ClientSamples>();
|
|
m_validate = validateResponse;
|
|
|
|
m_quitEvent = quitEvent;
|
|
m_verbose = verbose;
|
|
m_desiredEventFields = [];
|
|
int eventIndexCounter = 0;
|
|
|
|
m_desiredEventFields.Add(
|
|
eventIndexCounter++,
|
|
[QualifiedName.From(BrowseNames.Time)]);
|
|
m_desiredEventFields.Add(
|
|
eventIndexCounter++,
|
|
[QualifiedName.From(BrowseNames.ActiveState)]);
|
|
m_desiredEventFields.Add(
|
|
eventIndexCounter++,
|
|
[QualifiedName.From(BrowseNames.Message)]);
|
|
m_desiredEventFields.Add(
|
|
eventIndexCounter++,
|
|
[QualifiedName.From(BrowseNames.LimitState), QualifiedName.From(BrowseNames.CurrentState)]);
|
|
m_desiredEventFields.Add(
|
|
eventIndexCounter++,
|
|
[QualifiedName.From(BrowseNames.LimitState), QualifiedName.From(BrowseNames.LastTransition)]);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read a list of nodes from Server
|
|
/// </summary>
|
|
public async Task ReadNodesAsync(ISession session, CancellationToken ct = default)
|
|
{
|
|
if (session == null || !session.Connected)
|
|
{
|
|
Console.WriteLine("Session not connected!");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// build a list of nodes to be read
|
|
ArrayOf<ReadValueId> nodesToRead =
|
|
[
|
|
// Value of ServerStatus
|
|
new ReadValueId {
|
|
NodeId = VariableIds.Server_ServerStatus,
|
|
AttributeId = Attributes.Value },
|
|
// BrowseName of ServerStatus_StartTime
|
|
new ReadValueId
|
|
{
|
|
NodeId = VariableIds.Server_ServerStatus_StartTime,
|
|
AttributeId = Attributes.BrowseName
|
|
},
|
|
// Value of ServerStatus_StartTime
|
|
new ReadValueId
|
|
{
|
|
NodeId = VariableIds.Server_ServerStatus_StartTime,
|
|
AttributeId = Attributes.Value
|
|
}
|
|
];
|
|
|
|
// Read the node attributes
|
|
Console.WriteLine("Reading nodes...");
|
|
|
|
// Call Read Service
|
|
ReadResponse response = await session.ReadAsync(
|
|
null,
|
|
0,
|
|
TimestampsToReturn.Both,
|
|
nodesToRead,
|
|
ct).ConfigureAwait(false);
|
|
|
|
ArrayOf<DataValue> resultsValues = response.Results;
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos = response.DiagnosticInfos;
|
|
|
|
// Validate the results
|
|
ValidateResponse(resultsValues, nodesToRead);
|
|
|
|
// Display the results.
|
|
foreach (DataValue result in resultsValues)
|
|
{
|
|
Console.WriteLine($"Read Value = {result.WrappedValue} , StatusCode = {result.StatusCode}");
|
|
}
|
|
|
|
// Read Server NamespaceArray
|
|
Console.WriteLine("Reading Value of NamespaceArray node...");
|
|
DataValue namespaceArray = await session.ReadValueAsync(VariableIds.Server_NamespaceArray, ct)
|
|
.ConfigureAwait(false);
|
|
// Display the result
|
|
Console.WriteLine($"NamespaceArray Value = {namespaceArray}");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log Error
|
|
m_logger.ReadNodesError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Write a list of nodes to the Server.
|
|
/// </summary>
|
|
public async Task WriteNodesAsync(ISession session, CancellationToken ct = default)
|
|
{
|
|
if (session == null || !session.Connected)
|
|
{
|
|
Console.WriteLine("Session not connected!");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Write the configured nodes
|
|
ArrayOf<WriteValue> nodesToWrite =
|
|
[
|
|
// Int32 Node - Objects\CTT\Scalar\Scalar_Static\Int32
|
|
new WriteValue
|
|
{
|
|
NodeId = NodeId.Parse("ns=2;s=Scalar_Static_Int32"),
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(Variant.From(100))
|
|
},
|
|
|
|
// Float Node - Objects\CTT\Scalar\Scalar_Static\Float
|
|
new WriteValue
|
|
{
|
|
NodeId = NodeId.Parse("ns=2;s=Scalar_Static_Float"),
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(Variant.From(100.5f))
|
|
},
|
|
|
|
// String Node - Objects\CTT\Scalar\Scalar_Static\String
|
|
new WriteValue
|
|
{
|
|
NodeId = NodeId.Parse("ns=2;s=Scalar_Static_String"),
|
|
AttributeId = Attributes.Value,
|
|
Value = new DataValue(Variant.From("String Test"))
|
|
}
|
|
];
|
|
|
|
// Write the node attributes
|
|
Console.WriteLine("Writing nodes...");
|
|
|
|
// Call Write Service
|
|
WriteResponse response = await session.WriteAsync(
|
|
null,
|
|
nodesToWrite,
|
|
ct).ConfigureAwait(false);
|
|
|
|
ArrayOf<StatusCode> results = response.Results;
|
|
ArrayOf<DiagnosticInfo> diagnosticInfos = response.DiagnosticInfos;
|
|
|
|
// Validate the response
|
|
ValidateResponse(results, nodesToWrite);
|
|
|
|
// Display the results.
|
|
Console.WriteLine("Write Results :");
|
|
|
|
foreach (StatusCode writeResult in results)
|
|
{
|
|
Console.WriteLine($" {writeResult}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log Error
|
|
m_logger.WriteNodesError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Browse Server nodes
|
|
/// </summary>
|
|
public async Task BrowseAsync(ISession session, CancellationToken ct = default)
|
|
{
|
|
if (session == null || !session.Connected)
|
|
{
|
|
Console.WriteLine("Session not connected!");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Create a Browser object
|
|
var browser = new Browser(session)
|
|
{
|
|
// Set browse parameters
|
|
BrowseDirection = BrowseDirection.Forward,
|
|
NodeClassMask = (int)NodeClass.Object | (int)NodeClass.Variable,
|
|
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
|
IncludeSubtypes = true
|
|
};
|
|
|
|
NodeId nodeToBrowse = ObjectIds.Server;
|
|
|
|
// Call Browse service
|
|
Console.WriteLine($"Browsing {nodeToBrowse} node...");
|
|
ArrayOf<ReferenceDescription> browseResults =
|
|
await browser.BrowseAsync(nodeToBrowse, ct).ConfigureAwait(false);
|
|
|
|
// Display the results
|
|
Console.WriteLine($"Browse returned {browseResults.Count} results:");
|
|
|
|
foreach (ReferenceDescription result in browseResults)
|
|
{
|
|
Console.WriteLine($" DisplayName = {result.DisplayName.Text}, NodeClass = {result.NodeClass}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Log Error
|
|
m_logger.BrowseError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call UA method
|
|
/// </summary>
|
|
public async Task CallMethodAsync(ISession session, CancellationToken ct = default)
|
|
{
|
|
if (session == null || !session.Connected)
|
|
{
|
|
Console.WriteLine("Session not connected!");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Define the UA Method to call
|
|
// Parent node - Objects\CTT\Methods
|
|
// Method node - Objects\CTT\Methods\Add
|
|
var objectId = NodeId.Parse("ns=2;s=Methods");
|
|
var methodId = NodeId.Parse("ns=2;s=Methods_Add");
|
|
|
|
// Define the method parameters
|
|
// Input argument requires a Float and an UInt32 value
|
|
// Invoke Call service
|
|
Console.WriteLine($"Calling UAMethod for node {methodId} ...");
|
|
ArrayOf<Variant> outputArguments = await session.CallAsync(
|
|
objectId,
|
|
methodId,
|
|
ct,
|
|
(float)10.5,
|
|
(uint)10).ConfigureAwait(false);
|
|
|
|
// Display results
|
|
Console.WriteLine($"Method call returned {outputArguments.Count} output argument(s):");
|
|
|
|
foreach (Variant outputArgument in outputArguments)
|
|
{
|
|
Console.WriteLine($" OutputValue = {outputArgument}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.MethodCallError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Call the Start method for Alarming to enable events
|
|
/// </summary>
|
|
public async Task EnableEventsAsync(
|
|
ISession session,
|
|
uint timeToRun,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (session == null || !session.Connected)
|
|
{
|
|
Console.WriteLine("Session not connected!");
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Define the UA Method to call
|
|
// Parent node - Objects\CTT\Alarms
|
|
// Method node - Objects\CTT\Alarms\Start
|
|
var objectId = NodeId.Parse("ns=7;s=Alarms");
|
|
var methodId = NodeId.Parse("ns=7;s=Alarms.Start");
|
|
|
|
// Define the method parameters
|
|
// Input argument requires a Float and an UInt32 value
|
|
// Invoke Call service
|
|
Console.WriteLine($"Calling UAMethod for node {methodId} ...");
|
|
ArrayOf<Variant> outputArguments = await session.CallAsync(
|
|
objectId,
|
|
methodId,
|
|
ct,
|
|
timeToRun).ConfigureAwait(false);
|
|
|
|
// Display results
|
|
Console.WriteLine($"Method call returned {outputArguments.Count} output argument(s):");
|
|
|
|
foreach (Variant outputArgument in outputArguments)
|
|
{
|
|
Console.WriteLine($" OutputValue = {outputArgument}");
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.MethodCallError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create Subscription and MonitoredItems for DataChanges
|
|
/// </summary>
|
|
public async Task<bool> SubscribeToDataChangesAsync(
|
|
ISession session,
|
|
uint minLifeTime,
|
|
bool enableDurableSubscriptions,
|
|
CancellationToken ct = default)
|
|
{
|
|
bool isDurable = false;
|
|
|
|
if (session == null || !session.Connected)
|
|
{
|
|
Console.WriteLine("Session not connected!");
|
|
return isDurable;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Create a subscription for receiving data change notifications
|
|
const int subscriptionPublishingInterval = 1000;
|
|
const int itemSamplingInterval = 1000;
|
|
uint queueSize = 10;
|
|
uint lifetime = minLifeTime;
|
|
|
|
if (enableDurableSubscriptions)
|
|
{
|
|
queueSize = 100;
|
|
lifetime = 20;
|
|
}
|
|
|
|
// Define Subscription parameters
|
|
// Ownership of the subscription transfers to the session in AddSubscription
|
|
// below, which disposes all of its subscriptions when it is disposed.
|
|
// TODO: remove this suppression if CA2000 learns this ownership transfer pattern.
|
|
#pragma warning disable CA2000
|
|
var subscription = new Subscription(session.DefaultSubscription)
|
|
{
|
|
DisplayName = "Console ReferenceClient Subscription",
|
|
PublishingEnabled = true,
|
|
PublishingInterval = subscriptionPublishingInterval,
|
|
LifetimeCount = 0,
|
|
MinLifetimeInterval = lifetime,
|
|
KeepAliveCount = 5
|
|
};
|
|
#pragma warning restore CA2000
|
|
|
|
session.AddSubscription(subscription);
|
|
|
|
// Create the subscription on Server side
|
|
await subscription.CreateAsync(ct).ConfigureAwait(false);
|
|
m_logger.NewSubscriptionCreatedWithIntervals(
|
|
subscription.Id,
|
|
itemSamplingInterval,
|
|
subscriptionPublishingInterval);
|
|
|
|
if (enableDurableSubscriptions)
|
|
{
|
|
(bool success, uint revisedLifetimeInHours) =
|
|
await subscription.SetSubscriptionDurableAsync(1, ct).ConfigureAwait(false);
|
|
if (success)
|
|
{
|
|
isDurable = true;
|
|
|
|
m_logger.SubscriptionDurable(
|
|
subscription.Id,
|
|
revisedLifetimeInHours);
|
|
}
|
|
else
|
|
{
|
|
m_logger.SubscriptionDurableFailed(subscription.Id);
|
|
}
|
|
}
|
|
|
|
// Create MonitoredItems for data changes (Reference Server)
|
|
|
|
var intMonitoredItem = new MonitoredItem(subscription.DefaultItem)
|
|
{
|
|
// Int32 Node - Objects\CTT\Scalar\Simulation\Int32
|
|
StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_Int32"),
|
|
AttributeId = Attributes.Value,
|
|
DisplayName = "Int32 Variable",
|
|
SamplingInterval = itemSamplingInterval,
|
|
QueueSize = queueSize,
|
|
DiscardOldest = true
|
|
};
|
|
intMonitoredItem.Notification += OnMonitoredItemNotification;
|
|
|
|
subscription.AddItem(intMonitoredItem);
|
|
|
|
var floatMonitoredItem = new MonitoredItem(subscription.DefaultItem)
|
|
{
|
|
// Float Node - Objects\CTT\Scalar\Simulation\Float
|
|
StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_Float"),
|
|
AttributeId = Attributes.Value,
|
|
DisplayName = "Float Variable",
|
|
SamplingInterval = itemSamplingInterval,
|
|
QueueSize = queueSize
|
|
};
|
|
floatMonitoredItem.Notification += OnMonitoredItemNotification;
|
|
|
|
subscription.AddItem(floatMonitoredItem);
|
|
|
|
var stringMonitoredItem = new MonitoredItem(subscription.DefaultItem)
|
|
{
|
|
// String Node - Objects\CTT\Scalar\Simulation\String
|
|
StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_String"),
|
|
AttributeId = Attributes.Value,
|
|
DisplayName = "String Variable",
|
|
SamplingInterval = itemSamplingInterval,
|
|
QueueSize = queueSize
|
|
};
|
|
stringMonitoredItem.Notification += OnMonitoredItemNotification;
|
|
|
|
subscription.AddItem(stringMonitoredItem);
|
|
|
|
var eventMonitoredItem = new MonitoredItem(subscription.DefaultItem)
|
|
{
|
|
StartNodeId = ObjectIds.Server,
|
|
AttributeId = Attributes.EventNotifier,
|
|
DisplayName = "Event Variable",
|
|
SamplingInterval = itemSamplingInterval,
|
|
QueueSize = queueSize
|
|
};
|
|
eventMonitoredItem.Notification += OnMonitoredItemEventNotification;
|
|
|
|
var filter = new EventFilter();
|
|
|
|
var simpleAttributeOperands = new List<SimpleAttributeOperand>();
|
|
|
|
foreach (ArrayOf<QualifiedName> desiredEventField in m_desiredEventFields.Values)
|
|
{
|
|
simpleAttributeOperands.Add(
|
|
new SimpleAttributeOperand
|
|
{
|
|
AttributeId = Attributes.Value,
|
|
TypeDefinitionId = ObjectTypeIds.BaseEventType,
|
|
BrowsePath = desiredEventField
|
|
});
|
|
}
|
|
filter.SelectClauses = simpleAttributeOperands;
|
|
|
|
var whereClause = new ContentFilter();
|
|
var existingEventType = new SimpleAttributeOperand
|
|
{
|
|
AttributeId = Attributes.Value,
|
|
TypeDefinitionId = ObjectTypeIds.ExclusiveLevelAlarmType,
|
|
BrowsePath = [QualifiedName.From("EventType")]
|
|
};
|
|
var desiredEventType = new LiteralOperand
|
|
{
|
|
Value = Variant.From(ObjectTypeIds.ExclusiveLevelAlarmType)
|
|
};
|
|
|
|
whereClause.Push(
|
|
FilterOperator.Equals,
|
|
Variant.FromStructure(existingEventType),
|
|
Variant.FromStructure(desiredEventType));
|
|
|
|
filter.WhereClause = whereClause;
|
|
|
|
eventMonitoredItem.Filter = filter;
|
|
eventMonitoredItem.NodeClass = NodeClass.Object;
|
|
|
|
subscription.AddItem(eventMonitoredItem);
|
|
|
|
// Create the monitored items on Server side
|
|
await subscription.ApplyChangesAsync(ct).ConfigureAwait(false);
|
|
m_logger.MonitoredItemsCreated(subscription.Id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.SubscribeError(ex);
|
|
}
|
|
|
|
return isDurable;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Fetch all references and nodes with attributes except values from the server.
|
|
/// </summary>
|
|
/// <param name="uaClient">The UAClient with a session to use.</param>
|
|
/// <param name="startingNode">The node from which the hierarchical nodes are fetched.</param>
|
|
/// <param name="fetchTree">Iterate to fetch all nodes in the tree.</param>
|
|
/// <param name="addRootNode">Adds the root node to the result.</param>
|
|
/// <param name="filterUATypes">Filters nodes from namespace 0 from the result.</param>
|
|
/// <returns>The list of nodes on the server.</returns>
|
|
public async Task<IList<INode>> FetchAllNodesNodeCacheAsync(
|
|
IUAClient uaClient,
|
|
NodeId startingNode,
|
|
bool fetchTree = false,
|
|
bool addRootNode = false,
|
|
bool filterUATypes = true,
|
|
bool clearNodeCache = true,
|
|
CancellationToken ct = default)
|
|
{
|
|
var stopwatch = new Stopwatch();
|
|
var nodeDictionary = new Dictionary<ExpandedNodeId, INode>();
|
|
ArrayOf<NodeId> references = [ReferenceTypeIds.HierarchicalReferences];
|
|
ArrayOf<ExpandedNodeId> nodesToBrowse = [startingNode];
|
|
|
|
// start
|
|
stopwatch.Start();
|
|
|
|
if (clearNodeCache)
|
|
{
|
|
// clear NodeCache to fetch all nodes from server
|
|
uaClient.Session.NodeCache.Clear();
|
|
await FetchReferenceIdTypesAsync(uaClient.Session, ct).ConfigureAwait(false);
|
|
}
|
|
|
|
// add root node
|
|
if (addRootNode)
|
|
{
|
|
INode? rootNode = await uaClient.Session.NodeCache.FindAsync(startingNode, ct)
|
|
.ConfigureAwait(false);
|
|
if (rootNode == null)
|
|
{
|
|
m_logger.RootNodeNotFound(startingNode);
|
|
}
|
|
else
|
|
{
|
|
nodeDictionary[rootNode.NodeId] = rootNode;
|
|
}
|
|
}
|
|
|
|
int searchDepth = 0;
|
|
while (nodesToBrowse.Count > 0 && searchDepth < kMaxSearchDepth)
|
|
{
|
|
if (m_quitEvent?.WaitOne(0) == true)
|
|
{
|
|
m_logger.BrowseAborted();
|
|
break;
|
|
}
|
|
|
|
searchDepth++;
|
|
m_logger.FindReferencesAfterDuration(
|
|
searchDepth,
|
|
nodesToBrowse.Count,
|
|
stopwatch.ElapsedMilliseconds);
|
|
ArrayOf<INode> response = await uaClient
|
|
.Session.NodeCache.FindReferencesAsync(nodesToBrowse, references, false, true, ct)
|
|
.ConfigureAwait(false);
|
|
|
|
var nextNodesToBrowse = new List<ExpandedNodeId>();
|
|
int duplicates = 0;
|
|
int leafNodes = 0;
|
|
foreach (INode node in response)
|
|
{
|
|
if (!nodeDictionary.ContainsKey(node.NodeId))
|
|
{
|
|
if (fetchTree)
|
|
{
|
|
bool leafNode = false;
|
|
|
|
// no need to browse property types
|
|
if (node is VariableNode variableNode)
|
|
{
|
|
IReference? hasTypeDefinition = variableNode.ReferenceTable
|
|
.FirstOrDefault(r =>
|
|
r.ReferenceTypeId
|
|
.Equals(ReferenceTypeIds.HasTypeDefinition));
|
|
if (hasTypeDefinition != null)
|
|
{
|
|
leafNode = hasTypeDefinition.TargetId == VariableTypeIds
|
|
.PropertyType;
|
|
}
|
|
}
|
|
|
|
if (!leafNode)
|
|
{
|
|
nextNodesToBrowse.Add(node.NodeId);
|
|
}
|
|
else
|
|
{
|
|
leafNodes++;
|
|
}
|
|
}
|
|
|
|
if (filterUATypes)
|
|
{
|
|
if (node.NodeId.NamespaceIndex != 0)
|
|
{
|
|
// filter out default namespace
|
|
nodeDictionary[node.NodeId] = node;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
nodeDictionary[node.NodeId] = node;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
duplicates++;
|
|
}
|
|
}
|
|
if (duplicates > 0)
|
|
{
|
|
m_logger.FindReferencesDuplicateNodesIgnored(duplicates);
|
|
}
|
|
if (leafNodes > 0)
|
|
{
|
|
m_logger.FindReferencesLeafNodesIgnored(leafNodes);
|
|
}
|
|
nodesToBrowse = nextNodesToBrowse;
|
|
}
|
|
|
|
stopwatch.Stop();
|
|
|
|
m_logger.FetchAllNodesNodeCacheFound(
|
|
nodeDictionary.Count,
|
|
stopwatch.ElapsedMilliseconds);
|
|
|
|
var result = nodeDictionary.Values.ToList();
|
|
result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId));
|
|
|
|
if (m_verbose)
|
|
{
|
|
foreach (INode node in result)
|
|
{
|
|
m_logger.NodeInfo(
|
|
node.NodeId,
|
|
node.NodeClass,
|
|
node.BrowseName);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Browse full address space using the ManagedBrowseMethod, which
|
|
/// will take care of not sending to many nodes to the server,
|
|
/// calling BrowseNext and dealing with the status codes
|
|
/// BadNoContinuationPoint and BadInvalidContinuationPoint.
|
|
/// </summary>
|
|
/// <param name="uaClient">The UAClient with a session to use.</param>
|
|
/// <param name="startingNode">The node where the browse operation starts.</param>
|
|
/// <param name="browseDescription">An optional BrowseDescription to use.</param>
|
|
public async Task<ArrayOf<ReferenceDescription>> ManagedBrowseFullAddressSpaceAsync(
|
|
IUAClient uaClient,
|
|
NodeId startingNode = default,
|
|
BrowseDescription? browseDescription = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
ContinuationPointPolicy policyBackup = uaClient.Session.ContinuationPointPolicy;
|
|
uaClient.Session.ContinuationPointPolicy = ContinuationPointPolicy.Default;
|
|
|
|
var stopWatch = new Stopwatch();
|
|
stopWatch.Start();
|
|
BrowseDirection browseDirection = BrowseDirection.Forward;
|
|
NodeId referenceTypeId = ReferenceTypeIds.HierarchicalReferences;
|
|
bool includeSubtypes = true;
|
|
uint nodeClassMask = 0;
|
|
|
|
if (browseDescription != null)
|
|
{
|
|
startingNode = browseDescription.NodeId;
|
|
browseDirection = browseDescription.BrowseDirection;
|
|
referenceTypeId = browseDescription.ReferenceTypeId;
|
|
includeSubtypes = browseDescription.IncludeSubtypes;
|
|
nodeClassMask = browseDescription.NodeClassMask;
|
|
|
|
if (browseDescription.ResultMask != (uint)BrowseResultMask.All)
|
|
{
|
|
m_logger.UnsupportedBrowseResultMask(
|
|
BrowseResultMask.All,
|
|
browseDescription.ResultMask);
|
|
}
|
|
}
|
|
|
|
ArrayOf<NodeId> nodesToBrowse = [startingNode.IsNull ? ObjectIds.RootFolder : startingNode];
|
|
|
|
const int kMaxReferencesPerNode = 1000;
|
|
|
|
// Browse
|
|
var referenceDescriptions = new Dictionary<ExpandedNodeId, ReferenceDescription>();
|
|
|
|
int searchDepth = 0;
|
|
uint maxNodesPerBrowse = uaClient.Session.OperationLimits.MaxNodesPerBrowse;
|
|
|
|
var allReferenceDescriptions = new List<ArrayOf<ReferenceDescription>>();
|
|
var newReferenceDescriptions = new List<ArrayOf<ReferenceDescription>>();
|
|
var allServiceResults = new List<ServiceResult>();
|
|
|
|
while (nodesToBrowse.Count != 0 && searchDepth < kMaxSearchDepth)
|
|
{
|
|
searchDepth++;
|
|
m_logger.BrowseNodesAfterDuration(
|
|
searchDepth,
|
|
nodesToBrowse.Count,
|
|
stopWatch.ElapsedMilliseconds);
|
|
|
|
const bool repeatBrowse = false;
|
|
|
|
do
|
|
{
|
|
if (m_quitEvent?.WaitOne(0) == true)
|
|
{
|
|
m_logger.BrowseAborted();
|
|
break;
|
|
}
|
|
|
|
try
|
|
{
|
|
// the resultMask defaults to "all"
|
|
// maybe the API should be extended to
|
|
// support it. But that will then also be
|
|
// necessary for BrowseAsync
|
|
(ArrayOf<ArrayOf<ReferenceDescription>> descriptions, ArrayOf<ServiceResult> errors) =
|
|
await uaClient
|
|
.Session.ManagedBrowseAsync(
|
|
null,
|
|
null,
|
|
nodesToBrowse,
|
|
kMaxReferencesPerNode,
|
|
browseDirection,
|
|
referenceTypeId,
|
|
true,
|
|
nodeClassMask,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
|
|
allReferenceDescriptions.AddRange(descriptions);
|
|
newReferenceDescriptions.AddRange(descriptions);
|
|
allServiceResults.AddRange(errors);
|
|
}
|
|
catch (ServiceResultException sre)
|
|
{
|
|
// the maximum number of nodes per browse is
|
|
// set in the ManagedBrowse from the configuration
|
|
// and cannot be influenced from the outside.
|
|
// if that's desired it would be necessary to provide
|
|
// an additional parameter to the method.
|
|
m_logger.BrowseServiceError(sre);
|
|
throw;
|
|
}
|
|
} while (repeatBrowse);
|
|
|
|
// Build browse request for next level
|
|
var nodesForNextManagedBrowse = new List<NodeId>();
|
|
int duplicates = 0;
|
|
foreach (ArrayOf<ReferenceDescription> referenceCollection in newReferenceDescriptions)
|
|
{
|
|
foreach (ReferenceDescription reference in referenceCollection)
|
|
{
|
|
if (!referenceDescriptions.ContainsKey(reference.NodeId))
|
|
{
|
|
referenceDescriptions[reference.NodeId] = reference;
|
|
|
|
if (!reference.ReferenceTypeId.Equals(ReferenceTypeIds.HasProperty))
|
|
{
|
|
nodesForNextManagedBrowse.Add(
|
|
ExpandedNodeId.ToNodeId(
|
|
reference.NodeId,
|
|
uaClient.Session.NamespaceUris));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
duplicates++;
|
|
}
|
|
}
|
|
}
|
|
|
|
newReferenceDescriptions.Clear();
|
|
|
|
nodesToBrowse = nodesForNextManagedBrowse.ToArrayOf();
|
|
|
|
if (duplicates > 0)
|
|
{
|
|
m_logger.ManagedBrowseDuplicateNodesIgnored(duplicates);
|
|
}
|
|
}
|
|
|
|
stopWatch.Stop();
|
|
|
|
var result = new List<ReferenceDescription>(referenceDescriptions.Values);
|
|
|
|
result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId));
|
|
|
|
m_logger.ManagedBrowseFullAddressSpaceFound(
|
|
result.Count,
|
|
stopWatch.ElapsedMilliseconds);
|
|
|
|
if (m_verbose)
|
|
{
|
|
foreach (ReferenceDescription reference in result)
|
|
{
|
|
m_logger.NodeInfo(
|
|
reference.NodeId,
|
|
reference.NodeClass,
|
|
reference.BrowseName);
|
|
}
|
|
}
|
|
|
|
uaClient.Session.ContinuationPointPolicy = policyBackup;
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Browse full address space.
|
|
/// </summary>
|
|
/// <param name="uaClient">The UAClient with a session to use.</param>
|
|
/// <param name="startingNode">The node where the browse operation starts.</param>
|
|
/// <param name="browseDescription">An optional BrowseDescription to use.</param>
|
|
public async Task<ArrayOf<ReferenceDescription>> BrowseFullAddressSpaceAsync(
|
|
IUAClient uaClient,
|
|
NodeId startingNode = default,
|
|
BrowseDescription? browseDescription = null,
|
|
CancellationToken ct = default)
|
|
{
|
|
var stopWatch = new Stopwatch();
|
|
stopWatch.Start();
|
|
|
|
// Browse template
|
|
const int kMaxReferencesPerNode = 1000;
|
|
BrowseDescription browseTemplate =
|
|
browseDescription
|
|
?? new BrowseDescription
|
|
{
|
|
NodeId = startingNode.IsNull ? ObjectIds.RootFolder : startingNode,
|
|
BrowseDirection = BrowseDirection.Forward,
|
|
ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences,
|
|
IncludeSubtypes = true,
|
|
NodeClassMask = 0,
|
|
ResultMask = (uint)BrowseResultMask.All
|
|
};
|
|
ArrayOf<BrowseDescription> browseDescriptionCollection
|
|
= CreateBrowseDescriptionCollectionFromNodeId(
|
|
[startingNode.IsNull ? ObjectIds.RootFolder : startingNode],
|
|
browseTemplate);
|
|
|
|
// Browse
|
|
var referenceDescriptions = new Dictionary<ExpandedNodeId, ReferenceDescription>();
|
|
|
|
int searchDepth = 0;
|
|
uint maxNodesPerBrowse = uaClient.Session.OperationLimits.MaxNodesPerBrowse;
|
|
while (browseDescriptionCollection.Count > 0 && searchDepth < kMaxSearchDepth)
|
|
{
|
|
searchDepth++;
|
|
m_logger.BrowseNodesAfterDuration(
|
|
searchDepth,
|
|
browseDescriptionCollection.Count,
|
|
stopWatch.ElapsedMilliseconds);
|
|
|
|
var allBrowseResults = new List<BrowseResult>();
|
|
bool repeatBrowse;
|
|
ArrayOf<BrowseResult> browseResultCollection = default;
|
|
var unprocessedOperations = new List<BrowseDescription>();
|
|
ArrayOf<DiagnosticInfo> diagnosticsInfoCollection;
|
|
do
|
|
{
|
|
if (m_quitEvent?.WaitOne(0) == true)
|
|
{
|
|
m_logger.BrowseAborted();
|
|
break;
|
|
}
|
|
if (maxNodesPerBrowse >= browseDescriptionCollection.Count)
|
|
{
|
|
maxNodesPerBrowse = 0; // Do not slice, take all
|
|
}
|
|
|
|
ArrayOf<BrowseDescription> browseCollection =
|
|
maxNodesPerBrowse == 0
|
|
? browseDescriptionCollection
|
|
: browseDescriptionCollection[..(int)maxNodesPerBrowse];
|
|
repeatBrowse = false;
|
|
try
|
|
{
|
|
BrowseResponse browseResponse = await uaClient
|
|
.Session.BrowseAsync(
|
|
null,
|
|
null,
|
|
kMaxReferencesPerNode,
|
|
browseCollection,
|
|
ct)
|
|
.ConfigureAwait(false);
|
|
browseResultCollection = browseResponse.Results;
|
|
diagnosticsInfoCollection = browseResponse.DiagnosticInfos;
|
|
ClientBase.ValidateResponse(browseResultCollection, browseCollection);
|
|
ClientBase.ValidateDiagnosticInfos(
|
|
diagnosticsInfoCollection,
|
|
browseCollection);
|
|
|
|
// separate unprocessed nodes for later
|
|
int ii = 0;
|
|
foreach (BrowseResult browseResult in browseResultCollection)
|
|
{
|
|
// check for error.
|
|
StatusCode statusCode = browseResult.StatusCode;
|
|
if (StatusCode.IsBad(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 (statusCode == StatusCodes.BadNoContinuationPoints)
|
|
{
|
|
unprocessedOperations.Add(browseCollection[ii++]);
|
|
continue;
|
|
}
|
|
}
|
|
|
|
// save results.
|
|
allBrowseResults.Add(browseResult);
|
|
ii++;
|
|
}
|
|
}
|
|
catch (ServiceResultException sre)
|
|
{
|
|
if (sre.StatusCode == StatusCodes.BadEncodingLimitsExceeded ||
|
|
sre.StatusCode == StatusCodes.BadResponseTooLarge)
|
|
{
|
|
// try to address by overriding operation limit
|
|
maxNodesPerBrowse =
|
|
maxNodesPerBrowse == 0
|
|
? (uint)browseCollection.Count / 2
|
|
: maxNodesPerBrowse / 2;
|
|
repeatBrowse = true;
|
|
}
|
|
else
|
|
{
|
|
m_logger.BrowseServiceErrorWithPeriod(sre);
|
|
throw;
|
|
}
|
|
}
|
|
} while (repeatBrowse);
|
|
|
|
browseDescriptionCollection = maxNodesPerBrowse == 0 ?
|
|
default :
|
|
browseDescriptionCollection[(int)maxNodesPerBrowse..];
|
|
|
|
// Browse next
|
|
ArrayOf<ByteString> continuationPoints = PrepareBrowseNext(browseResultCollection);
|
|
while (continuationPoints.Count > 0)
|
|
{
|
|
if (m_quitEvent?.WaitOne(0) == true)
|
|
{
|
|
m_logger.BrowseAborted();
|
|
}
|
|
|
|
m_logger.BrowseNextContinuationPoints(continuationPoints.Count);
|
|
BrowseNextResponse browseNextResult = await uaClient
|
|
.Session.BrowseNextAsync(null, false, continuationPoints, ct)
|
|
.ConfigureAwait(false);
|
|
ArrayOf<BrowseResult> browseNextResultCollection = browseNextResult.Results;
|
|
diagnosticsInfoCollection = browseNextResult.DiagnosticInfos;
|
|
ClientBase.ValidateResponse(browseNextResultCollection, continuationPoints);
|
|
ClientBase.ValidateDiagnosticInfos(
|
|
diagnosticsInfoCollection,
|
|
continuationPoints);
|
|
allBrowseResults.AddRange(browseNextResultCollection);
|
|
continuationPoints = PrepareBrowseNext(browseNextResultCollection);
|
|
}
|
|
|
|
// Build browse request for next level
|
|
var browseTable = new List<NodeId>();
|
|
int duplicates = 0;
|
|
foreach (BrowseResult browseResult in allBrowseResults)
|
|
{
|
|
foreach (ReferenceDescription reference in browseResult.References)
|
|
{
|
|
if (!referenceDescriptions.ContainsKey(reference.NodeId))
|
|
{
|
|
referenceDescriptions[reference.NodeId] = reference;
|
|
if (reference.ReferenceTypeId != ReferenceTypeIds.HasProperty)
|
|
{
|
|
browseTable.Add(
|
|
ExpandedNodeId.ToNodeId(
|
|
reference.NodeId,
|
|
uaClient.Session.NamespaceUris));
|
|
}
|
|
}
|
|
else
|
|
{
|
|
duplicates++;
|
|
}
|
|
}
|
|
}
|
|
if (duplicates > 0)
|
|
{
|
|
m_logger.BrowseResultDuplicateNodesIgnored(duplicates);
|
|
}
|
|
browseDescriptionCollection = ArrayOf.Combine(
|
|
browseDescriptionCollection,
|
|
CreateBrowseDescriptionCollectionFromNodeId(browseTable, browseTemplate),
|
|
unprocessedOperations); // add unprocessed nodes if any
|
|
}
|
|
|
|
stopWatch.Stop();
|
|
|
|
var result = new List<ReferenceDescription>(referenceDescriptions.Values);
|
|
result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId));
|
|
|
|
m_logger.BrowseFullAddressSpaceFound(
|
|
referenceDescriptions.Count,
|
|
stopWatch.ElapsedMilliseconds);
|
|
|
|
if (m_verbose)
|
|
{
|
|
foreach (ReferenceDescription reference in result)
|
|
{
|
|
m_logger.NodeInfo(
|
|
reference.NodeId,
|
|
reference.NodeClass,
|
|
reference.BrowseName);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Loads the custom type system of the server in the session.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// Outputs elapsed time information for perf testing and lists all
|
|
/// types that were successfully added to the session encodeable type factory.
|
|
/// </remarks>
|
|
/// <exception cref="ServiceResultException"></exception>
|
|
public async Task LoadTypeSystemAsync(ComplexTypeSystem complexTypeSystem, CancellationToken ct = default)
|
|
{
|
|
m_logger.LoadServerTypeSystem();
|
|
|
|
var stopWatch = new Stopwatch();
|
|
stopWatch.Start();
|
|
|
|
bool loaded = await complexTypeSystem.LoadAsync(throwOnError: true, ct: ct).ConfigureAwait(false);
|
|
|
|
stopWatch.Stop();
|
|
|
|
if (m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
m_logger.LoadedTypes(
|
|
complexTypeSystem.GetDefinedTypes().Count,
|
|
stopWatch.ElapsedMilliseconds);
|
|
}
|
|
|
|
if (!loaded)
|
|
{
|
|
throw new ServiceResultException(
|
|
StatusCodes.BadTypeMismatch,
|
|
"ComplexTypeSystem.LoadAsync did not load all custom types.");
|
|
}
|
|
|
|
if (m_verbose)
|
|
{
|
|
m_logger.CustomTypesDefined();
|
|
foreach (XmlQualifiedName type in complexTypeSystem.GetDefinedTypes())
|
|
{
|
|
m_logger.TypeName(type.Namespace, type.Name);
|
|
}
|
|
|
|
m_logger.LoadedDictionaries(complexTypeSystem.DataTypeSystem.Count);
|
|
foreach (KeyValuePair<NodeId, DataDictionary> dictionary in complexTypeSystem
|
|
.DataTypeSystem)
|
|
{
|
|
m_logger.DictionaryName(dictionary.Value.Name);
|
|
foreach (KeyValuePair<NodeId, QualifiedName> type in dictionary.Value.DataTypes)
|
|
{
|
|
m_logger.DictionaryType(type.Key, type.Value);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Read all ReferenceTypeIds from the server that are not known by the client.
|
|
/// To reduce the number of calls due to traversal call pyramid, start with all
|
|
/// known reference types to reduce the number of FetchReferences/FetchNodes calls.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The NodeCache needs this information to function properly with subtypes
|
|
/// of hierarchical calls.
|
|
/// </remarks>
|
|
/// <param name="session">The session to use</param>
|
|
private static Task FetchReferenceIdTypesAsync(
|
|
ISession session,
|
|
CancellationToken ct = default)
|
|
{
|
|
NamespaceTable namespaceUris = session.NamespaceUris;
|
|
var referenceTypes = ReferenceTypeIds.Identifiers
|
|
.Select(nodeId => NodeId.ToExpandedNodeId(nodeId, namespaceUris))
|
|
.ToArrayOf();
|
|
return session.FetchTypeTreeAsync(referenceTypes, ct);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Output all values as JSON.
|
|
/// </summary>
|
|
public async Task<ResultSet<DataValue>> ReadAllValuesAsync(
|
|
IUAClient uaClient,
|
|
ArrayOf<NodeId> variableIds,
|
|
CancellationToken ct = default)
|
|
{
|
|
bool retrySingleRead = false;
|
|
List<DataValue>? values = null;
|
|
List<ServiceResult>? errors = null;
|
|
|
|
do
|
|
{
|
|
try
|
|
{
|
|
if (retrySingleRead)
|
|
{
|
|
values = [];
|
|
errors = [];
|
|
|
|
foreach (NodeId variableId in variableIds.ToList())
|
|
{
|
|
try
|
|
{
|
|
m_logger.ReadNode(variableId);
|
|
DataValue value = await uaClient
|
|
.Session.ReadValueAsync(variableId, ct)
|
|
.ConfigureAwait(false);
|
|
values.Add(value);
|
|
errors.Add(value.StatusCode);
|
|
|
|
if (ServiceResult.IsNotBad(value.StatusCode))
|
|
{
|
|
string valueString = FormatValueAsJson(
|
|
uaClient.Session.MessageContext,
|
|
variableId.ToString(),
|
|
value,
|
|
JsonEncoderOptions.Compact);
|
|
m_logger.Value(valueString);
|
|
}
|
|
else
|
|
{
|
|
m_logger.StatusCodeError(value.StatusCode);
|
|
}
|
|
}
|
|
catch (ServiceResultException sre)
|
|
{
|
|
m_logger.Error(sre);
|
|
values.Add(DataValue.FromStatusCode(sre.StatusCode));
|
|
errors.Add(sre.Result);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
(ArrayOf<DataValue> valueList, ArrayOf<ServiceResult> errorList) = await uaClient.Session.ReadValuesAsync(variableIds, ct)
|
|
.ConfigureAwait(false);
|
|
|
|
values = valueList.ToList();
|
|
errors = errorList.ToList();
|
|
|
|
int ii = 0;
|
|
foreach (DataValue value in values)
|
|
{
|
|
if (ServiceResult.IsNotBad(errors[ii]))
|
|
{
|
|
string valueString = FormatValueAsJson(
|
|
uaClient.Session.MessageContext,
|
|
variableIds[ii].ToString(),
|
|
value,
|
|
JsonEncoderOptions.Compact);
|
|
m_logger.Value(valueString);
|
|
}
|
|
else
|
|
{
|
|
m_logger.StatusCodeError(value.StatusCode);
|
|
}
|
|
ii++;
|
|
}
|
|
}
|
|
|
|
retrySingleRead = false;
|
|
}
|
|
catch (ServiceResultException sre) when (sre.StatusCode == StatusCodes
|
|
.BadEncodingLimitsExceeded)
|
|
{
|
|
m_logger.RetryReadValuesDueToError(sre.Message);
|
|
retrySingleRead = !retrySingleRead;
|
|
}
|
|
} while (retrySingleRead);
|
|
|
|
return ResultSet.From(values!, errors!);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Subscribe to all variables in the list.
|
|
/// </summary>
|
|
/// <param name="uaClient">The UAClient with a session to use.</param>
|
|
/// <param name="variableIds">The variables to subscribe.</param>
|
|
public async Task SubscribeAllValuesAsync(
|
|
IUAClient uaClient,
|
|
ArrayOf<Node> variableIds,
|
|
int samplingInterval,
|
|
int publishingInterval,
|
|
uint queueSize,
|
|
uint lifetimeCount,
|
|
uint keepAliveCount,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (uaClient.Session == null || !uaClient.Session.Connected)
|
|
{
|
|
m_logger.SessionNotConnected();
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Create a subscription for receiving data change notifications
|
|
ISession session = uaClient.Session;
|
|
|
|
// test for deferred ack of sequence numbers
|
|
session.PublishSequenceNumbersToAcknowledge += DeferSubscriptionAcknowledge;
|
|
|
|
// set a minimum amount of three publish requests per session
|
|
session.MinPublishRequestCount = 3;
|
|
|
|
// Define Subscription parameters
|
|
// Ownership of the subscription transfers to the session in AddSubscription
|
|
// below, which disposes all of its subscriptions when it is disposed.
|
|
// TODO: remove this suppression if CA2000 learns this ownership transfer pattern.
|
|
#pragma warning disable CA2000
|
|
var subscription = new Subscription(session.DefaultSubscription)
|
|
{
|
|
DisplayName = "Console ReferenceClient Subscription",
|
|
PublishingEnabled = true,
|
|
PublishingInterval = publishingInterval,
|
|
LifetimeCount = lifetimeCount,
|
|
KeepAliveCount = keepAliveCount,
|
|
SequentialPublishing = true,
|
|
RepublishAfterTransfer = true,
|
|
DisableMonitoredItemCache = true,
|
|
MaxNotificationsPerPublish = 1000,
|
|
MinLifetimeInterval = (uint)session.SessionTimeout,
|
|
FastDataChangeCallback = FastDataChangeNotification,
|
|
FastKeepAliveCallback = FastKeepAliveNotification
|
|
};
|
|
#pragma warning restore CA2000
|
|
|
|
session.AddSubscription(subscription);
|
|
|
|
// Create the subscription on Server side
|
|
await subscription.CreateAsync(ct).ConfigureAwait(false);
|
|
m_logger.NewSubscriptionCreated(subscription.Id);
|
|
|
|
// Create MonitoredItems for data changes
|
|
foreach (Node item in variableIds)
|
|
{
|
|
var monitoredItem = new MonitoredItem(subscription.DefaultItem)
|
|
{
|
|
StartNodeId = item.NodeId,
|
|
AttributeId = Attributes.Value,
|
|
SamplingInterval = samplingInterval,
|
|
DisplayName = item.DisplayName.Text ?? item.BrowseName.Name ?? "unknown",
|
|
QueueSize = queueSize,
|
|
DiscardOldest = true,
|
|
MonitoringMode = MonitoringMode.Reporting
|
|
};
|
|
subscription.AddItem(monitoredItem);
|
|
if (subscription.CurrentKeepAliveCount > 1000)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Create the monitored items on Server side
|
|
await subscription.ApplyChangesAsync(ct).ConfigureAwait(false);
|
|
m_logger.MonitoredItemsCreatedForSubscription(
|
|
subscription.MonitoredItemCount,
|
|
subscription.Id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.SubscribeError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a prettified JSON string of a DataValue.
|
|
/// </summary>
|
|
/// <param name="name">The key of the Json value.</param>
|
|
/// <param name="value">The DataValue.</param>
|
|
/// <param name="jsonEncodingType">Use reversible encoding.</param>
|
|
public static string FormatValueAsJson(
|
|
IServiceMessageContext messageContext,
|
|
string name,
|
|
DataValue value,
|
|
JsonEncoderOptions? jsonEncodingType = null)
|
|
{
|
|
string textbuffer;
|
|
using (var jsonEncoder = new JsonEncoder(messageContext, jsonEncodingType))
|
|
{
|
|
jsonEncoder.WriteDataValue(name, value);
|
|
textbuffer = jsonEncoder.CloseAndReturnText();
|
|
}
|
|
|
|
using var doc = JsonDocument.Parse(textbuffer);
|
|
using var stream = new MemoryStream();
|
|
using (var writer = new Utf8JsonWriter(stream, new JsonWriterOptions
|
|
{
|
|
Indented = true
|
|
}))
|
|
{
|
|
doc.WriteTo(writer);
|
|
}
|
|
return Encoding.UTF8.GetString(stream.ToArray());
|
|
}
|
|
|
|
/// <summary>
|
|
/// The fast keep alive notification callback.
|
|
/// </summary>
|
|
private void FastKeepAliveNotification(
|
|
Subscription subscription,
|
|
NotificationData notification)
|
|
{
|
|
try
|
|
{
|
|
m_logger.KeepAliveNotification(
|
|
subscription.Id,
|
|
notification.PublishTime,
|
|
notification.SequenceNumber);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.FastKeepAliveNotificationError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The fast data change notification callback.
|
|
/// </summary>
|
|
private void FastDataChangeNotification(
|
|
Subscription subscription,
|
|
DataChangeNotification notification,
|
|
ArrayOf<string> stringTable)
|
|
{
|
|
try
|
|
{
|
|
m_logger.FastDataChangeNotification(
|
|
subscription.Id,
|
|
notification.PublishTime,
|
|
notification.SequenceNumber,
|
|
notification.MonitoredItems.Count);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.FastDataChangeNotificationError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle DataChange notifications from Server
|
|
/// </summary>
|
|
private void OnMonitoredItemNotification(
|
|
MonitoredItem monitoredItem,
|
|
MonitoredItemNotificationEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
// Log MonitoredItem Notification event
|
|
if (e.NotificationValue is not MonitoredItemNotification notification)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Warning))
|
|
{
|
|
m_logger.UnexpectedNotificationType(e.NotificationValue?.GetType().Name ?? "null");
|
|
}
|
|
return;
|
|
}
|
|
DateTime localTime = notification.Value.SourceTimestamp.ToLocalTime();
|
|
if (m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
m_logger.MonitoredItemNotification(
|
|
notification.Message.SequenceNumber,
|
|
monitoredItem.ResolvedNodeId,
|
|
notification.Value,
|
|
localTime.ToLongTimeString());
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.OnMonitoredItemNotificationError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Handle Requested Event notifications from Server
|
|
/// </summary>
|
|
private void OnMonitoredItemEventNotification(
|
|
MonitoredItem monitoredItem,
|
|
MonitoredItemNotificationEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
// Log MonitoredItem Notification event
|
|
if (e.NotificationValue is not EventFieldList notification)
|
|
{
|
|
if (m_logger.IsEnabled(LogLevel.Warning))
|
|
{
|
|
m_logger.UnexpectedEventNotificationType(e.NotificationValue?.GetType().Name ?? "null");
|
|
}
|
|
return;
|
|
}
|
|
|
|
foreach (KeyValuePair<int, ArrayOf<QualifiedName>> entry in m_desiredEventFields)
|
|
{
|
|
Variant field = notification.EventFields[entry.Key];
|
|
if (field.TypeInfo.BuiltInType != BuiltInType.Null)
|
|
{
|
|
var fieldPath = new StringBuilder();
|
|
|
|
int lastIndex = entry.Value.Count - 1;
|
|
for (int index = 0; index < entry.Value.Count; index++)
|
|
{
|
|
fieldPath.Append(entry.Value[index].Name);
|
|
if (index < lastIndex)
|
|
{
|
|
fieldPath.Append('.');
|
|
}
|
|
}
|
|
|
|
string fieldName = fieldPath.ToString();
|
|
if (fieldName.Equals("Time", StringComparison.Ordinal))
|
|
{
|
|
try
|
|
{
|
|
DateTimeUtc currentTime = field.GetDateTime();
|
|
TimeSpan timeSpan = currentTime - m_lastEventTime;
|
|
m_lastEventTime = currentTime;
|
|
m_processedEvents++;
|
|
if (m_processedEvents > 1)
|
|
{
|
|
m_logger.EventReceivedWithTimeBetweenEvents(
|
|
m_processedEvents,
|
|
timeSpan.Seconds);
|
|
}
|
|
else
|
|
{
|
|
m_logger.EventReceived(m_processedEvents);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.UnexpectedEventTimeFieldValueError(ex);
|
|
}
|
|
}
|
|
|
|
m_logger.EventField(
|
|
entry.Key,
|
|
fieldName,
|
|
field);
|
|
}
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
m_logger.OnMonitoredItemEventNotificationError(ex);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Event handler to defer publish response sequence number acknowledge.
|
|
/// </summary>
|
|
private static void DeferSubscriptionAcknowledge(
|
|
ISession session,
|
|
PublishSequenceNumbersToAcknowledgeEventArgs e)
|
|
{
|
|
// for testing keep the latest sequence numbers for a while
|
|
const int ackDelay = 5;
|
|
if (e.AcknowledgementsToSend.Count > 0)
|
|
{
|
|
// defer latest sequence numbers
|
|
List<SubscriptionAcknowledgement> deferredItems = [.. e
|
|
.AcknowledgementsToSend.OrderByDescending(s => s.SequenceNumber)
|
|
.Take(ackDelay)];
|
|
e.DeferredAcknowledgementsToSend.AddRange(deferredItems);
|
|
foreach (SubscriptionAcknowledgement deferredItem in deferredItems)
|
|
{
|
|
e.AcknowledgementsToSend.Remove(deferredItem);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create a browse description from a node id collection.
|
|
/// </summary>
|
|
/// <param name="nodeIdCollection">The node id collection.</param>
|
|
/// <param name="template">The template for the browse description for each node id.</param>
|
|
private static ArrayOf<BrowseDescription> CreateBrowseDescriptionCollectionFromNodeId(
|
|
ArrayOf<NodeId> nodeIdCollection,
|
|
BrowseDescription template)
|
|
{
|
|
var browseDescriptionCollection = new List<BrowseDescription>();
|
|
foreach (NodeId nodeId in nodeIdCollection)
|
|
{
|
|
BrowseDescription browseDescription = CoreUtils.Clone(template)!;
|
|
browseDescription.NodeId = nodeId;
|
|
browseDescriptionCollection.Add(browseDescription);
|
|
}
|
|
return browseDescriptionCollection;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exports nodes to a NodeSet2 XML file.
|
|
/// </summary>
|
|
/// <param name="session">The session to use for exporting.</param>
|
|
/// <param name="nodes">The list of nodes to export.</param>
|
|
/// <param name="filePath">The path where the NodeSet2 XML file will be saved.</param>
|
|
public void ExportNodesToNodeSet2(ISession session, IList<INode> nodes, string filePath)
|
|
{
|
|
m_logger.ExportingNodes(nodes.Count, filePath);
|
|
|
|
var stopwatch = new Stopwatch();
|
|
stopwatch.Start();
|
|
|
|
using var outputStream = new FileStream(filePath, FileMode.Create);
|
|
var systemContext = new SystemContext(m_telemetry)
|
|
{
|
|
NamespaceUris = session.NamespaceUris,
|
|
ServerUris = session.ServerUris
|
|
};
|
|
|
|
CoreClientUtils.ExportNodesToNodeSet2(systemContext, nodes, outputStream);
|
|
|
|
stopwatch.Stop();
|
|
|
|
m_logger.ExportedNodes(
|
|
nodes.Count,
|
|
filePath,
|
|
stopwatch.ElapsedMilliseconds);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Exports nodes to separate NodeSet2 XML files, one per namespace.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// When <paramref name="targetNamespaces"/> is supplied and non-empty, only nodes whose
|
|
/// namespace URI is in the set are exported. Otherwise, all namespaces except the OPC UA
|
|
/// base namespace (<see cref="Namespaces.OpcUa"/>) are exported, which includes any
|
|
/// companion specifications hosted by the server.
|
|
/// </remarks>
|
|
/// <param name="session">The session to use for exporting.</param>
|
|
/// <param name="nodes">The list of nodes to export.</param>
|
|
/// <param name="outputDirectory">The directory where NodeSet2 XML files will be saved.</param>
|
|
/// <param name="targetNamespaces">
|
|
/// Optional set of namespace URIs to include. When null or empty, all namespaces except
|
|
/// the OPC UA base namespace are exported.
|
|
/// </param>
|
|
/// <param name="cancellationToken">Optional cancellation token.</param>
|
|
/// <returns>A dictionary mapping namespace URI to the file path of the exported NodeSet2 file.</returns>
|
|
/// <exception cref="ArgumentNullException">Thrown when session, nodes, or outputDirectory is null.</exception>
|
|
/// <exception cref="ArgumentException">Thrown when outputDirectory is empty or whitespace.</exception>
|
|
public async Task<IReadOnlyDictionary<string, string>> ExportNodesToNodeSet2PerNamespaceAsync(
|
|
ISession session,
|
|
IList<INode> nodes,
|
|
string outputDirectory,
|
|
IReadOnlyCollection<string>? targetNamespaces = null,
|
|
CancellationToken cancellationToken = default)
|
|
{
|
|
if (session == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(session));
|
|
}
|
|
if (nodes == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(nodes));
|
|
}
|
|
if (string.IsNullOrWhiteSpace(outputDirectory))
|
|
{
|
|
throw new ArgumentException("Value cannot be null or whitespace.", nameof(outputDirectory));
|
|
}
|
|
|
|
HashSet<string>? targetSet = targetNamespaces is { Count: > 0 }
|
|
? new HashSet<string>(targetNamespaces, StringComparer.OrdinalIgnoreCase)
|
|
: null;
|
|
|
|
if (m_logger.IsEnabled(LogLevel.Information))
|
|
{
|
|
m_logger.ExportingNodesPerNamespace(
|
|
nodes.Count,
|
|
outputDirectory,
|
|
targetSet != null ? string.Join(",", targetSet) : "all non-OPC-UA-base");
|
|
}
|
|
|
|
var stopwatch = Stopwatch.StartNew();
|
|
|
|
// Ensure output directory exists
|
|
Directory.CreateDirectory(outputDirectory);
|
|
|
|
// Group nodes by namespace, applying the requested filter.
|
|
var nodesByNamespace = nodes
|
|
.Where(node => node.NodeId.NamespaceIndex > 0) // Skip namespace 0 (OPC UA base)
|
|
.GroupBy(node => node.NodeId.NamespaceIndex)
|
|
.Where(group =>
|
|
{
|
|
string? namespaceUri = session.NamespaceUris.GetString(group.Key);
|
|
if (string.IsNullOrEmpty(namespaceUri))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (targetSet != null)
|
|
{
|
|
// Caller asked for a specific set of namespaces. namespaceUri is non-null
|
|
// after !IsNullOrEmpty (NotNullWhen attribute not available on net48).
|
|
return targetSet.Contains(namespaceUri!);
|
|
}
|
|
|
|
// Default: exclude only the OPC UA base namespace; include companion specs.
|
|
return !string.Equals(namespaceUri, Namespaces.OpcUa, StringComparison.OrdinalIgnoreCase);
|
|
})
|
|
.ToDictionary(
|
|
group => group.Key,
|
|
group => group.ToList());
|
|
|
|
var exportedFiles = new Dictionary<string, string>();
|
|
|
|
// Export each namespace to its own file
|
|
foreach (KeyValuePair<ushort, List<INode>> kvp in nodesByNamespace)
|
|
{
|
|
cancellationToken.ThrowIfCancellationRequested();
|
|
|
|
// The Where clause above filtered out null/empty namespace URIs.
|
|
string namespaceUri = session.NamespaceUris.GetString(kvp.Key)!;
|
|
|
|
// Create a safe filename from the namespace URI
|
|
|
|
string fileName = CreateSafeFileName(namespaceUri, kvp.Key);
|
|
string filePath = Path.Combine(outputDirectory, fileName);
|
|
|
|
m_logger.ExportingNamespace(
|
|
kvp.Key,
|
|
namespaceUri,
|
|
kvp.Value.Count,
|
|
filePath);
|
|
|
|
await Task.Run(() =>
|
|
{
|
|
using var outputStream = new FileStream(filePath, FileMode.Create);
|
|
var systemContext = new SystemContext(m_telemetry)
|
|
{
|
|
NamespaceUris = session.NamespaceUris,
|
|
ServerUris = session.ServerUris
|
|
};
|
|
|
|
CoreClientUtils.ExportNodesToNodeSet2(systemContext, kvp.Value, outputStream, NodeSetExportOptions.Complete);
|
|
}, cancellationToken).ConfigureAwait(false);
|
|
|
|
exportedFiles[namespaceUri] = filePath;
|
|
}
|
|
|
|
stopwatch.Stop();
|
|
|
|
m_logger.ExportedNamespaces(
|
|
exportedFiles.Count,
|
|
nodes.Count,
|
|
stopwatch.ElapsedMilliseconds);
|
|
|
|
return exportedFiles;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a safe filename from a namespace URI.
|
|
/// </summary>
|
|
/// <param name="namespaceUri">The namespace URI.</param>
|
|
/// <param name="namespaceIndex">The namespace index (used as fallback).</param>
|
|
/// <returns>A safe filename for the NodeSet2 export.</returns>
|
|
private static string CreateSafeFileName(string namespaceUri, ushort namespaceIndex)
|
|
{
|
|
// Extract meaningful part from URI
|
|
string fileName = namespaceUri
|
|
.Replace("http://", string.Empty, StringComparison.OrdinalIgnoreCase)
|
|
.Replace("https://", string.Empty, StringComparison.OrdinalIgnoreCase)
|
|
.Replace("urn:", string.Empty, StringComparison.OrdinalIgnoreCase);
|
|
|
|
// Replace invalid filename characters
|
|
foreach (char c in Path.GetInvalidFileNameChars())
|
|
{
|
|
fileName = fileName.Replace(c, '_');
|
|
}
|
|
|
|
// Additional cleanup for common URI characters
|
|
fileName = fileName
|
|
.Replace('/', '_')
|
|
.Replace('\\', '_')
|
|
.Replace(':', '_')
|
|
.TrimEnd('_');
|
|
|
|
// Limit length and ensure uniqueness with namespace index
|
|
if (fileName.Length > 200)
|
|
{
|
|
fileName = fileName[..200];
|
|
}
|
|
|
|
return $"{fileName}_ns{namespaceIndex}.xml";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Create the continuation point collection from the browse result
|
|
/// collection for the BrowseNext service.
|
|
/// </summary>
|
|
/// <param name="browseResultCollection">The browse result collection to use.</param>
|
|
/// <returns>The collection of continuation points for the BrowseNext service.</returns>
|
|
private static ArrayOf<ByteString> PrepareBrowseNext(
|
|
ArrayOf<BrowseResult> browseResultCollection)
|
|
{
|
|
var continuationPoints = new List<ByteString>();
|
|
foreach (BrowseResult browseResult in browseResultCollection)
|
|
{
|
|
if (!browseResult.ContinuationPoint.IsEmpty)
|
|
{
|
|
continuationPoints.Add(browseResult.ContinuationPoint);
|
|
}
|
|
}
|
|
return continuationPoints;
|
|
}
|
|
|
|
private void ValidateResponse<TRequest, TResponse>(
|
|
ArrayOf<TRequest> requests,
|
|
ArrayOf<TResponse> responses)
|
|
{
|
|
if (m_validate != null)
|
|
{
|
|
m_validate(requests.ToArray()!, responses.ToArray()!);
|
|
}
|
|
else
|
|
{
|
|
ClientBase.ValidateResponse(responses, requests);
|
|
}
|
|
}
|
|
|
|
private readonly Action<Array, Array>? m_validate;
|
|
private readonly ITelemetryContext m_telemetry;
|
|
private readonly ILogger m_logger;
|
|
private readonly ManualResetEvent? m_quitEvent;
|
|
private readonly bool m_verbose;
|
|
private readonly Dictionary<int, ArrayOf<QualifiedName>> m_desiredEventFields;
|
|
private int m_processedEvents;
|
|
private DateTimeUtc m_lastEventTime = DateTimeUtc.Now;
|
|
}
|
|
}
|
|
|
|
namespace Quickstarts
|
|
{
|
|
internal static partial class ClientSamplesLog
|
|
{
|
|
[LoggerMessage(EventId = 9100 + 0, Level = LogLevel.Error,
|
|
Message = "Read Nodes Error.")]
|
|
public static partial void ReadNodesError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 1, Level = LogLevel.Information,
|
|
Message = "Write Nodes Error.")]
|
|
public static partial void WriteNodesError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 2, Level = LogLevel.Error,
|
|
Message = "Browse Error.")]
|
|
public static partial void BrowseError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 3, Level = LogLevel.Error,
|
|
Message = "Method call error")]
|
|
public static partial void MethodCallError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 4, Level = LogLevel.Information,
|
|
Message = "New Subscription created with SubscriptionId = {Id}, Sampling Interval {SamplingInterval}, " +
|
|
"Publishing Interval {PublishingInterval}.")]
|
|
public static partial void NewSubscriptionCreatedWithIntervals(
|
|
this ILogger logger,
|
|
uint id,
|
|
int samplingInterval,
|
|
int publishingInterval);
|
|
|
|
[LoggerMessage(EventId = 9100 + 5, Level = LogLevel.Information,
|
|
Message = "Subscription {SubscriptionId} is now durable, Revised Lifetime {Lifetime} in hours.")]
|
|
public static partial void SubscriptionDurable(this ILogger logger, uint subscriptionId, uint lifetime);
|
|
|
|
[LoggerMessage(EventId = 9100 + 6, Level = LogLevel.Information,
|
|
Message = "Subscription {SubscriptionId} failed durable call")]
|
|
public static partial void SubscriptionDurableFailed(this ILogger logger, uint subscriptionId);
|
|
|
|
[LoggerMessage(EventId = 9100 + 7, Level = LogLevel.Information,
|
|
Message = "MonitoredItems created for SubscriptionId = {SubscriptionId}.")]
|
|
public static partial void MonitoredItemsCreated(this ILogger logger, uint subscriptionId);
|
|
|
|
[LoggerMessage(EventId = 9100 + 8, Level = LogLevel.Error,
|
|
Message = "Subscribe error")]
|
|
public static partial void SubscribeError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 9, Level = LogLevel.Warning,
|
|
Message = "Root node {NodeId} not found, skipping")]
|
|
public static partial void RootNodeNotFound(this ILogger logger, NodeId nodeId);
|
|
|
|
[LoggerMessage(EventId = 9100 + 10, Level = LogLevel.Information,
|
|
Message = "Browse aborted.")]
|
|
public static partial void BrowseAborted(this ILogger logger);
|
|
|
|
[LoggerMessage(EventId = 9100 + 11, Level = LogLevel.Information,
|
|
Message = "{Depth}: Find {Count} references after {Duration}ms")]
|
|
public static partial void FindReferencesAfterDuration(
|
|
this ILogger logger,
|
|
int depth,
|
|
int count,
|
|
long duration);
|
|
|
|
[LoggerMessage(EventId = 9100 + 12, Level = LogLevel.Information,
|
|
Message = "Find References {Count} duplicate nodes were ignored")]
|
|
public static partial void FindReferencesDuplicateNodesIgnored(this ILogger logger, int count);
|
|
|
|
[LoggerMessage(EventId = 9100 + 13, Level = LogLevel.Information,
|
|
Message = "Find References {Count} leaf nodes were ignored")]
|
|
public static partial void FindReferencesLeafNodesIgnored(this ILogger logger, int count);
|
|
|
|
[LoggerMessage(EventId = 9100 + 14, Level = LogLevel.Information,
|
|
Message = "FetchAllNodesNodeCache found {Count} nodes in {Duration}ms")]
|
|
public static partial void FetchAllNodesNodeCacheFound(this ILogger logger, int count, long duration);
|
|
|
|
[LoggerMessage(EventId = 9100 + 15, Level = LogLevel.Information,
|
|
Message = "NodeId {NodeId} {NodeClass} {BrowseName}")]
|
|
public static partial void NodeInfo(
|
|
this ILogger logger,
|
|
ExpandedNodeId nodeId,
|
|
NodeClass nodeClass,
|
|
QualifiedName browseName);
|
|
|
|
[LoggerMessage(EventId = 9100 + 16, Level = LogLevel.Warning,
|
|
Message = "Setting the BrowseResultMask is not supported by the ManagedBrowse method. " +
|
|
"Using '{BrowseResultMask}' instead of the mask {BrowseDescriptionResultMask} for the result mask")]
|
|
public static partial void UnsupportedBrowseResultMask(
|
|
this ILogger logger,
|
|
BrowseResultMask browseResultMask,
|
|
uint browseDescriptionResultMask);
|
|
|
|
[LoggerMessage(EventId = 9100 + 17, Level = LogLevel.Information,
|
|
Message = "{Depth}: Browse {Count} nodes after {Duration}ms")]
|
|
public static partial void BrowseNodesAfterDuration(this ILogger logger, int depth, int count, long duration);
|
|
|
|
[LoggerMessage(EventId = 9100 + 18, Level = LogLevel.Error,
|
|
Message = "Browse error")]
|
|
public static partial void BrowseServiceError(this ILogger logger, ServiceResultException sre);
|
|
|
|
[LoggerMessage(EventId = 9100 + 19, Level = LogLevel.Information,
|
|
Message = "Managed Browse Result {Count} duplicate nodes were ignored.")]
|
|
public static partial void ManagedBrowseDuplicateNodesIgnored(this ILogger logger, int count);
|
|
|
|
[LoggerMessage(EventId = 9100 + 20, Level = LogLevel.Information,
|
|
Message = "ManagedBrowseFullAddressSpace found {Count} references on server in {Duration}ms.")]
|
|
public static partial void ManagedBrowseFullAddressSpaceFound(this ILogger logger, int count, long duration);
|
|
|
|
[LoggerMessage(EventId = 9100 + 21, Level = LogLevel.Error,
|
|
Message = "Browse error.")]
|
|
public static partial void BrowseServiceErrorWithPeriod(this ILogger logger, ServiceResultException sre);
|
|
|
|
[LoggerMessage(EventId = 9100 + 22, Level = LogLevel.Information,
|
|
Message = "BrowseNext {Count} continuation points.")]
|
|
public static partial void BrowseNextContinuationPoints(this ILogger logger, int count);
|
|
|
|
[LoggerMessage(EventId = 9100 + 23, Level = LogLevel.Information,
|
|
Message = "Browse Result {Count} duplicate nodes were ignored.")]
|
|
public static partial void BrowseResultDuplicateNodesIgnored(this ILogger logger, int count);
|
|
|
|
[LoggerMessage(EventId = 9100 + 24, Level = LogLevel.Information,
|
|
Message = "BrowseFullAddressSpace found {Count} references on server in {Duration}ms.")]
|
|
public static partial void BrowseFullAddressSpaceFound(this ILogger logger, int count, long duration);
|
|
|
|
[LoggerMessage(EventId = 9100 + 25, Level = LogLevel.Information,
|
|
Message = "Load the server type system.")]
|
|
public static partial void LoadServerTypeSystem(this ILogger logger);
|
|
|
|
[LoggerMessage(EventId = 9100 + 26, Level = LogLevel.Information,
|
|
Message = "Loaded {Count} types took {Duration}ms.")]
|
|
public static partial void LoadedTypes(this ILogger logger, int count, long duration);
|
|
|
|
[LoggerMessage(EventId = 9100 + 27, Level = LogLevel.Information,
|
|
Message = "Custom types defined for this session:")]
|
|
public static partial void CustomTypesDefined(this ILogger logger);
|
|
|
|
[LoggerMessage(EventId = 9100 + 28, Level = LogLevel.Information,
|
|
Message = "{Namespace}.{TypeName}")]
|
|
public static partial void TypeName(this ILogger logger, string? @namespace, string typeName);
|
|
|
|
[LoggerMessage(EventId = 9100 + 29, Level = LogLevel.Information,
|
|
Message = "Loaded {Count} dictionaries:")]
|
|
public static partial void LoadedDictionaries(this ILogger logger, int count);
|
|
|
|
[LoggerMessage(EventId = 9100 + 30, Level = LogLevel.Information,
|
|
Message = " + {DictionaryName}")]
|
|
public static partial void DictionaryName(this ILogger logger, string? dictionaryName);
|
|
[LoggerMessage(EventId = 9100 + 31, Level = LogLevel.Information,
|
|
Message = " -- {NodeId}:{BrowseName}")]
|
|
public static partial void DictionaryType(this ILogger logger, NodeId nodeId, QualifiedName browseName);
|
|
|
|
[LoggerMessage(EventId = 9100 + 32, Level = LogLevel.Information,
|
|
Message = "Read {NodeId}")]
|
|
public static partial void ReadNode(this ILogger logger, NodeId nodeId);
|
|
|
|
[LoggerMessage(EventId = 9100 + 33, Level = LogLevel.Information,
|
|
Message = "{Value}")]
|
|
public static partial void Value(this ILogger logger, string value);
|
|
|
|
[LoggerMessage(EventId = 9100 + 34, Level = LogLevel.Information,
|
|
Message = "{StatusCode}")]
|
|
public static partial void StatusCodeError(this ILogger logger, StatusCode statusCode);
|
|
|
|
[LoggerMessage(EventId = 9100 + 35, Level = LogLevel.Error,
|
|
Message = "Error")]
|
|
public static partial void Error(this ILogger logger, ServiceResultException sre);
|
|
|
|
[LoggerMessage(EventId = 9100 + 36, Level = LogLevel.Information,
|
|
Message = "Retry to read the values due to error: {Error}")]
|
|
public static partial void RetryReadValuesDueToError(this ILogger logger, string error);
|
|
|
|
[LoggerMessage(EventId = 9100 + 37, Level = LogLevel.Information,
|
|
Message = "Session not connected!")]
|
|
public static partial void SessionNotConnected(this ILogger logger);
|
|
|
|
[LoggerMessage(EventId = 9100 + 38, Level = LogLevel.Information,
|
|
Message = "New Subscription created with SubscriptionId = {SubscriptionId}.")]
|
|
public static partial void NewSubscriptionCreated(this ILogger logger, uint subscriptionId);
|
|
|
|
[LoggerMessage(EventId = 9100 + 39, Level = LogLevel.Information,
|
|
Message = "MonitoredItems {Count} created for SubscriptionId = {SubscriptionId}.")]
|
|
public static partial void MonitoredItemsCreatedForSubscription(
|
|
this ILogger logger,
|
|
uint count,
|
|
uint subscriptionId);
|
|
|
|
[LoggerMessage(EventId = 9100 + 40, Level = LogLevel.Information,
|
|
Message = "Keep Alive : Id={SubscriptionId} PublishTime={PublishTime} SequenceNumber={SequenceNumber}.")]
|
|
public static partial void KeepAliveNotification(
|
|
this ILogger logger,
|
|
uint subscriptionId,
|
|
DateTimeUtc publishTime,
|
|
uint sequenceNumber);
|
|
|
|
[LoggerMessage(EventId = 9100 + 41, Level = LogLevel.Error,
|
|
Message = "FastKeepAliveNotification error")]
|
|
public static partial void FastKeepAliveNotificationError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 42, Level = LogLevel.Information,
|
|
Message = "Notification: Id={SubscriptionId} PublishTime={PublishTime} " +
|
|
"SequenceNumber={SequenceNumber} Items={Count}.")]
|
|
public static partial void FastDataChangeNotification(
|
|
this ILogger logger,
|
|
uint subscriptionId,
|
|
DateTimeUtc publishTime,
|
|
uint sequenceNumber,
|
|
int count);
|
|
|
|
[LoggerMessage(EventId = 9100 + 43, Level = LogLevel.Error,
|
|
Message = "FastDataChangeNotification error")]
|
|
public static partial void FastDataChangeNotificationError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 44, Level = LogLevel.Warning,
|
|
Message = "Unexpected notification type: {Type}")]
|
|
public static partial void UnexpectedNotificationType(this ILogger logger, string type);
|
|
|
|
[LoggerMessage(EventId = 9100 + 45, Level = LogLevel.Information,
|
|
Message = "Notification: {SequenceNumber} \"{NodeId}\" and Value = {Value} at [{CurrentTime}].")]
|
|
public static partial void MonitoredItemNotification(
|
|
this ILogger logger,
|
|
uint sequenceNumber,
|
|
NodeId nodeId,
|
|
DataValue value,
|
|
string currentTime);
|
|
|
|
[LoggerMessage(EventId = 9100 + 46, Level = LogLevel.Error,
|
|
Message = "OnMonitoredItemNotification error")]
|
|
public static partial void OnMonitoredItemNotificationError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 47, Level = LogLevel.Warning,
|
|
Message = "Unexpected event notification type: {Type}")]
|
|
public static partial void UnexpectedEventNotificationType(this ILogger logger, string type);
|
|
|
|
[LoggerMessage(EventId = 9100 + 48, Level = LogLevel.Information,
|
|
Message = "Event Received - total count = {Count}, time since last event = {TimeBetweenEvents} seconds")]
|
|
public static partial void EventReceivedWithTimeBetweenEvents(
|
|
this ILogger logger,
|
|
int count,
|
|
int timeBetweenEvents);
|
|
|
|
[LoggerMessage(EventId = 9100 + 49, Level = LogLevel.Information,
|
|
Message = "Event Received - total count = {Count}")]
|
|
public static partial void EventReceived(this ILogger logger, int count);
|
|
|
|
[LoggerMessage(EventId = 9100 + 50, Level = LogLevel.Error,
|
|
Message = "Unexpected error retrieving Event Time Field Value")]
|
|
public static partial void UnexpectedEventTimeFieldValueError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 51, Level = LogLevel.Information,
|
|
Message = "\tField [{Index}] \"{Name}\" = [{Value}]")]
|
|
public static partial void EventField(this ILogger logger, int index, string name, Variant value);
|
|
|
|
[LoggerMessage(EventId = 9100 + 52, Level = LogLevel.Error,
|
|
Message = "OnMonitoredItemEventNotification error")]
|
|
public static partial void OnMonitoredItemEventNotificationError(this ILogger logger, Exception ex);
|
|
|
|
[LoggerMessage(EventId = 9100 + 53, Level = LogLevel.Information,
|
|
Message = "Exporting {Count} nodes to {FilePath}...")]
|
|
public static partial void ExportingNodes(this ILogger logger, int count, string filePath);
|
|
|
|
[LoggerMessage(EventId = 9100 + 54, Level = LogLevel.Information,
|
|
Message = "Exported {Count} nodes to {FilePath} in {Duration}ms")]
|
|
public static partial void ExportedNodes(this ILogger logger, int count, string filePath, long duration);
|
|
|
|
[LoggerMessage(EventId = 9100 + 55, Level = LogLevel.Information,
|
|
Message = "Exporting {Count} nodes to separate NodeSet2 files per namespace in {Directory} " +
|
|
"(filter: {Filter})...")]
|
|
public static partial void ExportingNodesPerNamespace(
|
|
this ILogger logger,
|
|
int count,
|
|
string directory,
|
|
string filter);
|
|
|
|
[LoggerMessage(EventId = 9100 + 56, Level = LogLevel.Information,
|
|
Message = "Exporting namespace {NamespaceIndex} ({NamespaceUri}): {Count} nodes to {FilePath}")]
|
|
public static partial void ExportingNamespace(
|
|
this ILogger logger,
|
|
ushort namespaceIndex,
|
|
string namespaceUri,
|
|
int count,
|
|
string filePath);
|
|
|
|
[LoggerMessage(EventId = 9100 + 57, Level = LogLevel.Information,
|
|
Message = "Exported {NamespaceCount} namespaces ({NodeCount} total nodes) in {Duration}ms")]
|
|
public static partial void ExportedNamespaces(
|
|
this ILogger logger,
|
|
int namespaceCount,
|
|
int nodeCount,
|
|
long duration);
|
|
}
|
|
}
|