mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-29 07:07:24 +08:00
Add pinned UAFX Part 81 generated model contracts
This commit is contained in:
@@ -48,6 +48,7 @@ OPCFoundation.NetStandard.Opc.Ua.ECM
|
||||
OPCFoundation.NetStandard.Opc.Ua.Extrusion
|
||||
OPCFoundation.NetStandard.Opc.Ua.Extrusion.Client
|
||||
OPCFoundation.NetStandard.Opc.Ua.Extrusion.Server
|
||||
OPCFoundation.NetStandard.Opc.Ua.Fx
|
||||
OPCFoundation.NetStandard.Opc.Ua.Gds
|
||||
OPCFoundation.NetStandard.Opc.Ua.Gds.Client
|
||||
OPCFoundation.NetStandard.Opc.Ua.Gds.Server
|
||||
|
||||
@@ -36,6 +36,12 @@ src/Opc.Ua.AMB/Model/Opc.Ua.AMB.NodeSet2.xml text eol=lf
|
||||
src/Opc.Ua.AutoID/Model/Opc.Ua.AutoID.NodeSet2.xml text eol=lf whitespace=-blank-at-eol
|
||||
src/Opc.Ua.AutoID/Model/Opc.Ua.AutoID.NodeIds.csv text eol=lf
|
||||
|
||||
# Preserve the three release-pinned UAFX NodeSets and their upstream Git blob identities.
|
||||
src/Opc.Ua.Fx/Model/*.nodeset2.xml -text
|
||||
# The Data NodeSet contains trailing whitespace in a published Description.
|
||||
# TODO: remove this exception when a newer pinned upstream input removes it.
|
||||
src/Opc.Ua.Fx/Model/opc.ua.fx.data.nodeset2.xml whitespace=-blank-at-eol
|
||||
|
||||
# Denote all files that should not have line endings normalized, should not be
|
||||
# merged, and should not show in a textual diff.
|
||||
*.docm binary
|
||||
|
||||
@@ -221,6 +221,7 @@
|
||||
<Project Path="src/Opc.Ua.Extrusion.Client/Opc.Ua.Extrusion.Client.csproj" />
|
||||
<Project Path="src/Opc.Ua.Extrusion.Server/Opc.Ua.Extrusion.Server.csproj" />
|
||||
<Project Path="src/Opc.Ua.Extrusion/Opc.Ua.Extrusion.csproj" />
|
||||
<Project Path="src/Opc.Ua.Fx/Opc.Ua.Fx.csproj" />
|
||||
<Project Path="src/Opc.Ua.Gds.Client/Opc.Ua.Gds.Client.csproj" />
|
||||
<Project Path="src/Opc.Ua.Gds.Server/Opc.Ua.Gds.Server.csproj" />
|
||||
<Project Path="src/Opc.Ua.ISA95.Client/Opc.Ua.ISA95.Client.csproj" />
|
||||
@@ -417,6 +418,7 @@
|
||||
<Project Path="tests/Opc.Ua.Subscriptions.Tests/Opc.Ua.Subscriptions.Tests.csproj" />
|
||||
<Project Path="tests/Opc.Ua.Subscriptions.Classic.Tests/Opc.Ua.Subscriptions.Classic.Tests.csproj" />
|
||||
<Project Path="tests/Opc.Ua.Subscriptions.Durable.Tests/Opc.Ua.Subscriptions.Durable.Tests.csproj" />
|
||||
<Project Path="tests/Opc.Ua.Fx.Tests/Opc.Ua.Fx.Tests.csproj" />
|
||||
<Project Path="tests/Opc.Ua.Gds.Tests/Opc.Ua.Gds.Tests.csproj" />
|
||||
<Project Path="tests/Opc.Ua.ISA95.Tests/Opc.Ua.ISA95.Tests.csproj" />
|
||||
<Project Path="tests/Opc.Ua.Lens.Workflow.Tests/Opc.Ua.Lens.Workflow.Tests.csproj" />
|
||||
|
||||
@@ -441,6 +441,8 @@ substituting an encoding or ignoring incompatible settings. This is only the
|
||||
transport/config bridge: Part 81 ConnectionManager procedures,
|
||||
endpoint ownership, role claims, authorization, status, persistence and
|
||||
recovery remain unimplemented, so this API does not establish UAFX conformance.
|
||||
The [UAFX model package](UAFX.md) supplies the standard generated Part 81
|
||||
contracts independently of this transport bridge.
|
||||
|
||||
The PubSub adapter rejects `AllowQueueExpiry=true`. Dropping a key frame or
|
||||
metadata can invalidate later messages, so enabling expiry requires a
|
||||
@@ -701,7 +703,8 @@ or empty test run is not native acceptance.
|
||||
TCP evidence does not establish the complete transport assertion matrix.
|
||||
- **UAFX integration is partial.** The standard `pubsub-uasc` profile, strict
|
||||
local-policy validation and an opt-in shared-UASC PubSub transport bridge are
|
||||
implemented. Part 81 ConnectionManager/AutomationComponent configuration,
|
||||
implemented. The [UAFX model package](UAFX.md) supplies generated Part 81
|
||||
contracts. ConnectionManager/AutomationComponent configuration,
|
||||
endpoint ownership, authorization, status, persistence and recovery are not.
|
||||
This does not establish the Data Channel UAFX Binding Facet or a UAFX role
|
||||
claim. The binding selects ordered, confidential PubSub over UASC; it does
|
||||
|
||||
+4
-1
@@ -128,7 +128,7 @@ started exercise and the other two paths are optional here.
|
||||
encodings, and security.
|
||||
4. [Transport selection](PubSub.md#transports) — UDP, Ethernet, MQTT, and Kafka.
|
||||
5. [Brokerless PubSub over a SecureChannel](../samples/PubSub/ConsoleSecureChannelPubSub/README.md)
|
||||
(experimental) - run complete UADP or JSON NetworkMessages over a dedicated
|
||||
(experimental) - run complete UADP NetworkMessages over a dedicated
|
||||
flow, or share the connection with ordinary Read/Publish traffic.
|
||||
|
||||
## 3. Add data types and application services
|
||||
@@ -192,6 +192,9 @@ models that each model depends on.
|
||||
Integration first.
|
||||
- [ISA-95](ISA95.md) (OPC 10030 and OPC 10031-4) — the common
|
||||
model and job control.
|
||||
- [UAFX information models](UAFX.md) (OPC 10000-81) - generated Data,
|
||||
AutomationComponent and ConnectionManager contracts, without a lifecycle
|
||||
implementation or conformance claim.
|
||||
- [Positioning](Positioning.md) (OPC 10000-210 and OPC 10000-211; preview
|
||||
package) — relative frames and geographic positions.
|
||||
- [Machinery](Machinery.md) (OPC 40001-1, -2, -3, -4 and -101) —
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
# UAFX information models
|
||||
|
||||
`OPCFoundation.NetStandard.Opc.Ua.Fx` supplies generated OPC 10000-81 model
|
||||
contracts for the FX Data, AutomationComponent and ConnectionManager
|
||||
namespaces. It uses the existing Device Integration model and the stack's
|
||||
source generator; it adds no native runtime or third-party package.
|
||||
|
||||
## Register the models
|
||||
|
||||
Register dependencies before their consumers. The same registration order
|
||||
applies to encodeable factories, node-state factories and predefined nodes.
|
||||
The namespace table may already contain application namespaces.
|
||||
|
||||
```csharp
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.Di;
|
||||
using Opc.Ua.Fx.Data;
|
||||
using Opc.Ua.Fx.Ac;
|
||||
using Opc.Ua.Fx.Cm;
|
||||
|
||||
messageContext.Factory.Builder
|
||||
.AddOpcUaDi()
|
||||
.AddOpcUaFxData()
|
||||
.AddOpcUaFxAc()
|
||||
.AddOpcUaFxCm()
|
||||
.Commit();
|
||||
|
||||
nodes.AddOpcUaDi(systemContext)
|
||||
.AddOpcUaFxData(systemContext)
|
||||
.AddOpcUaFxAc(systemContext)
|
||||
.AddOpcUaFxCm(systemContext);
|
||||
```
|
||||
|
||||
The generated types include asynchronous `EstablishConnections` and
|
||||
`CloseConnections` Method handlers and client proxies, endpoint configuration
|
||||
and result structures, and PubSub communication-identifier reservation
|
||||
structures. `PubSubReserveCommunicationIds2DataType` and
|
||||
`PubSubReserveCommunicationIdsResult2DataType` preserve the standard
|
||||
`RequestTransportSpecificInfo` and `TransportSpecificInfo` fields.
|
||||
|
||||
## Source provenance
|
||||
|
||||
The three unmodified UAFX 1.00.04 NodeSets in `src/Opc.Ua.Fx/Model` come from
|
||||
[UA-Nodeset at dd178cf655ed3d74f1d085ef04e0e226a1d3ed9f](https://github.com/OPCFoundation/UA-Nodeset/tree/dd178cf655ed3d74f1d085ef04e0e226a1d3ed9f/UAFX).
|
||||
Their Git blob identifiers are:
|
||||
|
||||
| Input | Git blob |
|
||||
| --- | --- |
|
||||
| `opc.ua.fx.data.nodeset2.xml` | `12212843b7499c48059ca081963d349cd06d19a4` |
|
||||
| `opc.ua.fx.ac.nodeset2.xml` | `224276cd06fb200f7421d5370698fd187b199893` |
|
||||
| `opc.ua.fx.cm.nodeset2.xml` | `674ed1aa7ec9baad7119eba35ed1b87c10d2f0a8` |
|
||||
|
||||
Generated C# is produced during compilation and is not checked in.
|
||||
|
||||
## Implementation boundary
|
||||
|
||||
These are model contracts, not a ConnectionManager or AutomationComponent
|
||||
lifecycle implementation. Applications must implement authorization,
|
||||
verification, reservation, endpoint association, enablement, status,
|
||||
persistence and cleanup according to Part 81 and their selected Part 84
|
||||
profiles. Loading a model or completing a Method call is not evidence of
|
||||
valid PubSub DataSet reception or UAFX conformance.
|
||||
|
||||
The experimental [Data Channel binding](DataChannels.md) provides the
|
||||
`ConfigureUafx` transport-configuration bridge. It does not install Part 81
|
||||
configuration or grant a ConnectionManager authority over authenticated data
|
||||
peers.
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
||||
# OPC UA FX information models
|
||||
|
||||
`OPCFoundation.NetStandard.Opc.Ua.Fx` provides source-generated Part 81 Data,
|
||||
AutomationComponent and ConnectionManager types, identifiers, node states and
|
||||
asynchronous client proxies. It reuses the stack's Device Integration model.
|
||||
|
||||
The published UAFX 1.00.04 NodeSets are pinned to
|
||||
`OPCFoundation/UA-Nodeset@dd178cf655ed3d74f1d085ef04e0e226a1d3ed9f`.
|
||||
The XML files are unmodified upstream inputs; generated C# is not checked in.
|
||||
|
||||
This package supplies model contracts, not a ConnectionManager implementation
|
||||
or a UAFX conformance claim. Application authorization, commissioning,
|
||||
identifier reservation, endpoint lifecycle and network-service support require
|
||||
an implementation of the corresponding Part 81 and selected Part 84 behavior.
|
||||
|
||||
See [UAFX information models](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/docs/UAFX.md)
|
||||
for registration examples and the pinned input blob identifiers.
|
||||
@@ -0,0 +1,46 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<AssemblyName>$(AssemblyPrefix).Fx</AssemblyName>
|
||||
<TargetFrameworks>$(LibTargetFrameworks)</TargetFrameworks>
|
||||
<PackageId>$(PackagePrefix).Opc.Ua.Fx</PackageId>
|
||||
<RootNamespace>Opc.Ua.Fx</RootNamespace>
|
||||
<Description>OPC UA FX Part 81 Data, AutomationComponent and ConnectionManager information models.</Description>
|
||||
<IsPackable>true</IsPackable>
|
||||
<PackageReadmeFile>NugetREADME.md</PackageReadmeFile>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
<IsAotCompatible
|
||||
Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net10.0'))">true</IsAotCompatible>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<PackageId>$(PackageId).Debug</PackageId>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Opc.Ua.Di\Opc.Ua.Di.csproj" />
|
||||
<ProjectReference Include="..\..\tools\Opc.Ua.SourceGeneration\Opc.Ua.SourceGeneration.csproj"
|
||||
OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
|
||||
</ItemGroup>
|
||||
<Import Project="..\..\tools\Opc.Ua.SourceGeneration\OPCFoundation.Opc.Ua.SourceGeneration.props" />
|
||||
<PropertyGroup>
|
||||
<ModelSourceGeneratorVersion>v105</ModelSourceGeneratorVersion>
|
||||
<ModelSourceGeneratorOmitFluentApi>true</ModelSourceGeneratorOmitFluentApi>
|
||||
<ModelSourceGeneratorUseAllowSubtypes>true</ModelSourceGeneratorUseAllowSubtypes>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="Model\opc.ua.fx.data.nodeset2.xml">
|
||||
<ModelSourceGeneratorModelUri>http://opcfoundation.org/UA/FX/Data/</ModelSourceGeneratorModelUri>
|
||||
<ModelSourceGeneratorName>FxData</ModelSourceGeneratorName>
|
||||
<ModelSourceGeneratorPrefix>Opc.Ua.Fx.Data</ModelSourceGeneratorPrefix>
|
||||
</AdditionalFiles>
|
||||
<AdditionalFiles Include="Model\opc.ua.fx.ac.nodeset2.xml">
|
||||
<ModelSourceGeneratorModelUri>http://opcfoundation.org/UA/FX/AC/</ModelSourceGeneratorModelUri>
|
||||
<ModelSourceGeneratorName>FxAc</ModelSourceGeneratorName>
|
||||
<ModelSourceGeneratorPrefix>Opc.Ua.Fx.Ac</ModelSourceGeneratorPrefix>
|
||||
</AdditionalFiles>
|
||||
<AdditionalFiles Include="Model\opc.ua.fx.cm.nodeset2.xml">
|
||||
<ModelSourceGeneratorModelUri>http://opcfoundation.org/UA/FX/CM/</ModelSourceGeneratorModelUri>
|
||||
<ModelSourceGeneratorName>FxCm</ModelSourceGeneratorName>
|
||||
<ModelSourceGeneratorPrefix>Opc.Ua.Fx.Cm</ModelSourceGeneratorPrefix>
|
||||
</AdditionalFiles>
|
||||
<None Include="NugetREADME.md" Pack="true" PackagePath="\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,97 @@
|
||||
/* ========================================================================
|
||||
* Copyright (c) 2005-2026 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 Microsoft.Extensions.DependencyInjection;
|
||||
using Opc.Ua.Di;
|
||||
using Opc.Ua.Fx.Ac;
|
||||
using Opc.Ua.Fx.Cm;
|
||||
using Opc.Ua.Fx.Data;
|
||||
|
||||
namespace Opc.Ua.Aot.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Exercises generated UAFX model activation and binary decoding after native trimming.
|
||||
/// </summary>
|
||||
public sealed class FxAotTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Published models and derived reservation results require no reflection-based activation.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task GeneratedFxModelsAndReservationResultsAreAotSafeAsync()
|
||||
{
|
||||
var services = new ServiceCollection();
|
||||
services.AddOpcUa();
|
||||
using ServiceProvider provider = services.BuildServiceProvider();
|
||||
ITelemetryContext telemetry = provider.GetRequiredService<ITelemetryContext>();
|
||||
ServiceMessageContext message = ServiceMessageContext.Create(telemetry);
|
||||
message.NamespaceUris.GetIndexOrAppend("urn:uafx-aot:application");
|
||||
message.NamespaceUris.GetIndexOrAppend(Fx.Cm.Namespaces.FxCm);
|
||||
message.NamespaceUris.GetIndexOrAppend(Fx.Ac.Namespaces.FxAc);
|
||||
message.NamespaceUris.GetIndexOrAppend(Fx.Data.Namespaces.FxData);
|
||||
message.NamespaceUris.GetIndexOrAppend(Di.Namespaces.OpcUaDi);
|
||||
message.Factory.Builder.AddOpcUaDi().AddOpcUaFxData().AddOpcUaFxAc().AddOpcUaFxCm().Commit();
|
||||
var context = new SystemContext(telemetry)
|
||||
{
|
||||
NamespaceUris = message.NamespaceUris,
|
||||
EncodeableFactory = message.Factory
|
||||
};
|
||||
var nodes = new NodeStateCollection();
|
||||
nodes.AddOpcUaDi(context).AddOpcUaFxData(context).AddOpcUaFxAc(context).AddOpcUaFxCm(context);
|
||||
await Assert.That(nodes.Any(node => node.NodeId ==
|
||||
NodeId.Create(2, Fx.Ac.Namespaces.FxAc, context.NamespaceUris))).IsTrue();
|
||||
await Assert.That(nodes.Any(node => node.NodeId ==
|
||||
NodeId.Create(1002, Fx.Cm.Namespaces.FxCm, context.NamespaceUris))).IsTrue();
|
||||
|
||||
var result = new PubSubReserveCommunicationIdsResult2DataType
|
||||
{
|
||||
DefaultPublisherId = new Variant((uint)23),
|
||||
WriterGroupIds = [4],
|
||||
DataSetWriterIds = [5],
|
||||
TransportSpecificInfo = new Variant("opc.tcp://localhost:4840/uafx")
|
||||
};
|
||||
using var stream = new MemoryStream();
|
||||
using (var encoder = new BinaryEncoder(stream, message, true))
|
||||
{
|
||||
encoder.WriteExtensionObject("Result", new ExtensionObject(result));
|
||||
}
|
||||
stream.Position = 0;
|
||||
using var decoder = new BinaryDecoder(stream, message, true);
|
||||
ExtensionObject decodedObject = decoder.ReadExtensionObject("Result");
|
||||
bool decoded = decodedObject.TryGetValue(out PubSubReserveCommunicationIdsResult2DataType? value);
|
||||
await Assert.That(decoded).IsTrue();
|
||||
await Assert.That(value!.DefaultPublisherId).IsEqualTo(new Variant((uint)23));
|
||||
await Assert.That(value.WriterGroupIds[0]).IsEqualTo((ushort)4);
|
||||
await Assert.That(value.DataSetWriterIds[0]).IsEqualTo((ushort)5);
|
||||
await Assert.That(value.TransportSpecificInfo)
|
||||
.IsEqualTo(new Variant("opc.tcp://localhost:4840/uafx"));
|
||||
await Assert.That(stream.Position).IsEqualTo(stream.Length);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,7 @@
|
||||
<ProjectReference Include="..\..\src\Opc.Ua.AI.Client\Opc.Ua.AI.Client.csproj" />
|
||||
<ProjectReference Include="..\..\src\Opc.Ua.Core\Opc.Ua.Core.csproj" />
|
||||
<ProjectReference Include="..\..\src\Opc.Ua.Core.Schema\Opc.Ua.Core.Schema.csproj" />
|
||||
<ProjectReference Include="..\..\src\Opc.Ua.Fx\Opc.Ua.Fx.csproj" />
|
||||
<ProjectReference Include="..\..\src\Opc.Ua.Bindings.Https\Opc.Ua.Bindings.Https.csproj" />
|
||||
<ProjectReference Include="..\..\src\Opc.Ua.Client\Opc.Ua.Client.csproj" />
|
||||
<ProjectReference Include="..\..\src\Opc.Ua.Client.ComplexTypes\Opc.Ua.Client.ComplexTypes.csproj" />
|
||||
|
||||
@@ -0,0 +1,278 @@
|
||||
/* ========================================================================
|
||||
* Copyright (c) 2005-2026 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.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Security.Cryptography;
|
||||
using NUnit.Framework;
|
||||
using Opc.Ua.Di;
|
||||
using Opc.Ua.Fx.Ac;
|
||||
using Opc.Ua.Fx.Cm;
|
||||
using Opc.Ua.Fx.Data;
|
||||
using Opc.Ua.Tests;
|
||||
|
||||
namespace Opc.Ua.Fx.Tests
|
||||
{
|
||||
/// <summary>
|
||||
/// Exercises the generated Part 81 model contracts, independently of a connection lifecycle implementation.
|
||||
/// </summary>
|
||||
[TestFixture]
|
||||
[Category("UAFX")]
|
||||
public sealed class FxModelTests
|
||||
{
|
||||
/// <summary>
|
||||
/// Model inputs remain byte-identical to the pinned published files on every supported host.
|
||||
/// </summary>
|
||||
[TestCase("data", "80FFA5EAE396FB2ED6EA7FC8647013DC6BDFBD939B12249A3B711542E4881339")]
|
||||
[TestCase("ac", "35586E374EEA321291AEE9E46731D3CA547CC5A73626F99260D20254DA60B941")]
|
||||
[TestCase("cm", "6ABFB436BFC07051D7C972DC36A4D8744A760CD1B94A72D37D621A4C272F7929")]
|
||||
public void ModelInputsRemainByteIdentical(string model, string expectedSha256)
|
||||
{
|
||||
string path = Path.Combine(
|
||||
TestContext.CurrentContext.TestDirectory, "Model", $"opc.ua.fx.{model}.nodeset2.xml");
|
||||
using var stream = File.OpenRead(path);
|
||||
#if NET8_0_OR_GREATER
|
||||
string actual = CoreUtils.ToHexString(SHA256.HashData(stream));
|
||||
#else
|
||||
using var sha256 = SHA256.Create();
|
||||
string actual = CoreUtils.ToHexString(sha256.ComputeHash(stream));
|
||||
#endif
|
||||
Assert.That(actual, Is.EqualTo(expectedSha256));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Published declarations retain their NodeIds even when the caller changes namespace indexes.
|
||||
/// </summary>
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void PublishedModelsMaterializeWithRemappedNamespaces(bool reverseNamespaces)
|
||||
{
|
||||
ServiceMessageContext message = CreateContext(reverseNamespaces);
|
||||
var context = new SystemContext(NUnitTelemetryContext.Create())
|
||||
{
|
||||
NamespaceUris = message.NamespaceUris,
|
||||
EncodeableFactory = message.Factory
|
||||
};
|
||||
var nodes = new NodeStateCollection();
|
||||
nodes.AddOpcUaDi(context).AddOpcUaFxData(context).AddOpcUaFxAc(context).AddOpcUaFxCm(context);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(nodes.Any(node => node.NodeId ==
|
||||
NodeId.Create(2, Ac.Namespaces.FxAc, context.NamespaceUris)), Is.True,
|
||||
"AutomationComponentType");
|
||||
Assert.That(nodes.Any(node => node.NodeId ==
|
||||
NodeId.Create(1002, Ac.Namespaces.FxAc, context.NamespaceUris)), Is.True,
|
||||
"ConnectionEndpointType");
|
||||
Assert.That(nodes.Any(node => node.NodeId ==
|
||||
NodeId.Create(1002, Cm.Namespaces.FxCm, context.NamespaceUris)), Is.True,
|
||||
"ConnectionManagerType");
|
||||
Assert.That(nodes.Any(node => node.NodeId ==
|
||||
NodeId.Create(3005, Data.Namespaces.FxData, context.NamespaceUris)), Is.True,
|
||||
"PubSubReserveCommunicationIds2DataType");
|
||||
Assert.That(nodes.Any(node => node.NodeId ==
|
||||
NodeId.Create(3013, Data.Namespaces.FxData, context.NamespaceUris)), Is.True,
|
||||
"PubSubReserveCommunicationIdsResult2DataType");
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Standard lifecycle Methods expose their published arguments rather than Data Channel-specific schemas.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public void LifecycleMethodsRetainPublishedArgumentContracts()
|
||||
{
|
||||
ServiceMessageContext message = CreateContext(true);
|
||||
var context = new SystemContext(NUnitTelemetryContext.Create())
|
||||
{
|
||||
NamespaceUris = message.NamespaceUris,
|
||||
EncodeableFactory = message.Factory
|
||||
};
|
||||
var nodes = new NodeStateCollection();
|
||||
nodes.AddOpcUaFxAc(context);
|
||||
NodeState automationComponent = nodes.Single(node => node.NodeId ==
|
||||
NodeId.Create(2, Ac.Namespaces.FxAc, context.NamespaceUris));
|
||||
var children = new List<BaseInstanceState>();
|
||||
automationComponent.GetChildren(context, children);
|
||||
var establish = (EstablishConnectionsMethodState)children.Single(node => node.NodeId ==
|
||||
NodeId.Create(292, Ac.Namespaces.FxAc, context.NamespaceUris));
|
||||
var close = (CloseConnectionsMethodState)children.Single(node => node.NodeId ==
|
||||
NodeId.Create(293, Ac.Namespaces.FxAc, context.NamespaceUris));
|
||||
Argument[] inputs = establish.InputArguments!.Value.ToArray()!;
|
||||
Argument[] outputs = establish.OutputArguments!.Value.ToArray()!;
|
||||
string[] inputNames =
|
||||
[
|
||||
"CommandMask", "AssetVerifications", "ConnectionEndpointConfigurations",
|
||||
"ReserveCommunicationIds", "CommunicationConfigurations"
|
||||
];
|
||||
string[] outputNames =
|
||||
[
|
||||
"AssetVerificationResults", "ConnectionEndpointConfigurationResults",
|
||||
"ReserveCommunicationIdsResults", "CommunicationConfigurationResults"
|
||||
];
|
||||
string[] closeInputNames = ["ConnectionEndpoints", "Remove"];
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(inputs.Select(argument => argument.Name), Is.EqualTo(inputNames));
|
||||
Assert.That(inputs.Select(argument => argument.ValueRank), Is.EqualTo(new[] { -1, 1, 1, 1, 1 }));
|
||||
Assert.That(inputs.Select(argument => argument.DataType), Is.EqualTo(
|
||||
new uint[] { 1024, 1048, 1044, 3017, 1046 }
|
||||
.Select(id => NodeId.Create(id, Data.Namespaces.FxData, context.NamespaceUris))));
|
||||
Assert.That(outputs.Select(argument => argument.Name), Is.EqualTo(outputNames));
|
||||
Assert.That(outputs.Select(argument => argument.ValueRank), Is.All.EqualTo(ValueRanks.OneDimension));
|
||||
Assert.That(outputs.Select(argument => argument.DataType), Is.EqualTo(
|
||||
new uint[] { 1038, 3008, 3019, 1033 }
|
||||
.Select(id => NodeId.Create(id, Data.Namespaces.FxData, context.NamespaceUris))));
|
||||
Assert.That(close.InputArguments!.Value.ToArray()!.Select(argument => argument.Name),
|
||||
Is.EqualTo(closeInputNames));
|
||||
Assert.That(close.InputArguments.Value.ToArray()!.Select(argument => argument.DataType),
|
||||
Is.EqualTo(new[] { DataTypeIds.NodeId, DataTypeIds.Boolean }));
|
||||
Assert.That(close.InputArguments.Value.ToArray()!.Select(argument => argument.ValueRank),
|
||||
Is.EqualTo(new[] { ValueRanks.OneDimension, ValueRanks.Scalar }));
|
||||
Assert.That(close.OutputArguments!.Value.Count, Is.EqualTo(1));
|
||||
Assert.That(close.OutputArguments!.Value[0].Name, Is.EqualTo("Results"));
|
||||
Assert.That(close.OutputArguments.Value[0].DataType, Is.EqualTo(DataTypeIds.StatusCode));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reservation contracts preserve standard identifier counts and transport-specific request and result fields.
|
||||
/// </summary>
|
||||
[TestCase(false)]
|
||||
[TestCase(true)]
|
||||
public void ReservationContractsRoundTripWithoutReplacingPubSubIdentifiers(bool reverseNamespaces)
|
||||
{
|
||||
ServiceMessageContext context = CreateContext(reverseNamespaces);
|
||||
var request = new PubSubReserveCommunicationIds2DataType
|
||||
{
|
||||
TransportProfileUri = "http://opcfoundation.org/UA-Profile/Transport/pubsub-uasc",
|
||||
NumReqWriterGroupIds = 2,
|
||||
NumReqDataSetWriterIds = 3,
|
||||
RequestTransportSpecificInfo = true
|
||||
};
|
||||
var result = new PubSubReserveCommunicationIdsResult2DataType
|
||||
{
|
||||
DefaultPublisherId = new Variant((uint)17),
|
||||
WriterGroupIds = [4, 5],
|
||||
DataSetWriterIds = [10, 11, 12],
|
||||
TransportSpecificInfo = new Variant("opc.tcp://localhost:4840/uafx")
|
||||
};
|
||||
|
||||
using var stream = new MemoryStream();
|
||||
using (var encoder = new BinaryEncoder(stream, context, true))
|
||||
{
|
||||
encoder.WriteExtensionObject("Request", new ExtensionObject(request));
|
||||
encoder.WriteExtensionObject("Result", new ExtensionObject(result));
|
||||
}
|
||||
stream.Position = 0;
|
||||
using var decoder = new BinaryDecoder(stream, context, true);
|
||||
ExtensionObject requestObject = decoder.ReadExtensionObject("Request");
|
||||
ExtensionObject resultObject = decoder.ReadExtensionObject("Result");
|
||||
Assert.That(requestObject.TryGetValue(out PubSubReserveCommunicationIds2DataType? decodedRequest),
|
||||
Is.True);
|
||||
Assert.That(resultObject.TryGetValue(out PubSubReserveCommunicationIdsResult2DataType? decodedResult),
|
||||
Is.True);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decodedRequest!.TransportProfileUri, Is.EqualTo(request.TransportProfileUri));
|
||||
Assert.That(decodedRequest.NumReqWriterGroupIds, Is.EqualTo(2));
|
||||
Assert.That(decodedRequest.NumReqDataSetWriterIds, Is.EqualTo(3));
|
||||
Assert.That(decodedRequest.RequestTransportSpecificInfo, Is.True);
|
||||
Assert.That(decodedResult!.DefaultPublisherId, Is.EqualTo(new Variant((uint)17)));
|
||||
Assert.That(decodedResult.WriterGroupIds.ToArray(), Is.EqualTo(new ushort[] { 4, 5 }));
|
||||
Assert.That(decodedResult.DataSetWriterIds.ToArray(), Is.EqualTo(new ushort[] { 10, 11, 12 }));
|
||||
Assert.That(decodedResult.TransportSpecificInfo,
|
||||
Is.EqualTo(new Variant("opc.tcp://localhost:4840/uafx")));
|
||||
Assert.That(stream.Position, Is.EqualTo(stream.Length));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generated structures retain persistence, cleanup and variable associations without runtime admissions.
|
||||
/// </summary>
|
||||
[TestCase(false, 0)]
|
||||
[TestCase(true, 1234.5)]
|
||||
public void EndpointParametersPreservePersistenceAndVariableAssociations(bool persistent, double timeout)
|
||||
{
|
||||
ServiceMessageContext context = CreateContext(false);
|
||||
var parameters = new PubSubConnectionEndpointParameterDataType
|
||||
{
|
||||
Name = "Endpoint",
|
||||
ConnectionEndpointTypeId = NodeId.Create(1002, Ac.Namespaces.FxAc, context.NamespaceUris),
|
||||
InputVariableIds = [new NodeId("input", 1)],
|
||||
OutputVariableIds = [new NodeId("output", 1)],
|
||||
IsPersistent = persistent,
|
||||
CleanupTimeout = timeout
|
||||
};
|
||||
using var stream = new MemoryStream();
|
||||
using (var encoder = new BinaryEncoder(stream, context, true))
|
||||
{
|
||||
parameters.Encode(encoder);
|
||||
}
|
||||
stream.Position = 0;
|
||||
using var decoder = new BinaryDecoder(stream, context, true);
|
||||
var decoded = new PubSubConnectionEndpointParameterDataType();
|
||||
decoded.Decode(decoder);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.That(decoded.Name, Is.EqualTo("Endpoint"));
|
||||
Assert.That(decoded.ConnectionEndpointTypeId, Is.EqualTo(parameters.ConnectionEndpointTypeId));
|
||||
Assert.That(decoded.InputVariableIds.ToArray(), Is.EqualTo(parameters.InputVariableIds.ToArray()));
|
||||
Assert.That(decoded.OutputVariableIds.ToArray(), Is.EqualTo(parameters.OutputVariableIds.ToArray()));
|
||||
Assert.That(decoded.IsPersistent, Is.EqualTo(persistent));
|
||||
Assert.That(decoded.CleanupTimeout, Is.EqualTo(timeout));
|
||||
Assert.That(stream.Position, Is.EqualTo(stream.Length));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an isolated model-aware context with noncanonical namespace ordering.
|
||||
/// </summary>
|
||||
private static ServiceMessageContext CreateContext(bool reverseNamespaces)
|
||||
{
|
||||
ServiceMessageContext context = ServiceMessageContext.Create(NUnitTelemetryContext.Create());
|
||||
context.NamespaceUris.GetIndexOrAppend("urn:uafx-tests:application");
|
||||
string[] namespaces =
|
||||
[
|
||||
Di.Namespaces.OpcUaDi,
|
||||
Data.Namespaces.FxData,
|
||||
Ac.Namespaces.FxAc,
|
||||
Cm.Namespaces.FxCm
|
||||
];
|
||||
foreach (string uri in reverseNamespaces ? namespaces.Reverse() : namespaces)
|
||||
{
|
||||
context.NamespaceUris.GetIndexOrAppend(uri);
|
||||
}
|
||||
context.Factory.Builder.AddOpcUaDi().AddOpcUaFxData().AddOpcUaFxAc().AddOpcUaFxCm().Commit();
|
||||
return context;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFrameworks>$(TestsTargetFrameworks)</TargetFrameworks>
|
||||
<RootNamespace>Opc.Ua.Fx.Tests</RootNamespace>
|
||||
<AssemblyName>Opc.Ua.Fx.Tests</AssemblyName>
|
||||
<GenerateProgramFile>false</GenerateProgramFile>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" />
|
||||
<PackageReference Include="NUnit" />
|
||||
<PackageReference Include="NUnit.Analyzers">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="NUnit3TestAdapter">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="coverlet.collector">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="..\Common\Main.cs" />
|
||||
<ProjectReference Include="..\..\src\Opc.Ua.Fx\Opc.Ua.Fx.csproj" />
|
||||
<ProjectReference Include="..\Opc.Ua.Test.Common\Opc.Ua.Test.Common.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="..\..\src\Opc.Ua.Fx\Model\*.nodeset2.xml"
|
||||
Link="Model\%(Filename)%(Extension)" CopyToOutputDirectory="PreserveNewest" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user