/* ======================================================================== * 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.Text; using Opc.Ua.Client; namespace Opc.Ua.Aot.Tests { /// /// Simplified OPC UA client samples for AOT integration testing. /// Each method performs an OPC UA operation and asserts the results /// using TUnit assertions. /// public static class AotClientSamples { private const int kMaxSearchDepth = 128; /// /// Read a list of nodes from the server and assert values are returned. /// public static async Task ReadNodesAsync(ISession session) { ArrayOf nodesToRead = [ new ReadValueId { NodeId = VariableIds.Server_ServerStatus, AttributeId = Attributes.Value }, new ReadValueId { NodeId = VariableIds.Server_ServerStatus_StartTime, AttributeId = Attributes.BrowseName }, new ReadValueId { NodeId = VariableIds.Server_ServerStatus_StartTime, AttributeId = Attributes.Value } ]; ReadResponse response = await session.ReadAsync( null, 0, TimestampsToReturn.Both, nodesToRead, CancellationToken.None).ConfigureAwait(false); await Assert.That(response).IsNotNull(); await Assert.That(response.Results.Count).IsEqualTo(nodesToRead.Count); foreach (DataValue result in response.Results.ToList()) { await Assert.That(StatusCode.IsGood(result.StatusCode)).IsTrue(); } // Also read the NamespaceArray DataValue namespaceArray = await session.ReadValueAsync( VariableIds.Server_NamespaceArray, CancellationToken.None).ConfigureAwait(false); await Assert.That(namespaceArray.IsNull).IsFalse(); await Assert.That(StatusCode.IsGood(namespaceArray.StatusCode)).IsTrue(); } /// /// Write a list of nodes to the server and assert success. /// public static async Task WriteNodesAsync(ISession session) { ArrayOf nodesToWrite = [ new WriteValue { NodeId = NodeId.Parse("ns=2;s=Scalar_Static_Int32"), AttributeId = Attributes.Value, Value = new DataValue(Variant.From(100)) }, new WriteValue { NodeId = NodeId.Parse("ns=2;s=Scalar_Static_Float"), AttributeId = Attributes.Value, Value = new DataValue(Variant.From(100.5f)) }, new WriteValue { NodeId = NodeId.Parse("ns=2;s=Scalar_Static_String"), AttributeId = Attributes.Value, Value = new DataValue(Variant.From("String Test")) } ]; WriteResponse response = await session.WriteAsync( null, nodesToWrite, CancellationToken.None).ConfigureAwait(false); await Assert.That(response).IsNotNull(); await Assert.That(response.Results.Count).IsEqualTo(nodesToWrite.Count); foreach (StatusCode writeResult in response.Results.ToList()) { await Assert.That(StatusCode.IsGood(writeResult)).IsTrue(); } } /// /// Browse the Server node and assert references are found. /// public static async Task BrowseAsync(ISession session) { var browser = new Browser(session) { BrowseDirection = BrowseDirection.Forward, NodeClassMask = (int)NodeClass.Object | (int)NodeClass.Variable, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true }; ArrayOf browseResults = await browser.BrowseAsync(ObjectIds.Server, CancellationToken.None).ConfigureAwait(false); await Assert.That(browseResults.Count).IsGreaterThan(0); foreach (ReferenceDescription result in browseResults.ToList()) { await Assert.That(result.DisplayName.Text).IsNotNull(); } } /// /// Call the Add method on the server and assert result. /// public static async Task CallMethodAsync(ISession session) { var objectId = NodeId.Parse("ns=2;s=Methods"); var methodId = NodeId.Parse("ns=2;s=Methods_Add"); ArrayOf outputArguments = await session.CallAsync( objectId, methodId, CancellationToken.None, (float)10.5, (uint)10).ConfigureAwait(false); await Assert.That(outputArguments.Count).IsGreaterThan(0); } /// /// Create a subscription with monitored items and assert creation. /// public static async Task SubscribeToDataChangesAsync(ISession session) { const int publishingInterval = 1000; const int samplingInterval = 1000; const uint queueSize = 10; #pragma warning disable CA2000 // Dispose objects before losing scope var subscription = new Subscription(session.DefaultSubscription) { DisplayName = "AotTest Subscription", PublishingEnabled = true, PublishingInterval = publishingInterval, LifetimeCount = 0, MinLifetimeInterval = 3, KeepAliveCount = 5 }; #pragma warning restore CA2000 // Dispose objects before losing scope session.AddSubscription(subscription); await subscription.CreateAsync(CancellationToken.None).ConfigureAwait(false); await Assert.That(subscription.Id).IsGreaterThan((uint)0); await Assert.That(subscription.Created).IsTrue(); // Add monitored items var intMonitoredItem = new MonitoredItem(subscription.DefaultItem) { StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_Int32"), AttributeId = Attributes.Value, DisplayName = "Int32 Variable", SamplingInterval = samplingInterval, QueueSize = queueSize, DiscardOldest = true }; subscription.AddItem(intMonitoredItem); var floatMonitoredItem = new MonitoredItem(subscription.DefaultItem) { StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_Float"), AttributeId = Attributes.Value, DisplayName = "Float Variable", SamplingInterval = samplingInterval, QueueSize = queueSize }; subscription.AddItem(floatMonitoredItem); var stringMonitoredItem = new MonitoredItem(subscription.DefaultItem) { StartNodeId = NodeId.Parse("ns=2;s=Scalar_Simulation_String"), AttributeId = Attributes.Value, DisplayName = "String Variable", SamplingInterval = samplingInterval, QueueSize = queueSize }; subscription.AddItem(stringMonitoredItem); await subscription.ApplyChangesAsync(CancellationToken.None).ConfigureAwait(false); await Assert.That((int)subscription.MonitoredItemCount).IsEqualTo(3); } /// /// Fetch all nodes via NodeCache and assert nodes are found. /// public static async Task> FetchAllNodesNodeCacheAsync( ISession session, NodeId startingNode) { var nodeDictionary = new Dictionary(); ArrayOf references = [ReferenceTypeIds.HierarchicalReferences]; ArrayOf nodesToBrowse = [startingNode]; // Prime NodeCache with reference types session.NodeCache.Clear(); await FetchReferenceIdTypesAsync(session).ConfigureAwait(false); int searchDepth = 0; while (nodesToBrowse.Count > 0 && searchDepth < kMaxSearchDepth) { searchDepth++; ArrayOf response = await session.NodeCache .FindReferencesAsync(nodesToBrowse, references, false, true, CancellationToken.None) .ConfigureAwait(false); var nextNodesToBrowse = new List(); foreach (INode node in response) { if (!nodeDictionary.ContainsKey(node.NodeId)) { nodeDictionary[node.NodeId] = node; } } nodesToBrowse = nextNodesToBrowse; } var result = nodeDictionary.Values.ToList(); result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); await Assert.That(result.Count).IsGreaterThan(0); return result; } /// /// Browse the full address space and assert references are found. /// public static async Task> BrowseFullAddressSpaceAsync( ISession session, NodeId startingNode) { const int kMaxReferencesPerNode = 1000; var browseTemplate = new BrowseDescription { NodeId = startingNode.IsNull ? ObjectIds.RootFolder : startingNode, BrowseDirection = BrowseDirection.Forward, ReferenceTypeId = ReferenceTypeIds.HierarchicalReferences, IncludeSubtypes = true, NodeClassMask = 0, ResultMask = (uint)BrowseResultMask.All }; ArrayOf browseDescriptionCollection = CreateBrowseDescriptionCollectionFromNodeId( [startingNode.IsNull ? ObjectIds.RootFolder : startingNode], browseTemplate); var referenceDescriptions = new Dictionary(); int searchDepth = 0; uint maxNodesPerBrowse = session.OperationLimits.MaxNodesPerBrowse; while (browseDescriptionCollection.Count > 0 && searchDepth < kMaxSearchDepth) { searchDepth++; var allBrowseResults = new List(); bool repeatBrowse; ArrayOf browseResultCollection = default; var unprocessedOperations = new List(); do { if (maxNodesPerBrowse >= browseDescriptionCollection.Count) { maxNodesPerBrowse = 0; } ArrayOf browseCollection = maxNodesPerBrowse == 0 ? browseDescriptionCollection : browseDescriptionCollection[..(int)maxNodesPerBrowse]; repeatBrowse = false; BrowseResponse browseResponse = await session.BrowseAsync( null, null, kMaxReferencesPerNode, browseCollection, CancellationToken.None).ConfigureAwait(false); browseResultCollection = browseResponse.Results; ClientBase.ValidateResponse(browseResultCollection, browseCollection); int ii = 0; foreach (BrowseResult browseResult in browseResultCollection) { if (browseResult.StatusCode == StatusCodes.BadNoContinuationPoints) { unprocessedOperations.Add(browseCollection[ii++]); continue; } allBrowseResults.Add(browseResult); ii++; } } while (repeatBrowse); browseDescriptionCollection = maxNodesPerBrowse == 0 ? default : browseDescriptionCollection[(int)maxNodesPerBrowse..]; // Browse next ArrayOf continuationPoints = PrepareBrowseNext(browseResultCollection); while (continuationPoints.Count > 0) { BrowseNextResponse browseNextResult = await session.BrowseNextAsync( null, false, continuationPoints, CancellationToken.None).ConfigureAwait(false); ArrayOf browseNextResultCollection = browseNextResult.Results; ClientBase.ValidateResponse(browseNextResultCollection, continuationPoints); allBrowseResults.AddRange(browseNextResultCollection); continuationPoints = PrepareBrowseNext(browseNextResultCollection); } // Build browse request for next level var browseTable = new List(); 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, session.NamespaceUris)); } } } } browseDescriptionCollection = ArrayOf.Combine( browseDescriptionCollection, CreateBrowseDescriptionCollectionFromNodeId(browseTable, browseTemplate), unprocessedOperations); } var result = new List(referenceDescriptions.Values); result.Sort((x, y) => x.NodeId.CompareTo(y.NodeId)); await Assert.That(result.Count).IsGreaterThan(0); return result; } /// /// Create a prettified JSON string of a DataValue. /// 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 = System.Text.Json.JsonDocument.Parse(textbuffer); using var stream = new MemoryStream(); using (var writer = new System.Text.Json.Utf8JsonWriter( stream, new System.Text.Json.JsonWriterOptions { Indented = true })) { doc.WriteTo(writer); } return Encoding.UTF8.GetString(stream.ToArray()); } /// /// Read all ReferenceTypeIds from the server that are not known by the client. /// private static Task FetchReferenceIdTypesAsync(ISession session) { NamespaceTable namespaceUris = session.NamespaceUris; var referenceTypes = ReferenceTypeIds.Identifiers .Select(nodeId => NodeId.ToExpandedNodeId(nodeId, namespaceUris)) .ToArrayOf(); return session.FetchTypeTreeAsync(referenceTypes, CancellationToken.None); } private static ArrayOf CreateBrowseDescriptionCollectionFromNodeId( ArrayOf nodeIdCollection, BrowseDescription template) { var browseDescriptionCollection = new List(); foreach (NodeId nodeId in nodeIdCollection) { BrowseDescription browseDescription = CoreUtils.Clone(template); browseDescription.NodeId = nodeId; browseDescriptionCollection.Add(browseDescription); } return browseDescriptionCollection; } private static ArrayOf PrepareBrowseNext( ArrayOf browseResultCollection) { var continuationPoints = new List(); foreach (BrowseResult browseResult in browseResultCollection) { if (!browseResult.ContinuationPoint.IsEmpty) { continuationPoints.Add(browseResult.ContinuationPoint); } } return continuationPoints; } } }