diff --git a/.gitattributes b/.gitattributes
index eda18c728a..e56fe8f67b 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -28,3 +28,7 @@
# OPC UA
*.uanodes binary
+
+# Preserve official NodeSet artifacts byte-for-byte.
+src/Opc.Ua.Positioning/Model/*.csv binary
+src/Opc.Ua.Positioning/Model/*.xml binary
diff --git a/Directory.Build.targets b/Directory.Build.targets
index c0d9129dc0..ff434bfa78 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -4,13 +4,25 @@
sets `true` in its main
`PropertyGroup`, this Directory.Build.targets makes the project a
no-op build (empty Library, no compile items, no references) for
- `dotnet build โฆ /p:CustomTestTarget=` invocations where
- `` is one of `net472` / `net48` / `netstandard2.0` /
- `netstandard2.1`. Use this for projects that are `.NET 8+` only
+ `dotnet build โฆ /p:CustomTestTarget=` invocations that the
+ project cannot take part in. That is the case for
+ `` in `net472` / `net48` / `netstandard2.0` / `netstandard2.1`,
+ and for any project that pins a single `TargetFramework` different
+ from ``. Use this for projects that are `.NET 8+` only
(e.g. `PublishAot=true`, `ModelContextProtocol` server, TUnit AOT
tests) and therefore can't participate in a `UA.slnx` build pinned
to a legacy TFM via `CustomTestTarget`.
+ The single-`TargetFramework` case matters because `CustomTestTarget`
+ only redirects the shared TFM variables in `targets.props`; a project
+ that hardcodes, say, `net10.0` keeps compiling against its own
+ reference assemblies while the rest of the stack is built for
+ ``. Mixing the two surfaces as `CS0433` on every BCL type the
+ stack polyfills for older frameworks (`System.Threading.Lock` is
+ defined by `Opc.Ua.Types` on `net8.0` and by `System.Runtime` on
+ `net9.0+`), which is a property of the pinned build rather than of
+ the project's own sources.
+
The project still loads in the `.slnx` graph so the matrix prep
script discovers it, but `Compile` / `ProjectReference` /
`PackageReference` / `AdditionalFiles` / `None` / `Content` are all
@@ -32,6 +44,11 @@
<_RestrictedToLegacyTfm
Condition="'$(RestrictForLegacyTfm)' == 'true' and ('$(CustomTestTarget)' == 'net48' or '$(CustomTestTarget)' == 'net472' or '$(CustomTestTarget)' == 'netstandard2.0' or '$(CustomTestTarget)' == 'netstandard2.1')">true
+
+ <_RestrictedToLegacyTfm
+ Condition="'$(RestrictForLegacyTfm)' == 'true' and '$(CustomTestTarget)' != '' and '$(TargetFrameworks)' == '' and '$(TargetFramework)' != '' and '$(CustomTestTarget)' != '$(TargetFramework)'">true
Library
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 254bbc830d..2df0eaa5d3 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -128,6 +128,18 @@
+
+
+
+
+
+
+
+
+
+
+ Cell.usda
+ robot.usda
+ tool.usda
+
+
diff --git a/samples/MinimalRobotServer/MobileRobotPositionOptions.cs b/samples/MinimalRobotServer/MobileRobotPositionOptions.cs
new file mode 100644
index 0000000000..91e2114e37
--- /dev/null
+++ b/samples/MinimalRobotServer/MobileRobotPositionOptions.cs
@@ -0,0 +1,162 @@
+/* ========================================================================
+ * 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/
+ * ======================================================================*/
+
+namespace Robotics
+{
+ ///
+ /// Supported deterministic motion styles for a robot mount.
+ ///
+ public enum RobotMotionMode
+ {
+ ///
+ /// The robot stays at its origin and keeps a fixed heading.
+ ///
+ Fixed,
+
+ ///
+ /// The robot traces a lemniscate around its origin.
+ ///
+ FigureEight,
+
+ ///
+ /// The robot traces a circle around its origin.
+ ///
+ Circle,
+
+ ///
+ /// The robot travels back and forth along the X axis.
+ ///
+ Shuttle
+ }
+
+ ///
+ /// Per-robot motion configuration.
+ ///
+ public sealed class RobotMotionOptions
+ {
+ ///
+ /// The motion style the robot follows.
+ ///
+ public RobotMotionMode Mode { get; set; } = RobotMotionMode.FigureEight;
+
+ ///
+ /// The X coordinate the path is centred on, in metres.
+ ///
+ public double OriginX { get; set; }
+
+ ///
+ /// The Y coordinate the path is centred on, in metres.
+ ///
+ public double OriginY { get; set; }
+
+ ///
+ /// The Z coordinate the path is centred on, in metres.
+ ///
+ public double OriginZ { get; set; }
+
+ ///
+ /// Half the width of the
+ /// path, in metres.
+ ///
+ public double AmplitudeX { get; set; } = 0.6;
+
+ ///
+ /// Half the height of the
+ /// path, in metres.
+ ///
+ public double AmplitudeY { get; set; } = 0.35;
+
+ ///
+ /// The radius of the path, in
+ /// metres.
+ ///
+ public double Radius { get; set; } = 0.5;
+
+ ///
+ /// The travel distance of the
+ /// path, in metres.
+ ///
+ public double ShuttleDistance { get; set; } = 0.8;
+
+ ///
+ /// The time the robot takes to complete one full path, in seconds.
+ ///
+ public double PeriodSeconds { get; set; } = 20.0;
+
+ ///
+ /// The offset into the path the robot starts from, in seconds. Lets
+ /// two robots sharing a path stay apart.
+ ///
+ public double PhaseSeconds { get; set; }
+
+ ///
+ /// The heading held when is
+ /// false, in degrees.
+ ///
+ public double FixedHeadingDegrees { get; set; }
+
+ ///
+ /// Whether the heading is derived from the direction of travel rather
+ /// than from .
+ ///
+ public bool HeadingFollowsPath { get; set; } = true;
+ }
+
+ ///
+ /// Position simulation configuration for both mobile robots.
+ ///
+ public sealed class MobileRobotPositionOptions
+ {
+ ///
+ /// Creates the options with the two robots placed either side of the
+ /// origin and offset in phase so they do not overlap.
+ ///
+ public MobileRobotPositionOptions()
+ {
+ R1.OriginX = -1.2;
+ R2.OriginX = 1.2;
+ R2.PhaseSeconds = 10.0;
+ }
+
+ ///
+ /// Motion configuration for the first robot.
+ ///
+ public RobotMotionOptions R1 { get; set; } = new();
+
+ ///
+ /// Motion configuration for the second robot.
+ ///
+ public RobotMotionOptions R2 { get; set; } = new();
+
+ ///
+ /// How often a new position is published, in milliseconds.
+ ///
+ public int UpdateIntervalMilliseconds { get; set; } = 200;
+ }
+}
diff --git a/samples/MinimalRobotServer/MobileRobotPositionProvider.cs b/samples/MinimalRobotServer/MobileRobotPositionProvider.cs
new file mode 100644
index 0000000000..454f0ef6d3
--- /dev/null
+++ b/samples/MinimalRobotServer/MobileRobotPositionProvider.cs
@@ -0,0 +1,225 @@
+/* ========================================================================
+ * 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;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Options;
+using Opc.Ua;
+using Opc.Ua.Gpos;
+using Opc.Ua.Positioning;
+using Opc.Ua.Positioning.Server;
+
+namespace Robotics
+{
+ ///
+ /// Deterministic sample provider that publishes a global location per robot.
+ ///
+ public sealed class MobileRobotPositionProvider : IGeoLocationProvider
+ {
+ ///
+ /// The EPSG code of WGS84, which the cell's GlobalLocation Variables
+ /// are configured for.
+ ///
+ private const uint WGS84EpsgCode = 4326;
+
+ private readonly MobileRobotPositionOptions m_options;
+ private readonly TimeProvider m_timeProvider;
+ private readonly DateTimeOffset m_startedAt;
+
+ public MobileRobotPositionProvider(
+ IOptions options,
+ RobotPositioningScenario scenario)
+ : this(options.Value, scenario, TimeProvider.System)
+ {
+ }
+
+ public MobileRobotPositionProvider(
+ MobileRobotPositionOptions options,
+ RobotPositioningScenario scenario,
+ TimeProvider timeProvider)
+ {
+ m_options = options ?? throw new ArgumentNullException(nameof(options));
+ Scenario = scenario ?? throw new ArgumentNullException(nameof(scenario));
+ m_timeProvider = timeProvider ??
+ throw new ArgumentNullException(nameof(timeProvider));
+ m_startedAt = m_timeProvider.GetUtcNow();
+ }
+
+ public RobotPositioningScenario Scenario { get; }
+
+ ///
+ public bool SupportsPush => true;
+
+ ///
+ public ValueTask ReadAsync(
+ string sourceId,
+ CancellationToken cancellationToken = default)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+ TimeSpan elapsed = m_timeProvider.GetUtcNow() - m_startedAt;
+ return new ValueTask(
+ CreateSample(sourceId, elapsed));
+ }
+
+ ///
+ public async IAsyncEnumerable WatchAsync(
+ string sourceId,
+ [EnumeratorCancellation] CancellationToken cancellationToken = default)
+ {
+ RobotMotionOptions options = GetOptions(sourceId);
+ if (options.Mode == RobotMotionMode.Fixed)
+ {
+ yield break;
+ }
+
+ var interval = TimeSpan.FromMilliseconds(
+ Math.Max(20, m_options.UpdateIntervalMilliseconds));
+ while (!cancellationToken.IsCancellationRequested)
+ {
+#if NET8_0_OR_GREATER
+ await Task.Delay(
+ interval,
+ m_timeProvider,
+ cancellationToken).ConfigureAwait(false);
+#else
+ await Task.Delay(
+ interval,
+ cancellationToken).ConfigureAwait(false);
+#endif
+ TimeSpan elapsed = m_timeProvider.GetUtcNow() - m_startedAt;
+ yield return CreateSample(sourceId, elapsed);
+ }
+ }
+
+ internal ThreeDFrame EvaluateLocalFrame(
+ string sourceId,
+ TimeSpan elapsed)
+ {
+ RobotMotionOptions options = GetOptions(sourceId);
+ double period = Math.Max(0.1, options.PeriodSeconds);
+ double theta = 2.0 *
+ Math.PI *
+ (elapsed.TotalSeconds + options.PhaseSeconds) /
+ period;
+ double x = options.OriginX;
+ double y = options.OriginY;
+ double dx = 1.0;
+ double dy = 0.0;
+
+ switch (options.Mode)
+ {
+ case RobotMotionMode.FigureEight:
+ x += options.AmplitudeX * Math.Sin(theta);
+ y += options.AmplitudeY * Math.Sin(2.0 * theta);
+ dx = options.AmplitudeX * Math.Cos(theta);
+ dy = 2.0 * options.AmplitudeY * Math.Cos(2.0 * theta);
+ break;
+ case RobotMotionMode.Circle:
+ x += options.Radius * Math.Cos(theta);
+ y += options.Radius * Math.Sin(theta);
+ dx = -options.Radius * Math.Sin(theta);
+ dy = options.Radius * Math.Cos(theta);
+ break;
+ case RobotMotionMode.Shuttle:
+ x += options.ShuttleDistance * Math.Sin(theta);
+ dx = options.ShuttleDistance * Math.Cos(theta);
+ break;
+ case RobotMotionMode.Fixed:
+ break;
+ default:
+ throw new InvalidOperationException(
+ $"Unsupported robot motion mode '{options.Mode}'.");
+ }
+
+ double heading = options.HeadingFollowsPath &&
+ options.Mode != RobotMotionMode.Fixed
+ ? Math.Atan2(dy, dx) * (180.0 / Math.PI)
+ : options.FixedHeadingDegrees;
+ return new ThreeDFrame
+ {
+ CartesianCoordinates = new ThreeDCartesianCoordinates
+ {
+ X = x,
+ Y = y,
+ Z = options.OriginZ
+ },
+ Orientation = new ThreeDOrientation
+ {
+ A = 0.0,
+ B = 0.0,
+ C = heading
+ }
+ };
+ }
+
+ private GeoLocationSample CreateSample(
+ string sourceId,
+ TimeSpan elapsed)
+ {
+ ThreeDFrame local = EvaluateLocalFrame(sourceId, elapsed);
+ S3DGeographicCoordinateDataType geographic = Scenario.Fit.LocalToGlobal(
+ local.CartesianCoordinates,
+ AngleUnit.Degrees);
+ bool hasElevation = (geographic.EncodingMask &
+ (uint)S3DGeographicCoordinateDataTypeFields.Elevation) != 0;
+ var position = new GeoPosition(
+ geographic.Latitude,
+ geographic.Longitude,
+ hasElevation ? geographic.Elevation : null,
+ Accuracy: 0.05,
+ EpsgCode: WGS84EpsgCode);
+ var orientation = new GeoOrientation(
+ local.Orientation.A,
+ local.Orientation.B,
+ local.Orientation.C);
+ return new GeoLocationSample(
+ position,
+ orientation,
+ default,
+ StatusCodes.Good,
+ new DateTimeUtc(m_timeProvider.GetUtcNow()),
+ sourceId);
+ }
+
+ private RobotMotionOptions GetOptions(string sourceId)
+ {
+ return sourceId switch
+ {
+ "R1" => m_options.R1,
+ "R2" => m_options.R2,
+ _ => throw new ArgumentException(
+ $"Unknown robot positioning source '{sourceId}'.",
+ nameof(sourceId))
+ };
+ }
+ }
+}
diff --git a/samples/MinimalRobotServer/OpenUsdRepresentation.cs b/samples/MinimalRobotServer/OpenUsdRepresentation.cs
new file mode 100644
index 0000000000..800add6ee5
--- /dev/null
+++ b/samples/MinimalRobotServer/OpenUsdRepresentation.cs
@@ -0,0 +1,317 @@
+/* ========================================================================
+ * 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;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Opc.Ua;
+using Opc.Ua.OpenUsd;
+using Opc.Ua.OpenUsd.Server;
+using Opc.Ua.Server;
+
+namespace Robotics
+{
+ ///
+ /// Wires the draft OPC UA โ OpenUSD Bindings companion model onto the Robotics
+ /// server: the well-known OpenUSD facility (stage + representation registries),
+ /// a RobotCellStage descriptor, and the generic representation/binding helpers
+ /// used by RobotCell.cs.
+ ///
+ public sealed partial class RobotCell
+ {
+ private OpenUsdRootState? m_openUsdRoot;
+ private OpenUsdStageState? m_cellStage;
+
+ private const string CellRootLayerIdentifier = "asset-repo/Cell.usd";
+
+ private async ValueTask MaterialiseOpenUsdFacilityAsync(
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ ushort ns = (ushort)Server.NamespaceUris.GetIndex(Opc.Ua.OpenUsd.Namespaces.OpenUSD);
+
+ OpenUsdRootState root = SystemContext.CreateInstanceOfOpenUsdRootType(
+ null!, new QualifiedName("OpenUSD", ns));
+ root.NodeId = new NodeId("OpenUSD", ns);
+
+ FolderState stages = root.Stages
+ ?? root.CreateOrReplaceStages(SystemContext, null!);
+ _ = root.Representations
+ ?? root.CreateOrReplaceRepresentations(SystemContext, null!);
+
+ m_cellStage = SystemContext.CreateInstanceOfOpenUsdStageType(
+ stages, new QualifiedName("RobotCellStage", ns));
+ stages.AddChild(m_cellStage);
+ m_cellStage.CreateOrReplaceRootLayerIdentifier(SystemContext, null!)
+ .Value = CellRootLayerIdentifier;
+
+ // ยง5.2 Twin-BOM content integrity: the digest is computed over the
+ // *resolved root-layer content*, never over the identifier string, so a
+ // connector can detect tampering of the bytes it actually composes.
+ List servedAssets = LoadServedAssets();
+ byte[] rootLayerBytes = servedAssets
+ .Find(a => a.Kind == OpenUsdAssetKindEnum.RootLayer)!.Bytes;
+ byte[] digest;
+#pragma warning disable CA1850 // Prefer static HashData (net48/netstandard2.0 compatibility)
+ using (var sha = System.Security.Cryptography.SHA256.Create())
+ {
+ digest = sha.ComputeHash(rootLayerBytes);
+ }
+#pragma warning restore CA1850
+ m_cellStage.CreateOrReplaceRootLayerDigest(
+ SystemContext,
+ SystemContext.CreateOpenUsdStageType_RootLayerDigest(m_cellStage, forInstance: true))
+ .Value = (ByteString)digest;
+ m_cellStage.CreateOrReplaceRootLayerDigestAlgorithm(
+ SystemContext,
+ SystemContext.CreateOpenUsdStageType_RootLayerDigestAlgorithm(m_cellStage, forInstance: true))
+ .Value = OpenUsdDigestAlgorithmEnum.Sha256;
+
+ // Per the companion spec ยง4.2 the OpenUSD facility SHALL be a component
+ // of the Server Object (i=2253) so any conformant connector can Browse
+ // Server -> OpenUSD -> Representations. Record the inverse reference
+ // here; the matching forward reference from the Server Object is added
+ // via the externalReferences dictionary in LinkOpenUsdRootToServer.
+ root.AddReference(ReferenceTypeIds.HasComponent, true, Opc.Ua.ObjectIds.Server);
+
+ // ยง5.15 asset content delivery (OU-AssetDelivery): serve this stage's
+ // artist-authored USD layer closure so a connector can render the twin
+ // with no external asset resolver.
+ UsdAssetDelivery.AttachStageAssets(SystemContext, m_cellStage, ns, servedAssets);
+
+ SystemContext.AssignInstanceChildNodeIds(root);
+ await Manager.AddPredefinedNodeAsync(root, cancellationToken)
+ .ConfigureAwait(false);
+
+ m_openUsdRoot = root;
+ await LinkOpenUsdRootToServerAsync(cancellationToken).ConfigureAwait(false);
+ m_logger.MaterialisedOpenUsdFacility(root.NodeId, m_cellStage.NodeId);
+ }
+ catch (Exception ex)
+ {
+ m_cellStage = null;
+ m_openUsdRoot = null;
+ m_logger.OpenUsdFacilityFailed(ex);
+ }
+ }
+
+ ///
+ /// Loads the embedded artist-authored USD layers this server serves (spec ยง5.15).
+ ///
+ ///
+ private static List LoadServedAssets()
+ {
+ return
+ [
+ new("Cell.usda", OpenUsdAssetKindEnum.RootLayer, ReadEmbeddedAsset("Cell.usda")),
+ new("robot.usda", OpenUsdAssetKindEnum.Reference, ReadEmbeddedAsset("robot.usda")),
+ new("tool.usda", OpenUsdAssetKindEnum.Reference, ReadEmbeddedAsset("tool.usda"))
+ ];
+ }
+
+ private static byte[] ReadEmbeddedAsset(string resourceName)
+ {
+ using Stream? s = typeof(RobotCell).Assembly.GetManifestResourceStream(resourceName);
+ if (s == null)
+ {
+ return [];
+ }
+ using var ms = new MemoryStream();
+ s.CopyTo(ms);
+ return ms.ToArray();
+ }
+
+ private async ValueTask LinkOpenUsdRootToServerAsync(
+ CancellationToken cancellationToken)
+ {
+ if (m_openUsdRoot == null)
+ {
+ return;
+ }
+
+ var references = new IReference[]
+ {
+ new NodeStateReference(
+ ReferenceTypeIds.HasComponent,
+ false,
+ m_openUsdRoot.NodeId)
+ };
+ await Server.NodeManager.AddReferencesAsync(
+ Opc.Ua.ObjectIds.Server,
+ references,
+ cancellationToken).ConfigureAwait(false);
+ m_logger.LinkedOpenUsdFacility();
+ }
+
+ ///
+ /// Attaches an OpenUsdRepresentation AddIn to an Object and registers it in the
+ /// discovery registry (Server/OpenUSD/Representations, Organizes). Returns the
+ /// representation so the caller can add live/component bindings to it.
+ ///
+ ///
+ ///
+ ///
+ ///
+ private OpenUsdRepresentationState AttachRepresentation(
+ NodeState owner, string primPath, ushort ns)
+ {
+ OpenUsdRepresentationState rep = SystemContext
+ .CreateInstanceOfOpenUsdRepresentationType(
+ owner, new QualifiedName("OpenUsdRepresentation", ns));
+ // The instance factory leaves ReferenceTypeId = Null; set HasComponent so
+ // the AddIn is browsable from the represented object.
+ rep.ReferenceTypeId = ReferenceTypeIds.HasComponent;
+ owner.AddChild(rep);
+ rep.NodeId = SystemContext.NodeIdFactory.New(SystemContext, rep);
+ rep.CreateOrReplaceStage(SystemContext, null!).Value = m_cellStage!.NodeId;
+ rep.CreateOrReplacePrimPath(SystemContext, null!).Value = primPath;
+ AssignInstanceSubtree(rep, owner);
+ return rep;
+ }
+
+ ///
+ /// Adds the Organizes reference pair between the discovery registry and a
+ /// representation, so a generic connector discovers it without domain knowledge.
+ ///
+ ///
+ private void OrganiseRepresentation(OpenUsdRepresentationState rep)
+ {
+ FolderState? registry = m_openUsdRoot?.Representations;
+ if (registry == null)
+ {
+ return;
+ }
+ registry.AddReference(ReferenceTypeIds.Organizes, false, rep.NodeId);
+ rep.AddReference(ReferenceTypeIds.Organizes, true, registry.NodeId);
+ }
+
+ ///
+ /// Instantiates the OpenUsdLiveBinding placeholder as a concrete
+ /// browsable child of a representation and sets its members (spec ยง5.4/ยง5.10/ยง5.11).
+ /// Thin adapter over the reusable Opc.Ua.OpenUsd.Server authoring API: binds the
+ /// cell stage and forwards. The binding-authoring logic lives in the SDK
+ /// (OpenUsdRepresentationAuthoring.AddLiveBinding), not in this sample.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private OpenUsdLiveBindingState CreateBinding(
+ OpenUsdRepresentationState rep, ushort ns, string name,
+ Guid bindingDefinitionId, NodeId sourceNodeId, string targetPrimPath,
+ string targetPropertyName, string targetUsdTypeName,
+ OpenUsdRenderTargetKindEnum? kind, double scale,
+ uint bindingTypeId = Opc.Ua.OpenUsd.ObjectTypes.OpenUsdValueChangeBindingType,
+ OpenUsdSignalRoleEnum signalRole = OpenUsdSignalRoleEnum.Observable,
+ string? sourceSemanticId = null,
+ OpenUsdAlarmAspectEnum? alarmAspect = null,
+ NodeId commandTargetNodeId = default,
+ string? commandTriggerPropertyName = null)
+ {
+ OpenUsdLiveBindingState binding = rep.AddLiveBinding(
+ SystemContext, ns, m_cellStage!.NodeId, name, bindingDefinitionId, sourceNodeId,
+ targetPrimPath, targetPropertyName, targetUsdTypeName, kind, scale,
+ bindingTypeId, signalRole, sourceSemanticId, alarmAspect,
+ commandTargetNodeId, commandTriggerPropertyName);
+ AssignInstanceSubtree(binding, rep);
+ return binding;
+ }
+
+ ///
+ /// Instantiates the OpenUsdComponentBinding placeholder on a
+ /// representation and sets its members (spec ยง5.12โ5.14).
+ /// Thin adapter over the reusable Opc.Ua.OpenUsd.Server authoring API: the
+ /// component/composition-binding logic lives in the SDK
+ /// (OpenUsdRepresentationAuthoring.AddComponentBinding), not in this sample.
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ private void CreateComponentBinding(
+ OpenUsdRepresentationState rep, ushort ns, string name, Guid bindingDefinitionId,
+ OpenUsdCardinalityEnum cardinality, OpenUsdCompositionArcEnum arc, string targetPrimPath,
+ NodeId componentRepresentation = default, string? assetReference = null,
+ bool dynamic = false, NodeId changeEventSource = default,
+ NodeId componentTypeDefinition = default)
+ {
+ OpenUsdComponentBindingState binding = rep.AddComponentBinding(
+ SystemContext, ns, name, bindingDefinitionId, cardinality, arc, targetPrimPath,
+ componentRepresentation, assetReference, dynamic, changeEventSource,
+ componentTypeDefinition: componentTypeDefinition);
+ AssignInstanceSubtree(binding, rep);
+ }
+ }
+
+ internal static partial class OpenUsdRepresentationLog
+ {
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.OpenUsdRepresentation + 1,
+ Level = LogLevel.Information,
+ Message = "Materialised OpenUSD facility (root {RootId}, RobotCellStage {StageId}).")]
+ public static partial void MaterialisedOpenUsdFacility(
+ this ILogger logger, NodeId rootId, NodeId stageId);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.OpenUsdRepresentation + 2,
+ Level = LogLevel.Error,
+ Message = "Failed to materialise the OpenUSD facility.")]
+ public static partial void OpenUsdFacilityFailed(
+ this ILogger logger,
+ Exception exception);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.OpenUsdRepresentation + 3,
+ Level = LogLevel.Information,
+ Message = "Linked OpenUSD facility under the Server Object (i=2253).")]
+ public static partial void LinkedOpenUsdFacility(this ILogger logger);
+ }
+}
diff --git a/samples/MinimalRobotServer/Program.cs b/samples/MinimalRobotServer/Program.cs
new file mode 100644
index 0000000000..bdb2183afb
--- /dev/null
+++ b/samples/MinimalRobotServer/Program.cs
@@ -0,0 +1,85 @@
+/* ========================================================================
+ * 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 Microsoft.Extensions.Hosting;
+using Microsoft.Extensions.Logging;
+using Opc.Ua.Positioning.Server.Hosting;
+using Opc.Ua.Robotics.Server;
+using Robotics;
+
+// Self-contained OPC UA server exposing an OPC 40010 Robotics MotionDeviceSystem
+// (a robot cell of two 6-axis articulated robots) bound to OpenUSD via the draft
+// OPC UA - OpenUSD Bindings companion model. A generic connector renders the cell
+// live: each Axis' ActualPosition articulates one joint, the cell emergency-stop
+// drives a safety visual, a gripper tool is composed dynamically, and the robots
+// compose recursively (system -> devices -> axes).
+HostApplicationBuilder builder = Host.CreateApplicationBuilder(args);
+
+builder.Logging.ClearProviders();
+builder.Logging.AddConsole();
+
+int port = int.TryParse(builder.Configuration["port"], out int p) ? p : 62830;
+
+// Bind host for the OPC UA endpoint. Defaults to 0.0.0.0 so the server is reachable
+// from outside a container; override with --host / host env var (e.g. "localhost").
+string host = builder.Configuration["host"] is { Length: > 0 } h ? h : "0.0.0.0";
+
+builder.Services.AddOptions()
+ .Bind(builder.Configuration.GetSection("Robots"));
+builder.Services.AddSingleton();
+
+IPositioningServerBuilder positioning = builder.Services
+ .AddOpcUa()
+ .AddServer(o =>
+ {
+ o.ApplicationName = "MinimalRobotServer";
+ o.ApplicationUri = "urn:localhost:OPCFoundation:MinimalRobotServer";
+ o.ProductUri = "uri:opcfoundation.org:MinimalRobotServer";
+ o.AutoAcceptUntrustedCertificates = true;
+ o.EndpointUrls.Add($"opc.tcp://{host}:{port}/MinimalRobotServer");
+ })
+ .AddRobotics()
+ .AddRoboticsModel()
+ .AddRoboticsModel()
+ .AddRoboticsModel()
+ .ConfigureRobotics()
+ .ConfigureRobotics(async context =>
+ await context.GetRequiredService()
+ .RunAsync(context.Manager, context.CancellationToken).ConfigureAwait(false))
+ .AddPositioningFor();
+
+positioning
+ .AddGeoLocationProvider()
+ .ConfigurePositioningFor(
+ context => RobotCell.GetForManager(context.Manager)
+ .ConfigurePositioningAsync(context));
+
+using IHost app = builder.Build();
+await app.RunAsync().ConfigureAwait(false);
diff --git a/samples/MinimalRobotServer/Properties/AssemblyInfo.cs b/samples/MinimalRobotServer/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..2b9848014c
--- /dev/null
+++ b/samples/MinimalRobotServer/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+/* ========================================================================
+ * 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;
+
+[assembly: CLSCompliant(false)]
diff --git a/samples/MinimalRobotServer/README.md b/samples/MinimalRobotServer/README.md
new file mode 100644
index 0000000000..7eb27de1d2
--- /dev/null
+++ b/samples/MinimalRobotServer/README.md
@@ -0,0 +1,157 @@
+# MinimalRobotServer
+
+A minimal, self-contained .NET console OPC UA server that demonstrates the **OPC 40010
+Robotics**, **OPC 10000-210 Relative Spatial Location**, and **OPC 10000-211
+Global Positioning** companion specifications bound to **OpenUSD** through the draft
+[*OPC UA โ OpenUSD Bindings*](../../../opcua-drafts/core-specs/openusd-binding/OPC-UA-OpenUSD-Bindings.md)
+companion model, so a **generic** connector renders a robot cell live with **no
+robot-specific code**. It is built on the `Opc.Ua.Robotics` and `Opc.Ua.OpenUsd`
+SDK libraries. See [`docs/Robotics.md`](../../docs/Robotics.md) for the Robotics
+developer guide and [`docs/OpenUsd.md`](../../docs/OpenUsd.md) for the OpenUSD
+binding.
+
+It is the Robotics counterpart of `PumpDeviceIntegrationServer` and is validated
+end-to-end by `RobotOpenUsdE2eTests` (in `tests/Opc.Ua.Di.Tests`).
+
+## What it exposes
+
+A `MotionDeviceSystem` **"RobotCell"** (prim `/Cell`) composed recursively of:
+
+- **Two 6-axis articulated robots** (`MotionDeviceType` `R1`, `R2` โ `/Cell/Robots/R1`,
+ `/Cell/Robots/R2`), aggregated 1..n with a `Many` / `Reference` ``
+ binding (Reference โ not Instance โ so each robot articulates independently).
+- **Six axes per robot** (`AxisType` `A1..A6`), aggregated with a nested `Many` /
+ `Child` `` binding. Each Axis' `ParameterSet/ActualPosition` (degrees)
+ drives one joint `xformOp:rotate{Z|Y|X}` on the (pre-authored) `robot.usda`
+ kinematic chain โ the live articulation (`RenderTargetKind = Rotation`). The axis
+ limits are those of the reference robot (see below), and the simulation runs an eased
+ pick-and-place cycle rather than a free sweep.
+- A cell **EmergencyStop** safety state driving a beacon and per-robot warning halo
+ visibility (`UaAlarmToUsd`, `Visibility`).
+- An opt-in **SpeedOverride** command (`UsdToUaCommand`, fail-closed).
+- A **gripper tool** mounted on R1's flange at runtime (`One` / `Reference`,
+ `Dynamic = true`) via a model-change event.
+- One RSL spatial-object list with a world frame, R1/R2 SpatialObject AddIns,
+ each robot's PositionFrame, and R1's ToolFlange AttachPoint.
+- One GPOS Zone with ground-control points and one live GlobalLocation per
+ robot. Each robot independently selects `Fixed`, `FigureEight`, `Circle`, or
+ `Shuttle` motion; the default uses phase-shifted figure-eight paths.
+
+All 15 representations (1 system + 2 robots + 12 axes) are discoverable through the
+well-known `Server/OpenUSD/Representations` registry.
+
+## The USD assets
+
+`Assets/robot.usda`, `Assets/tool.usda` and `Assets/Cell.usda` are embedded in the server
+and served over Part 5 `FileType` with SHA-256 digests, so a connector can fetch the whole
+closure and render the twin without any external asset resolver.
+
+### Reference robot โ KUKA KR 16-2
+
+`robot.usda` reproduces the published kinematics of a **KUKA KR 16-2**, taken from the
+ROS-Industrial URDF (`ros-industrial/kuka_experimental`, `kuka_kr16_support`):
+
+| Joint | Offset from the previous axis | Rotate op | Limit |
+| --- | --- | --- | --- |
+| A1 | 675 mm up from the mounting face | `xformOp:rotateZ` | ยฑ185ยฐ |
+| A2 | 260 mm forward | `xformOp:rotateY` | โ155ยฐโฆ+35ยฐ |
+| A3 | 680 mm along the upper arm | `xformOp:rotateY` | โ130ยฐโฆ+154ยฐ |
+| A4 | 670 mm along the forearm, โ35 mm lateral | `xformOp:rotateX` | ยฑ350ยฐ |
+| A5 | coincident with A4 | `xformOp:rotateY` | ยฑ130ยฐ |
+| A6 | coincident with A4 | `xformOp:rotateX` | ยฑ350ยฐ |
+| Flange | 158 mm past the wrist centre | โ | โ |
+
+`260 + 680 + 670 = 1611 mm`, the robot's published reach. A4, A5 and A6 are coincident,
+which is the real spherical wrist. The livery is KUKA orange over dark cast housings, and
+the cell is scaled around that reach: a 7.2 ร 4.8 m floor with the guarding at
+x = ยฑ3.4 m, y = ยฑ2.2 m, plus a controller cabinet, a shared work table and a stack light.
+
+### Mobile platform
+
+Each arm rides on an omnidirectional AGV (`/Robot/MobileBase`, 1.10 ร 0.72 m, 365 mm deck)
+in the style of a KUKA KMP: orange chassis, dark skirt, four wheels, diagonally opposed
+safety scanners, bumpers and status strips. The RSL scenario drives each robot's spatial
+location across the cell, so the arm has to be carried rather than bolted to the floor.
+The platform footprint and the work-table position are chosen so the two AGVs clear each
+other at their closest approach and never drive through the table.
+
+To floor-mount the arm instead, delete `/Robot/MobileBase` and clear the `xformOp:translate`
+on `/Robot/Base`; nothing else depends on it.
+
+### Editing the assets
+
+The prim paths and rotate ops above are the **binding contract**: `RobotCell.cs` addresses
+them by name, and the connector writes each rotate op as a scalar `double`. Link offsets,
+geometry, materials and lighting are all free to change; the paths, op names,
+`xformOpOrder` entries, `/Robot/Base/.../Flange`, `/Robot/Warning`, `/Cell/SafetyBeacon`,
+`/Cell.inputs:speedOverride` and the `R1`/`R2` mount attributes are not.
+
+`RobotAssetContractTests` (in `tests/Opc.Ua.OpenUsd.Tests`) parses the shipped assets
+and asserts that contract, so an edit that breaks it fails the build instead of silently
+freezing the twin. Note that the `.usda` files under
+`tests/Opc.Ua.OpenUsd.Tests/Assets` are a **frozen** reader/materializer fixture and
+are deliberately *not* kept in sync with these assets.
+
+Author with real USDA syntax: the managed `UsdaReader` is more forgiving than OpenUSD
+itself, so a layer that parses in the tests can still fail to load in a viewer. The
+end-to-end run below is the authoritative check.
+
+## Design note โ stock Robotics hosting
+
+The server uses the stock `Opc.Ua.Robotics.Server` hosting pipeline:
+
+- `AddRobotics()` registers the sealed stock `RoboticsNodeManager`, DI/IA/Robotics
+ model provider, and the shared Robotics configurator pipeline.
+- `AddRoboticsModel()`,
+ `AddRoboticsModel()`, and
+ `AddRoboticsModel()` contribute the additional compiled
+ OpenUSD, RSL, and GPOS companion models to the same manager.
+- `ConfigureRobotics()` builds the cell with the validated fluent
+ topology builders (`AddMotionDeviceSystemAsync`, `AddMotionDevice`,
+ `AddAxis`, safety, controller, task-control, and operation builders).
+- `AddPositioningFor()` and
+ `ConfigurePositioningFor(...)` add the RSL/GPOS spatial
+ objects, frames, attach point, zone, and live global-location bindings after
+ the Robotics topology exists.
+
+The OpenUSD facility still lives under the standard `Server/OpenUSD` entry point,
+but it is now authored by the `RobotCell` configurator against the stock manager
+instead of by subclassing a custom node manager.
+
+## Run it
+
+```
+dotnet run --project samples/MinimalRobotServer -- --host localhost --port 62830
+```
+
+Then drive it with the **same** generic connector used for pumps
+(`Opc.Ua.OpenUsd.Connector`) to author a live USD override layer:
+
+```
+dotnet run --project tools/Opc.Ua.OpenUsd.Connector -- \
+ --server opc.tcp://localhost:62830/MinimalRobotServer \
+ --out /live.usda --insecure --seconds 10
+```
+
+Compose `live.usda` over the base `Cell.usda` (see the example `stage.usda`) and open
+it in `usdview` / NVIDIA Omniverse to see the two arms articulate live. The example
+USD assets, descriptor, writer, and a step-by-step guide live in the `opcua-drafts`
+repo under `core-specs/extras/openusd-binding/examples/robotics/`.
+
+To watch the cell animate without leaving the connector, install the optional
+`Opc.Ua.OpenUsd.Connector.Viewer` assembly beside it and add `--view`:
+
+```
+dotnet run --project tools/Opc.Ua.OpenUsd.Connector -- \
+ --server opc.tcp://localhost:62830/MinimalRobotServer --insecure --view
+```
+
+That fetches this server's served asset closure, composes `stage.usda`, opens a viewport, and streams
+the same subscribed values into both the override layer and the rendered stage, so the joints move on
+screen as the simulation runs. See [`docs/OpenUsd.md`](../../docs/OpenUsd.md#rendering-the-twin-live).
+
+The robot motion is configured through `RobotMobilityOptions`. RSL
+Position/Orientation values author `xformOp:translate` and
+`xformOp:rotateXYZ`; GPOS longitude, latitude, and elevation author the
+`inputs:longitude`, `inputs:latitude`, and `inputs:elevation` attributes. See
+[`docs/Positioning.md`](../../docs/Positioning.md).
diff --git a/samples/MinimalRobotServer/RobotCell.Configure.cs b/samples/MinimalRobotServer/RobotCell.Configure.cs
new file mode 100644
index 0000000000..7212157828
--- /dev/null
+++ b/samples/MinimalRobotServer/RobotCell.Configure.cs
@@ -0,0 +1,178 @@
+/* ========================================================================
+ * 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;
+using System.Threading;
+using Opc.Ua;
+using Opc.Ua.Server.Fluent;
+
+namespace Robotics
+{
+ ///
+ /// Sibling partial that animates the robot cell: a single manager-owned
+ /// simulation tick sweeps every axis about its home pose (so the two arms move
+ /// live and independently) and briefly asserts the cell emergency-stop so the
+ /// safety beacon and per-robot warnings blink.
+ ///
+ public sealed partial class RobotCell
+ {
+ private long m_simulationTicks;
+
+ // 20 Hz. The twin can never move more smoothly than the source publishes, so the
+ // tick is chosen for animation rather than for a typical telemetry cadence.
+ private const double TickSeconds = 0.05;
+ private const int TickMilliseconds = 50;
+ private const int EstopPeriodTicks = 600;
+ private const int EstopActiveFromTick = 560;
+
+ ///
+ /// Key poses of one transfer cycle, in the axis order A1..A6 (degrees). The arm
+ /// swings to the pick station, descends, closes on the part, lifts clear, traverses
+ /// while rotating the part upright, places it, and retracts. Each entry moves from
+ /// the previous pose over MoveSeconds and then holds for DwellSeconds,
+ /// which is what makes the motion read as a real duty cycle instead of a sweep.
+ ///
+ private static readonly (double[] Pose, double MoveSeconds, double DwellSeconds)[] s_cycle =
+ [
+ ([0.0, -60.0, 75.0, 0.0, 45.0, 0.0], 1.6, 0.4),
+ ([55.0, -45.0, 70.0, 0.0, 45.0, 0.0], 1.8, 0.2),
+ ([55.0, -30.0, 62.0, 0.0, 58.0, 0.0], 1.0, 0.8),
+ ([55.0, -55.0, 78.0, 0.0, 47.0, 0.0], 1.0, 0.2),
+ ([-15.0, -55.0, 78.0, 0.0, 47.0, 90.0], 2.0, 0.2),
+ ([-45.0, -45.0, 70.0, 0.0, 50.0, 90.0], 1.4, 0.2),
+ ([-45.0, -28.0, 60.0, 0.0, 58.0, 90.0], 1.0, 0.8),
+ ([-45.0, -60.0, 80.0, 0.0, 45.0, 0.0], 1.4, 0.3)
+ ];
+
+ private static readonly double s_cycleSeconds = ComputeCycleSeconds();
+
+ partial void Configure(INodeManagerBuilder builder)
+ {
+ // Single manager-owned simulation tick advances every axis position and the
+ // emergency-stop flag.
+ builder.Simulation(TimeSpan.FromMilliseconds(TickMilliseconds))
+ .OnTick((ctx, elapsed) => AdvanceSimulation());
+ }
+
+ private void AdvanceSimulation()
+ {
+ long t = Interlocked.Increment(ref m_simulationTicks);
+ double time = t * TickSeconds;
+
+ // Drive every axis from the shared pick-and-place cycle, offset per robot so
+ // the two arms are not synchronised, and clamp to the axis' own limits.
+ foreach (AxisRuntime ax in m_axes)
+ {
+ double target = EvaluateCycle(time + ax.PhaseSeconds, ax.Index);
+ if (target < ax.Min)
+ {
+ target = ax.Min;
+ }
+ if (target > ax.Max)
+ {
+ target = ax.Max;
+ }
+ UpdateDouble(ax.Position, target);
+ }
+
+ // Emergency-stop pulses active (~2 s) roughly every 30 s so the safety
+ // beacon (cell) and warning zones (robots) toggle live.
+ bool estop = (t % EstopPeriodTicks) >= EstopActiveFromTick;
+ UpdateBool(m_estopVar, estop);
+ }
+
+ ///
+ /// Samples the cycle for one axis at a point in time, easing each move so the arm
+ /// accelerates and settles rather than stepping between poses.
+ ///
+ private static double EvaluateCycle(double time, int axisIndex)
+ {
+ double position = time % s_cycleSeconds;
+ if (position < 0.0)
+ {
+ position += s_cycleSeconds;
+ }
+
+ for (int i = 0; i < s_cycle.Length; i++)
+ {
+ (double[] pose, double move, double dwell) = s_cycle[i];
+ if (position < move)
+ {
+ double[] previous = s_cycle[(i + s_cycle.Length - 1) % s_cycle.Length].Pose;
+ double u = position / move;
+ double eased = u * u * (3.0 - (2.0 * u));
+ return previous[axisIndex] + ((pose[axisIndex] - previous[axisIndex]) * eased);
+ }
+ position -= move;
+ if (position < dwell)
+ {
+ return pose[axisIndex];
+ }
+ position -= dwell;
+ }
+ return s_cycle[^1].Pose[axisIndex];
+ }
+
+ private static double ComputeCycleSeconds()
+ {
+ double total = 0.0;
+ foreach ((double[] _, double move, double dwell) in s_cycle)
+ {
+ total += move + dwell;
+ }
+ return total;
+ }
+
+ private void UpdateDouble(BaseDataVariableState? v, double value)
+ {
+ if (v == null)
+ {
+ return;
+ }
+ v.Value = value;
+ v.Timestamp = DateTime.UtcNow;
+ v.ClearChangeMasks(SystemContext, includeChildren: false);
+ }
+
+ private void UpdateBool(BaseDataVariableState? v, bool value)
+ {
+ if (v == null)
+ {
+ return;
+ }
+ bool current = v.Value.TryGetValue(out bool b) && b;
+ if (current != value)
+ {
+ v.Value = value;
+ v.Timestamp = DateTime.UtcNow;
+ v.ClearChangeMasks(SystemContext, includeChildren: false);
+ }
+ }
+ }
+}
diff --git a/samples/MinimalRobotServer/RobotCell.cs b/samples/MinimalRobotServer/RobotCell.cs
new file mode 100644
index 0000000000..f9ede9e0d4
--- /dev/null
+++ b/samples/MinimalRobotServer/RobotCell.cs
@@ -0,0 +1,839 @@
+/* ========================================================================
+ * 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;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Opc.Ua;
+using Opc.Ua.Di.Server;
+using Opc.Ua.Gpos;
+using Opc.Ua.OpenUsd;
+using Opc.Ua.Positioning;
+using Opc.Ua.Positioning.Server;
+using Opc.Ua.Positioning.Server.Hosting;
+using Opc.Ua.Robotics;
+using Opc.Ua.Robotics.Server;
+using Opc.Ua.Robotics.Server.Builders;
+using Opc.Ua.Rsl;
+using Opc.Ua.Server;
+using Opc.Ua.Server.Fluent;
+using ReferenceTypeIds = Opc.Ua.ReferenceTypeIds;
+
+namespace Robotics
+{
+ ///
+ /// Builds the sample robot cell using the stock Robotics hosting configurator.
+ ///
+ public sealed partial class RobotCell : IRoboticsConfigurator, IDisposable
+ {
+ private const string CellPrimPath = "/Cell";
+ private const string RobotsScopePrimPath = "/Cell/Robots";
+ private const string ToolSuffix = "/Base/J1/J2/J3/J4/J5/J6/Flange/Tool";
+ private const uint MotionDeviceTypeId = 1004;
+ private const uint AxisTypeId = 16601;
+
+ // Axis table of the reference robot, a KUKA KR 16-2. The limits are the published
+ // software limits; the home values are a plausible ready pose and match the
+ // defaults authored into robot.usda so the asset looks right when opened on its
+ // own. LinkPrimPath and RotateOp are the binding contract with the USD asset.
+ private static readonly (
+ string Name,
+ string LinkPrimPath,
+ string RotateOp,
+ double Home,
+ double Min,
+ double Max)[] s_axisTemplate =
+ [
+ ("A1", "Base/J1", "xformOp:rotateZ", 0.0, -185.0, 185.0),
+ ("A2", "Base/J1/J2", "xformOp:rotateY", -60.0, -155.0, 35.0),
+ ("A3", "Base/J1/J2/J3", "xformOp:rotateY", 75.0, -130.0, 154.0),
+ ("A4", "Base/J1/J2/J3/J4", "xformOp:rotateX", 0.0, -350.0, 350.0),
+ ("A5", "Base/J1/J2/J3/J4/J5", "xformOp:rotateY", 45.0, -130.0, 130.0),
+ ("A6", "Base/J1/J2/J3/J4/J5/J6", "xformOp:rotateX", 0.0, -350.0, 350.0)
+ ];
+
+ private static readonly (string BrowseName, string PrimPath, bool HasTool, double PhaseSeconds)[] s_robots =
+ [
+ ("R1", "/Cell/Robots/R1", true, 0.0),
+ ("R2", "/Cell/Robots/R2", false, 3.0)
+ ];
+
+ private static readonly ConditionalWeakTable s_cells = new();
+
+ private readonly ILogger m_logger;
+ private readonly List m_axes = [];
+ private readonly List m_axisReps = [];
+ private readonly List m_robots = [];
+ private readonly List m_positioningSubscriptions = [];
+ private BaseDataVariableState? m_estopVar;
+ private BaseDataVariableState? m_speedOverrideVar;
+ private DiNodeManager? m_manager;
+ private NodeId m_r1NodeId;
+ private MotionDeviceSystemState? m_robotCell;
+
+ ///
+ /// Creates the sample Robotics configurator.
+ ///
+ ///
+ /// The configurator logger.
+ ///
+ public RobotCell(ILogger logger)
+ {
+ m_logger = logger ?? throw new ArgumentNullException(nameof(logger));
+ }
+
+ private DiNodeManager Manager => m_manager ?? throw new InvalidOperationException(
+ "RobotCell has not been attached to a Robotics build context.");
+
+ private IServerInternal Server => Manager.Server;
+
+ private ServerSystemContext SystemContext => Manager.SystemContext;
+
+ ///
+ public async ValueTask ConfigureAsync(
+ IRoboticsBuildContext context,
+ CancellationToken cancellationToken)
+ {
+ if (context == null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+
+ m_manager = context.Manager;
+ s_cells.Remove(context.Manager);
+ s_cells.Add(context.Manager, this);
+ await MaterialiseOpenUsdFacilityAsync(cancellationToken).ConfigureAwait(false);
+ await MaterialiseRobotCellAsync(context, cancellationToken).ConfigureAwait(false);
+ Configure(context.Nodes);
+ m_logger.RoboticsAddressSpaceReady(m_axes.Count + m_robots.Count + 1);
+ }
+
+
+ internal static RobotCell GetForManager(AsyncCustomNodeManager manager)
+ {
+ if (manager == null)
+ {
+ throw new ArgumentNullException(nameof(manager));
+ }
+
+ return s_cells.TryGetValue(manager, out RobotCell? cell)
+ ? cell
+ : throw new InvalidOperationException("RobotCell has not configured this manager.");
+ }
+
+ internal sealed class AxisRuntime
+ {
+ public BaseDataVariableState Position = null!;
+ public double Home;
+ public double Min;
+ public double Max;
+ public double PhaseSeconds;
+ public int Index;
+ }
+
+ internal sealed class RobotRuntime
+ {
+ public string SourceId = string.Empty;
+ public string PrimPath = string.Empty;
+ public MotionDeviceState Robot = null!;
+ public OpenUsdRepresentationState Representation = null!;
+ }
+
+ private NodeId RoboticsType(uint id)
+ {
+ return NodeId.Create(id, Opc.Ua.Robotics.Namespaces.Robotics, Server.NamespaceUris);
+ }
+
+ private async ValueTask MaterialiseRobotCellAsync(
+ IRoboticsBuildContext context,
+ CancellationToken cancellationToken)
+ {
+ if (m_cellStage == null)
+ {
+ return;
+ }
+
+ try
+ {
+ ushort ns = context.InstanceNamespaceIndex;
+ ushort usdNs = (ushort)Server.NamespaceUris.GetIndex(Opc.Ua.OpenUsd.Namespaces.OpenUSD);
+ OpenUsdRepresentationState? cellRep = null;
+ var robotReps = new List();
+
+ await context.AddMotionDeviceSystemAsync(
+ "RobotCell",
+ system =>
+ {
+ system.WithComponentName("RobotCell");
+ cellRep = AttachRepresentation(system.State, CellPrimPath, usdNs);
+ m_robotCell = system.State;
+
+ IControllerBuilder controller = system.AddController(
+ "Controller_C1",
+ ConfigureController);
+ ISafetyStateBuilder safety = system.AddSafetyState(
+ "Safety",
+ ConfigureSafetyState);
+
+ foreach ((string BrowseName, string PrimPath, bool HasTool, double PhaseSeconds) robot
+ in s_robots)
+ {
+ IMotionDeviceBuilder motionDevice = system.AddMotionDevice(
+ robot.BrowseName,
+ builder => BuildRobot(builder, robot, ns, usdNs, robotReps));
+ controller.Controls(motionDevice).UsesSafetyState(safety);
+ }
+
+ CreateComponentBinding(cellRep, usdNs, "RobotsAggregation",
+ new Guid("a1b2c3d4-0001-4a10-9c01-100000000001"),
+ OpenUsdCardinalityEnum.Many, OpenUsdCompositionArcEnum.Reference,
+ RobotsScopePrimPath, assetReference: "@robot.usda@",
+ componentTypeDefinition: RoboticsType(MotionDeviceTypeId));
+
+ CreateBinding(cellRep, usdNs, "EmergencyStopBeacon",
+ new Guid("a1b2c3d4-0002-4a10-9c01-100000000002"),
+ m_estopVar!.NodeId, "/Cell/SafetyBeacon", "visibility", "token",
+ OpenUsdRenderTargetKindEnum.Visibility, 1.0,
+ bindingTypeId: Opc.Ua.OpenUsd.ObjectTypes.OpenUsdAlarmBindingType,
+ alarmAspect: OpenUsdAlarmAspectEnum.ActiveState);
+
+ CreateBinding(cellRep, usdNs, "SpeedOverrideCommand",
+ new Guid("a1b2c3d4-0003-4a10-9c01-100000000003"),
+ NodeId.Null, "/Cell", "inputs:speedOverride", "double",
+ kind: null, 1.0,
+ bindingTypeId: Opc.Ua.OpenUsd.ObjectTypes.OpenUsdCommandBindingType,
+ signalRole: OpenUsdSignalRoleEnum.Controllable,
+ commandTargetNodeId: m_speedOverrideVar!.NodeId,
+ commandTriggerPropertyName: "inputs:speedOverride");
+ },
+ cancellationToken).ConfigureAwait(false);
+
+ if (cellRep == null || m_estopVar == null || m_speedOverrideVar == null)
+ {
+ throw ServiceResultException.Create(
+ StatusCodes.BadConfigurationError,
+ "RobotCell was not fully materialised.");
+ }
+
+ OrganiseRepresentation(cellRep);
+ foreach (OpenUsdRepresentationState robotRep in robotReps)
+ {
+ OrganiseRepresentation(robotRep);
+ }
+ foreach (OpenUsdRepresentationState axisRep in m_axisReps)
+ {
+ OrganiseRepresentation(axisRep);
+ }
+
+ Manager.ModelChangeEmissionEnabled = true;
+ _ = RunDynamicToolAsync(ns, usdNs);
+
+ m_logger.MaterialisedRobotCell(
+ s_robots.Length,
+ m_axes.Count,
+ 1 + robotReps.Count + m_axisReps.Count);
+ }
+ catch (Exception ex)
+ {
+ m_logger.RobotCellFailed(ex);
+ }
+ }
+
+ private void ConfigureController(IControllerBuilder controller)
+ {
+ controller.WithComponentName("Controller_C1")
+ .WithCurrentUser(user => user.WithLevel("Operator").WithName("Operator"))
+ .AddSoftware("ControllerSoftware");
+ controller.AddTaskControl(
+ "TaskControl_C1",
+ taskControl => taskControl
+ .WithComponentName("TaskControl_C1")
+ .WithTaskProgramLoaded(false)
+ .WithTaskProgramName(string.Empty)
+ .AddTaskControlOperation());
+ }
+
+ private void ConfigureSafetyState(ISafetyStateBuilder safety)
+ {
+ safety.WithComponentName("Safety")
+ .WithEmergencyStop(false)
+ .AddEmergencyStop("EmergencyStop", "EmergencyStop", stop => stop.WithActive(false));
+ safety.Configure((state, ctx) =>
+ {
+ m_estopVar = FindRequiredChild(
+ state.ParameterSet!,
+ "EmergencyStop");
+ });
+ }
+
+ private void BuildRobot(
+ IMotionDeviceBuilder builder,
+ (string BrowseName, string PrimPath, bool HasTool, double PhaseSeconds) robot,
+ ushort ns,
+ ushort usdNs,
+ List robotReps)
+ {
+ builder.WithComponentName(robot.BrowseName)
+ .WithCategory(MotionDeviceCategoryEnumeration.ARTICULATED_ROBOT)
+ .WithSpeedOverride(100.0);
+ if (robot.HasTool)
+ {
+ builder.BindSpeedOverrideWrite(WriteSpeedOverrideAsync);
+ }
+
+ OpenUsdRepresentationState? robotRep = null;
+ IPowerTrainBuilder powerTrain = builder.AddPowerTrain(
+ $"{robot.BrowseName}_PowerTrain",
+ pt => pt.WithComponentName($"{robot.BrowseName}_PowerTrain")
+ .AddMotor($"{robot.BrowseName}_Motor", motor => motor.WithMotorTemperature(25.0)));
+
+ for (int i = 0; i < s_axisTemplate.Length; i++)
+ {
+ IAxisBuilder axis = builder.AddAxis(
+ s_axisTemplate[i].Name,
+ axisBuilder => BuildAxis(axisBuilder, robot, s_axisTemplate[i], i, usdNs));
+ axis.Requires(powerTrain);
+ powerTrain.Moves(axis);
+ }
+
+ builder.Configure((state, ctx) =>
+ {
+ if (robot.HasTool)
+ {
+ m_r1NodeId = state.NodeId;
+ m_speedOverrideVar = FindRequiredChild(
+ state.ParameterSet!,
+ "SpeedOverride");
+ m_speedOverrideVar.OnReadUserAccessLevel = OnReadCommandTargetUserAccessLevel;
+ }
+
+ robotRep = AttachRepresentation(state, robot.PrimPath, usdNs);
+ m_robots.Add(new RobotRuntime
+ {
+ SourceId = robot.BrowseName,
+ PrimPath = robot.PrimPath,
+ Robot = state,
+ Representation = robotRep
+ });
+ robotReps.Add(robotRep);
+ });
+
+ if (robotRep == null)
+ {
+ return;
+ }
+
+ CreateComponentBinding(robotRep, usdNs, "AxesAggregation",
+ GuidFor(robot.BrowseName + ":axes"),
+ OpenUsdCardinalityEnum.Many, OpenUsdCompositionArcEnum.Child,
+ robot.PrimPath, componentTypeDefinition: RoboticsType(AxisTypeId));
+
+ CreateBinding(robotRep, usdNs, "EmergencyStopWarning",
+ GuidFor(robot.BrowseName + ":warning"),
+ m_estopVar!.NodeId, robot.PrimPath + "/Warning", "visibility", "token",
+ OpenUsdRenderTargetKindEnum.Visibility, 1.0,
+ bindingTypeId: Opc.Ua.OpenUsd.ObjectTypes.OpenUsdAlarmBindingType,
+ alarmAspect: OpenUsdAlarmAspectEnum.ActiveState);
+
+ if (robot.HasTool)
+ {
+ CreateComponentBinding(robotRep, usdNs, "GripperTool",
+ GuidFor(robot.BrowseName + ":tool"),
+ OpenUsdCardinalityEnum.One, OpenUsdCompositionArcEnum.Reference,
+ robot.PrimPath + ToolSuffix, assetReference: "@tool.usda@",
+ dynamic: true, changeEventSource: Opc.Ua.ObjectIds.Server,
+ componentTypeDefinition: Opc.Ua.ObjectTypeIds.BaseObjectType);
+ }
+ }
+
+ private void BuildAxis(
+ IAxisBuilder builder,
+ (string BrowseName, string PrimPath, bool HasTool, double PhaseSeconds) robot,
+ (string Name, string LinkPrimPath, string RotateOp, double Home, double Min, double Max) axis,
+ int index,
+ ushort usdNs)
+ {
+ builder.WithMotionProfile(AxisMotionProfileEnumeration.ROTARY)
+ .WithActualPosition(axis.Home);
+ builder.Configure((state, ctx) =>
+ {
+ BaseDataVariableState position = FindRequiredChild(
+ state.ParameterSet!,
+ "ActualPosition");
+ string linkPrim = robot.PrimPath + "/" + axis.LinkPrimPath;
+ OpenUsdRepresentationState axisRep = AttachRepresentation(state, linkPrim, usdNs);
+
+ CreateBinding(axisRep, usdNs, "Articulation",
+ GuidFor($"{robot.BrowseName}:{axis.Name}:articulation"),
+ position.NodeId, linkPrim, axis.RotateOp, "double",
+ OpenUsdRenderTargetKindEnum.Rotation, 1.0,
+ signalRole: OpenUsdSignalRoleEnum.Observable,
+ sourceSemanticId: "0173-1#02-BAF564#005");
+
+ m_axisReps.Add(axisRep);
+ m_axes.Add(new AxisRuntime
+ {
+ Position = position,
+ Home = axis.Home,
+ Min = axis.Min,
+ Max = axis.Max,
+ PhaseSeconds = robot.PhaseSeconds,
+ Index = index
+ });
+ });
+ }
+
+ private T FindRequiredChild(NodeState parent, string browseName)
+ where T : BaseInstanceState
+ {
+ if (parent == null)
+ {
+ throw new ArgumentNullException(nameof(parent));
+ }
+
+ var children = new List();
+ parent.GetChildren(SystemContext, children);
+ for (int i = 0; i < children.Count; i++)
+ {
+ if (children[i] is T typed &&
+ string.Equals(children[i].BrowseName.Name, browseName, StringComparison.Ordinal))
+ {
+ return typed;
+ }
+ }
+
+ throw ServiceResultException.Create(
+ StatusCodes.BadConfigurationError,
+ "Child '{0}' was not found below '{1}'.",
+ browseName,
+ parent.BrowseName);
+ }
+
+ private void AssignInstanceSubtree(BaseInstanceState node, NodeState referenceRoot)
+ {
+ NodeId previousNodeId = SystemContext.AssignInstanceNodeId(node);
+ SystemContext.AssignInstanceChildNodeIds(node, previousNodeId, referenceRoot);
+ }
+
+ private ValueTask WriteSpeedOverrideAsync(
+ Variant value,
+ CancellationToken cancellationToken)
+ {
+ if (!value.TryGetValue(out double speedOverride))
+ {
+ return new ValueTask(new ServiceResult(StatusCodes.BadTypeMismatch));
+ }
+
+ UpdateDouble(m_speedOverrideVar, speedOverride);
+ return new ValueTask(ServiceResult.Good);
+ }
+
+ private static ServiceResult OnReadCommandTargetUserAccessLevel(
+ ISystemContext context,
+ NodeState node,
+ ref byte value)
+ {
+ value = IsAuthenticatedSession(context)
+ ? AccessLevels.CurrentReadOrWrite
+ : AccessLevels.CurrentRead;
+ return ServiceResult.Good;
+ }
+
+ private static bool IsAuthenticatedSession(ISystemContext context)
+ {
+ IUserIdentity? identity = (context as ISessionSystemContext)?.UserIdentity;
+ if (identity == null || identity.TokenType == UserTokenType.Anonymous)
+ {
+ return false;
+ }
+ ArrayOf roles = identity.GrantedRoleIds;
+ for (int i = 0; i < roles.Count; i++)
+ {
+ if (roles[i] != Opc.Ua.ObjectIds.WellKnownRole_Anonymous)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private async Task RunDynamicToolAsync(ushort ns, ushort usdNs)
+ {
+ try
+ {
+ if (m_r1NodeId.IsNull)
+ {
+ return;
+ }
+ await Task.Delay(3000).ConfigureAwait(false);
+ while (!m_r1NodeId.IsNull)
+ {
+ NodeId toolId = await AddMountedToolAsync(ns, usdNs).ConfigureAwait(false);
+ if (toolId.IsNull)
+ {
+ return;
+ }
+ m_logger.GripperToolMounted();
+ await Task.Delay(12000).ConfigureAwait(false);
+ _ = await Manager.DeleteNodeAsync(SystemContext, toolId).ConfigureAwait(false);
+ m_logger.DetachedGripperTool(toolId);
+ await Task.Delay(6000).ConfigureAwait(false);
+ }
+ }
+ catch (Exception ex)
+ {
+ m_logger.DynamicToolFailed(ex);
+ }
+ }
+
+ private async Task AddMountedToolAsync(ushort ns, ushort usdNs)
+ {
+ if (m_r1NodeId.IsNull)
+ {
+ return NodeId.Null;
+ }
+ var tool = new BaseObjectState(null)
+ {
+ SymbolicName = "MountedTool",
+ BrowseName = new QualifiedName("MountedTool", ns),
+ DisplayName = new LocalizedText("MountedTool"),
+ TypeDefinitionId = Opc.Ua.ObjectTypeIds.BaseObjectType
+ };
+
+ OpenUsdRepresentationState rep = SystemContext.CreateInstanceOfOpenUsdRepresentationType(
+ tool,
+ new QualifiedName("OpenUsdRepresentation", usdNs));
+ rep.ReferenceTypeId = ReferenceTypeIds.HasComponent;
+ tool.AddChild(rep);
+ rep.CreateOrReplaceStage(SystemContext, null!).Value = m_cellStage!.NodeId;
+ rep.CreateOrReplacePrimPath(SystemContext, null!).Value = s_robots[0].PrimPath + ToolSuffix;
+
+ NodeId newId = await Manager.AddNodeAsync(
+ SystemContext,
+ m_r1NodeId,
+ tool,
+ CancellationToken.None).ConfigureAwait(false);
+ m_logger.AttachedGripperTool(newId);
+ return newId;
+ }
+
+ internal async ValueTask ConfigurePositioningAsync(PositioningServerContext context)
+ {
+ if (m_robotCell == null)
+ {
+ throw new ServiceResultException(
+ StatusCodes.BadInvalidState,
+ "RobotCell must exist before Positioning is configured.");
+ }
+
+ MobileRobotPositionProvider? provider = null;
+ for (int i = 0; i < context.GeoLocationProviders.Count; i++)
+ {
+ if (context.GeoLocationProviders[i] is MobileRobotPositionProvider candidate)
+ {
+ provider = candidate;
+ break;
+ }
+ }
+ if (provider == null)
+ {
+ throw new ServiceResultException(
+ StatusCodes.BadConfigurationError,
+ "MobileRobotPositionProvider is not registered.");
+ }
+
+ PositioningAddressSpaceBuilder builder = context.AddressSpace;
+ ushort rslNamespaceIndex = (ushort)Server.NamespaceUris.GetIndex(Opc.Ua.Rsl.Namespaces.RSL);
+ ushort gposNamespaceIndex = (ushort)Server.NamespaceUris.GetIndex(Opc.Ua.Gpos.Namespaces.GPOS);
+ ushort usdNamespaceIndex = (ushort)Server.NamespaceUris.GetIndex(Opc.Ua.OpenUsd.Namespaces.OpenUSD);
+ EUInformation metres = new("m", "metre", "http://www.opcfoundation.org/UA/units/un/cefact");
+ EUInformation degrees = new("deg", "degree", "http://www.opcfoundation.org/UA/units/un/cefact");
+
+ SpatialObjectsListState list = builder.CreateSpatialObjectsList(
+ m_robotCell,
+ new QualifiedName("RobotCellFrames", rslNamespaceIndex),
+ "RobotCell",
+ CreateZeroFrame(),
+ metres,
+ degrees);
+ await builder.RegisterAsync(list, context.CancellationToken).ConfigureAwait(false);
+
+ ZoneState zone = builder.CreateZone(
+ new QualifiedName("RobotCellZone", gposNamespaceIndex),
+ RobotPositioningScenario.ZoneId,
+ provider.Scenario.GroundControlPoints);
+ await builder.RegisterAsync(zone, context.CancellationToken).ConfigureAwait(false);
+
+ foreach (RobotRuntime runtime in m_robots)
+ {
+ await ConfigureRobotPositioningAsync(
+ context,
+ builder,
+ provider,
+ runtime,
+ list,
+ zone,
+ metres,
+ degrees,
+ rslNamespaceIndex,
+ gposNamespaceIndex,
+ usdNamespaceIndex).ConfigureAwait(false);
+ }
+
+ m_logger.PositioningConfigured(m_robots.Count);
+ }
+
+ private async ValueTask ConfigureRobotPositioningAsync(
+ PositioningServerContext context,
+ PositioningAddressSpaceBuilder builder,
+ MobileRobotPositionProvider provider,
+ RobotRuntime runtime,
+ SpatialObjectsListState list,
+ ZoneState zone,
+ EUInformation metres,
+ EUInformation degrees,
+ ushort rslNamespaceIndex,
+ ushort gposNamespaceIndex,
+ ushort usdNamespaceIndex)
+ {
+ GeoLocationSample initial = await provider.ReadAsync(
+ runtime.SourceId,
+ context.CancellationToken).ConfigureAwait(false);
+ GeoPosition initialPosition = initial.Position!.Value;
+ GeoOrientation initialOrientation = initial.Orientation!.Value;
+ ThreeDCartesianCoordinates localPosition =
+ provider.Scenario.Fit.GlobalToLocal(
+ ToGeographic(initialPosition),
+ AngleUnit.Degrees);
+ var localFrame = new ThreeDFrame
+ {
+ CartesianCoordinates = localPosition,
+ Orientation = new ThreeDOrientation
+ {
+ A = initialOrientation.A,
+ B = initialOrientation.B,
+ C = initialOrientation.C
+ }
+ };
+
+ SpatialObjectState spatialObject = builder.AttachSpatialObject(
+ runtime.Robot,
+ list,
+ new QualifiedName("SpatialObject", rslNamespaceIndex),
+ runtime.SourceId,
+ localFrame,
+ metres,
+ degrees);
+ var positionFrame = (CartesianFrameAngleOrientationState)spatialObject.PositionFrame!;
+ if (runtime.SourceId == "R1")
+ {
+ _ = builder.AddAttachPoint(
+ spatialObject,
+ new QualifiedName("ToolFlange", rslNamespaceIndex),
+ positionFrame.NodeId,
+ CreateZeroFrame(),
+ metres,
+ degrees);
+ }
+ await builder.RegisterAsync(spatialObject, context.CancellationToken).ConfigureAwait(false);
+
+ GlobalLocationState globalLocation = builder.AttachGlobalLocation(
+ runtime.Robot,
+ new QualifiedName("GlobalLocation", gposNamespaceIndex),
+ zone.NodeId,
+ 4326);
+ globalLocation.Position!.AddElevationReference(SystemContext);
+ globalLocation.Position.ElevationReference!.Value = 1;
+ globalLocation.Orientation!.AddAngleUnit(SystemContext);
+ globalLocation.Orientation!.AngleUnit!.Value = degrees;
+
+ PositioningProviderSubscription subscription =
+ await builder.BindGlobalLocationAsync(
+ globalLocation,
+ provider,
+ runtime.SourceId,
+ (sample, _) =>
+ {
+ GeoPosition samplePosition = sample.Position!.Value;
+ GeoOrientation sampleOrientation = sample.Orientation!.Value;
+ ThreeDCartesianCoordinates local =
+ provider.Scenario.Fit.GlobalToLocal(
+ ToGeographic(samplePosition),
+ AngleUnit.Degrees);
+ builder.SetFrameValue(
+ positionFrame,
+ new ThreeDFrame
+ {
+ CartesianCoordinates = local,
+ Orientation = new ThreeDOrientation
+ {
+ A = sampleOrientation.A,
+ B = sampleOrientation.B,
+ C = sampleOrientation.C
+ }
+ },
+ sample.StatusCode,
+ sample.GetEffectiveSourceTimestamp());
+ return default;
+ },
+ context.CancellationToken).ConfigureAwait(false);
+ m_positioningSubscriptions.Add(subscription);
+ await builder.RegisterAsync(globalLocation, context.CancellationToken).ConfigureAwait(false);
+
+ OpenUsdLiveBindingState[] bindings =
+ [
+ CreateBinding(runtime.Representation, usdNamespaceIndex, $"{runtime.SourceId}Position",
+ GuidFor($"{runtime.SourceId}:position"), positionFrame.Position!.NodeId,
+ runtime.PrimPath, "xformOp:translate", "double3", OpenUsdRenderTargetKindEnum.Translation, 1.0),
+ CreateBinding(runtime.Representation, usdNamespaceIndex, $"{runtime.SourceId}Orientation",
+ GuidFor($"{runtime.SourceId}:orientation"), positionFrame.Orientation!.NodeId,
+ runtime.PrimPath, "xformOp:rotateXYZ", "double3", OpenUsdRenderTargetKindEnum.Rotation, 1.0),
+ CreateBinding(runtime.Representation, usdNamespaceIndex, $"{runtime.SourceId}Longitude",
+ GuidFor($"{runtime.SourceId}:longitude"), globalLocation.Position.Longitude!.NodeId,
+ runtime.PrimPath, "inputs:longitude", "double", OpenUsdRenderTargetKindEnum.Custom, 1.0),
+ CreateBinding(runtime.Representation, usdNamespaceIndex, $"{runtime.SourceId}Latitude",
+ GuidFor($"{runtime.SourceId}:latitude"), globalLocation.Position.Latitude!.NodeId,
+ runtime.PrimPath, "inputs:latitude", "double", OpenUsdRenderTargetKindEnum.Custom, 1.0),
+ CreateBinding(runtime.Representation, usdNamespaceIndex, $"{runtime.SourceId}Elevation",
+ GuidFor($"{runtime.SourceId}:elevation"), globalLocation.Position.Elevation!.NodeId,
+ runtime.PrimPath, "inputs:elevation", "double", OpenUsdRenderTargetKindEnum.Custom, 1.0)
+ ];
+
+ foreach (OpenUsdLiveBindingState binding in bindings)
+ {
+ runtime.Representation.RemoveChild(binding);
+ _ = await Manager.AddNodeAsync(
+ SystemContext,
+ runtime.Representation.NodeId,
+ binding,
+ context.CancellationToken).ConfigureAwait(false);
+ }
+ }
+
+ private static ThreeDFrame CreateZeroFrame()
+ {
+ return new ThreeDFrame
+ {
+ CartesianCoordinates = new ThreeDCartesianCoordinates(),
+ Orientation = new ThreeDOrientation()
+ };
+ }
+
+ ///
+ /// Rebuilds the OPC 10000-211 geographic coordinate a provider position
+ /// describes, so the scenario's fit can project it back into the cell's
+ /// local frame.
+ ///
+ private static S3DGeographicCoordinateDataType ToGeographic(GeoPosition position)
+ {
+ return new S3DGeographicCoordinateDataType
+ {
+ EncodingMask = position.Height.HasValue
+ ? (uint)S3DGeographicCoordinateDataTypeFields.Elevation
+ : 0,
+ Latitude = position.Latitude,
+ Longitude = position.Longitude,
+ Elevation = position.Height ?? 0.0
+ };
+ }
+
+ partial void Configure(INodeManagerBuilder builder);
+
+ ///
+ public void Dispose()
+ {
+ foreach (PositioningProviderSubscription subscription in m_positioningSubscriptions)
+ {
+ subscription.Dispose();
+ }
+ m_positioningSubscriptions.Clear();
+ }
+
+ private static Guid GuidFor(string key)
+ {
+ byte[] hash;
+#pragma warning disable CA1850 // Prefer static HashData (net48/netstandard2.0 compatibility)
+ using (var sha = System.Security.Cryptography.SHA256.Create())
+ {
+ hash = sha.ComputeHash(System.Text.Encoding.UTF8.GetBytes("robotics:" + key));
+ }
+#pragma warning restore CA1850
+ byte[] g = new byte[16];
+ Array.Copy(hash, g, 16);
+ return new Guid(g);
+ }
+ }
+
+ internal static partial class RobotCellLog
+ {
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.RobotCell + 1,
+ Level = LogLevel.Information,
+ Message = "Materialised RobotCell ({RobotCount} robots, {AxisCount} axes, {RepCount} representations).")]
+ public static partial void MaterialisedRobotCell(
+ this ILogger logger, int robotCount, int axisCount, int repCount);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.RobotCell + 2,
+ Level = LogLevel.Information,
+ Message = "Dynamic composition: attached gripper tool (NodeId={NodeId}); model-change emitted.")]
+ public static partial void AttachedGripperTool(this ILogger logger, NodeId nodeId);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.RobotCell + 3,
+ Level = LogLevel.Information,
+ Message = "Configured RSL/GPOS positioning for {RobotCount} robots.")]
+ public static partial void PositioningConfigured(this ILogger logger, int robotCount);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.RobotCell + 5,
+ Level = LogLevel.Error,
+ Message = "Failed to materialise RobotCell.")]
+ public static partial void RobotCellFailed(this ILogger logger, Exception exception);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.RobotCell + 6,
+ Level = LogLevel.Information,
+ Message = "Dynamic composition mounted the gripper tool on R1.")]
+ public static partial void GripperToolMounted(this ILogger logger);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.RobotCell + 7,
+ Level = LogLevel.Warning,
+ Message = "Dynamic tool composition failed.")]
+ public static partial void DynamicToolFailed(this ILogger logger, Exception exception);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.RobotCell + 8,
+ Level = LogLevel.Information,
+ Message = "Dynamic composition: detached gripper tool (NodeId={NodeId}); model-change emitted.")]
+ public static partial void DetachedGripperTool(this ILogger logger, NodeId nodeId);
+
+ [LoggerMessage(EventId = MinimalRobotServerEventIds.RobotCell + 9,
+ Level = LogLevel.Information,
+ Message = "RobotCell configurator: address space ready ({NodeCount} runtime nodes).")]
+ public static partial void RoboticsAddressSpaceReady(this ILogger logger, int nodeCount);
+ }
+}
diff --git a/samples/MinimalRobotServer/RobotPositioningScenario.cs b/samples/MinimalRobotServer/RobotPositioningScenario.cs
new file mode 100644
index 0000000000..d52af159f0
--- /dev/null
+++ b/samples/MinimalRobotServer/RobotPositioningScenario.cs
@@ -0,0 +1,90 @@
+/* ========================================================================
+ * 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 Opc.Ua;
+using Opc.Ua.Gpos;
+using Opc.Ua.Positioning;
+
+namespace Robotics
+{
+ ///
+ /// Stable Zone control points and fitted local/global transform for the sample.
+ ///
+ public sealed class RobotPositioningScenario
+ {
+ public const string ZoneId = "RobotCellZone";
+
+ public RobotPositioningScenario()
+ {
+ var origin = new S3DGeographicCoordinateDataType
+ {
+ EncodingMask =
+ (uint)S3DGeographicCoordinateDataTypeFields.Elevation,
+ Longitude = 11.576124,
+ Latitude = 48.137154,
+ Elevation = 520.0
+ };
+ var plane = new LocalTangentPlane(origin, AngleUnit.Degrees);
+ var points = new List();
+ foreach (ThreeDCartesianCoordinates local in new[]
+ {
+ new ThreeDCartesianCoordinates { X = -5.0, Y = -5.0, Z = 0.0 },
+ new ThreeDCartesianCoordinates { X = 5.0, Y = -5.0, Z = 0.0 },
+ new ThreeDCartesianCoordinates { X = 5.0, Y = 5.0, Z = 0.0 },
+ new ThreeDCartesianCoordinates { X = -5.0, Y = 5.0, Z = 0.0 },
+ new ThreeDCartesianCoordinates { X = 0.0, Y = 0.0, Z = 2.0 }
+ })
+ {
+ points.Add(new GroundControlPointDataType
+ {
+ LocalPosition = local,
+ GlobalPosition = plane.EnuToGeographic(
+ local,
+ AngleUnit.Degrees,
+ includeElevation: true)
+ });
+ }
+
+ GroundControlPoints =
+ ArrayOf.Create(points.ToArray());
+ Fit = new GroundControlPointFitter().Fit(
+ GroundControlPoints,
+ new GroundControlPointFitOptions
+ {
+ Mode = GroundControlPointFitMode.Rigid,
+ AngleUnit = AngleUnit.Degrees
+ });
+ }
+
+ public ArrayOf GroundControlPoints { get; }
+
+ public GroundControlPointFitResult Fit { get; }
+ }
+}
diff --git a/samples/PumpDeviceIntegrationServer/Assets/Plant.usda b/samples/PumpDeviceIntegrationServer/Assets/Plant.usda
new file mode 100644
index 0000000000..631e7921d3
--- /dev/null
+++ b/samples/PumpDeviceIntegrationServer/Assets/Plant.usda
@@ -0,0 +1,1200 @@
+#usda 1.0
+(
+ doc = """Base pump asset for the OPC UA - OpenUSD Bindings pump end-to-end.
+
+ Authored by the '3D / USD asset author' actor (Annex B). It carries only
+ geometry and materials; it contains NO live values. The live OPC UA values
+ are composed on top by `live.usda` (see `stage.usda`), which a connector
+ authors at runtime. The prim paths and attribute names here are the contract
+ the OpenUSD bindings target:
+
+ /Plant/Pumps/P101/Impeller xformOp:rotateZ (MassFlow -> Rotation)
+ /Plant/Pumps/P101/Body primvars:displayColor(BearingTemperature -> DisplayColor)
+ /Plant/Pumps/P101/Body/Mat/Surface.inputs:diffuseColor
+ (BearingTemperature -> DisplayColor)
+ /Plant/Pumps/P101/StatusLight/Mat/Surface.inputs:emissiveColor
+ (DifferentialPressure -> EmissiveColor)
+ /Plant/Pumps/P101/StatusLight.visibility (alarm ActiveState -> Visibility)
+ /Plant/Pumps/P101/Impeller.inputs:speedSetpoint (opt-in command intent -> UA write)
+
+ MODEL
+ P101 is a horizontal long-coupled end-suction centrifugal pump built to
+ EN 733 (formerly DIN 24255), size 65-200 - the geometry follows the published
+ dimensions of the Grundfos NK 65-200 / KSB Etanorm 65-200 family, driven by an
+ IEC 160M motor, both bolted to a fabricated steel baseplate. Key dimensions
+ (metres): baseplate 1.80 x 0.46 x 0.12; shaft centreline 0.160 above the
+ baseplate (the IEC 160M frame number *is* the shaft height in mm); volute
+ casing 0.355 outer diameter x 0.110 wide; DN80 axial suction flange (OD 0.200)
+ and DN65 vertical discharge flange (OD 0.185) per EN 1092-2 PN16; 0.198
+ impeller with six backward-curved vanes; IEC 160M motor frame 0.254 outer
+ diameter x 0.615 long. Livery is KSB signal blue (RAL 5005) for the wetted
+ castings and RAL 7035 light grey for the motor.
+
+ Two deliberate departures from a real pump, both so the twin can be *seen*:
+ * The suction pipe is drawn as a short stub, leaving the casing eye open -
+ a cutaway, the way a trade-show display pump is presented - so the
+ rotating impeller is visible.
+ * The coupling guard is a cage (end rings plus longitudinal bars) rather
+ than a solid sheet-metal barrel, so the rotating coupling is visible.
+
+ The shaft of a real pump is horizontal, but the binding contract fixes the
+ driven operation as `xformOp:rotateZ`. `Impeller` therefore carries a static
+ `xformOp:rotateY = 90` *ahead of* `xformOp:rotateZ` in `xformOpOrder`, which
+ lays its local Z along the world shaft axis (X). The contract operation still
+ spins the shaft, and it turns the impeller and the coupling together because
+ both hang off the same rotating prim - as they do on the real machine.
+ """
+ defaultPrim = "Plant"
+ metersPerUnit = 1
+ upAxis = "Z"
+)
+
+def Xform "Plant"
+{
+ def Scope "Looks"
+ {
+ def Material "PumpBlue"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.02, 0.24, 0.60)
+ float inputs:metallic = 0.1
+ float inputs:roughness = 0.45
+ token outputs:surface
+ }
+ }
+
+ def Material "MotorGrey"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.60, 0.61, 0.60)
+ float inputs:metallic = 0.2
+ float inputs:roughness = 0.50
+ token outputs:surface
+ }
+ }
+
+ def Material "Galvanised"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.68, 0.70, 0.72)
+ float inputs:metallic = 0.8
+ float inputs:roughness = 0.35
+ token outputs:surface
+ }
+ }
+
+ def Material "DarkSteel"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.16, 0.17, 0.19)
+ float inputs:metallic = 0.6
+ float inputs:roughness = 0.55
+ token outputs:surface
+ }
+ }
+
+ def Material "Bronze"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.72, 0.53, 0.24)
+ float inputs:metallic = 0.9
+ float inputs:roughness = 0.30
+ token outputs:surface
+ }
+ }
+ }
+
+ def DistantLight "KeyLight"
+ {
+ float inputs:angle = 1.0
+ float inputs:intensity = 3200
+ color3f inputs:color = (1.0, 0.98, 0.94)
+ double3 xformOp:rotateXYZ = (-48, 0, 34)
+ uniform token[] xformOpOrder = ["xformOp:rotateXYZ"]
+ }
+
+ def DistantLight "FillLight"
+ {
+ float inputs:angle = 2.0
+ float inputs:intensity = 1100
+ color3f inputs:color = (0.82, 0.88, 1.0)
+ double3 xformOp:rotateXYZ = (-24, 0, -122)
+ uniform token[] xformOpOrder = ["xformOp:rotateXYZ"]
+ }
+
+ def DistantLight "AmbientLight"
+ {
+ float inputs:angle = 6.0
+ float inputs:intensity = 420
+ color3f inputs:color = (0.72, 0.78, 0.88)
+ double3 xformOp:rotateXYZ = (-78, 0, 180)
+ uniform token[] xformOpOrder = ["xformOp:rotateXYZ"]
+ }
+
+ # Three-quarter view along the pump line. A horizontal end-suction pump is
+ # all profile, so an overhead camera shows little but baseplate; this one
+ # looks down the line from the front-right and slightly above, which reads
+ # the volute, the coupling guard and the motor. Hosts start on it by prim
+ # path (the connector's --camera option).
+ def Camera "HeroCamera"
+ {
+ token projection = "perspective"
+ float focalLength = 24
+ float horizontalAperture = 20.955
+ float verticalAperture = 13.97
+ float2 clippingRange = (0.1, 200)
+ double3 xformOp:translate = (6.0, -5.94, 3.77)
+ double3 xformOp:rotateXYZ = (70, 0, 41)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:rotateXYZ"]
+ }
+
+ def Mesh "Floor"
+ {
+ int[] faceVertexCounts = [4]
+ int[] faceVertexIndices = [0, 1, 2, 3]
+ point3f[] points = [
+ (-4, -3.4, 0), (7, -3.4, 0), (7, 4.2, 0), (-4, 4.2, 0)
+ ]
+ normal3f[] primvars:normals = [
+ (0, 0, 1), (0, 0, 1), (0, 0, 1), (0, 0, 1)
+ ] (
+ interpolation = "faceVarying"
+ )
+ color3f[] primvars:displayColor = [(0.34, 0.34, 0.35)]
+ }
+
+ def Scope "Pumps"
+ {
+ def Xform "P101" (
+ kind = "component"
+ )
+ {
+ double3 xformOp:translate = (0, 0, 0)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+
+ def Xform "Baseplate"
+ {
+ def Cube "Plate" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.20, 0.21, 0.23)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.08, 0, 0.06)
+ double3 xformOp:scale = (1.80, 0.46, 0.12)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "PumpPad" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.62, 0, 0.135)
+ double3 xformOp:scale = (0.44, 0.40, 0.03)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "MotorRailA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (0.31, -0.105, 0.1365)
+ double3 xformOp:scale = (0.62, 0.07, 0.033)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "MotorRailB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (0.31, 0.105, 0.1365)
+ double3 xformOp:scale = (0.62, 0.07, 0.033)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "LiftingLugA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.24, 0.25, 0.27)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.86, -0.24, 0.10)
+ double3 xformOp:scale = (0.10, 0.014, 0.05)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "LiftingLugB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.24, 0.25, 0.27)]
+ rel material:binding =
+ double3 xformOp:translate = (0.70, 0.24, 0.10)
+ double3 xformOp:scale = (0.10, 0.014, 0.05)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Cylinder "Body" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.110
+ double radius = 0.1775
+ color3f[] primvars:displayColor = [(0.02, 0.24, 0.60)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+
+ def Material "Mat"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.02, 0.24, 0.60)
+ float inputs:metallic = 0.1
+ float inputs:roughness = 0.45
+ token outputs:surface
+ }
+ }
+ }
+
+ def Xform "Casing"
+ {
+ def Cylinder "VoluteShoulder" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.070
+ double radius = 0.196
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.014, 0.293)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "Foot" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.1875)
+ double3 xformOp:scale = (0.11, 0.30, 0.135)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootPad" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.02, 0.20, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.159)
+ double3 xformOp:scale = (0.16, 0.36, 0.018)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "DrainPlug" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.022
+ double radius = 0.016
+ color3f[] primvars:displayColor = [(0.72, 0.53, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.700, 0, 0.115)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Xform "Suction"
+ {
+ def Cylinder "Flange" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.020
+ double radius = 0.100
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Neck" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.055
+ double radius = 0.051
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.822, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.360)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltBottom" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.200)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, -0.080, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0.080, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Xform "Discharge"
+ {
+ def Cylinder "Neck" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.150
+ double radius = 0.043
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.455)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Flange" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.018
+ double radius = 0.0925
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltFront" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.5725, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltBack" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.7175, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.0725, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, -0.0725, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Xform "Gauge"
+ {
+ def Cylinder "Stem" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.055
+ double radius = 0.007
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.070, 0.470)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Dial" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.022
+ double radius = 0.041
+ color3f[] primvars:displayColor = [(0.90, 0.90, 0.88)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.108, 0.470)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+ }
+
+ def Xform "PowerEnd"
+ {
+ def Cylinder "SealHousing" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.060
+ double radius = 0.046
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.560, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Lantern" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.040
+ double radius = 0.058
+ color3f[] primvars:displayColor = [(0.02, 0.20, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.510, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Bracket" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.220
+ double radius = 0.0625
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.380, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "BracketFoot" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.02, 0.20, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.360, 0, 0.1875)
+ double3 xformOp:scale = (0.16, 0.20, 0.105)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "SightGlass" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.020
+ double radius = 0.015
+ color3f[] primvars:displayColor = [(0.85, 0.86, 0.62)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.330, 0.064, 0.245)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Cylinder "Bearing" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.036
+ double radius = 0.052
+ color3f[] primvars:displayColor = [(0.30, 0.31, 0.33)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.300, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Xform "Impeller"
+ {
+ double3 xformOp:translate = (0, 0, 0.28)
+ double xformOp:rotateY = 90
+ double xformOp:rotateZ = 0
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateY", "xformOp:rotateZ"
+ ]
+
+ custom double inputs:speedSetpoint = 0
+
+ def Cylinder "Shaft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.760
+ double radius = 0.019
+ color3f[] primvars:displayColor = [(0.62, 0.63, 0.65)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.300)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Shroud" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.012
+ double radius = 0.099
+ color3f[] primvars:displayColor = [(0.66, 0.48, 0.22)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.664)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Hub" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.034
+ double radius = 0.030
+ color3f[] primvars:displayColor = [(0.66, 0.48, 0.22)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.645)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "VaneA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.052, 0.014, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 22)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.014, 0.052, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 82)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneC" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.038, 0.038, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 142)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneD" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.052, -0.014, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 202)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneE" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.014, -0.052, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 262)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneF" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.038, -0.038, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 322)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cylinder "CouplingHubPump" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.052
+ double radius = 0.070
+ color3f[] primvars:displayColor = [(0.34, 0.35, 0.37)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.196)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "CouplingSpider" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.014
+ double radius = 0.058
+ color3f[] primvars:displayColor = [(0.92, 0.66, 0.06)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.163)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "CouplingHubMotor" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.052
+ double radius = 0.070
+ color3f[] primvars:displayColor = [(0.34, 0.35, 0.37)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.130)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "CouplingKey" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.94, 0.94, 0.95)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.168)
+ double3 xformOp:scale = (0.020, 0.140, 0.010)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Xform "CouplingGuard"
+ {
+ def Cylinder "RingPump" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.012
+ double radius = 0.110
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.264, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "RingMotor" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.012
+ double radius = 0.110
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.006, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "BarTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0, 0.388)
+ double3 xformOp:scale = (0.264, 0.030, 0.008)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarBottom" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0, 0.172)
+ double3 xformOp:scale = (0.264, 0.030, 0.008)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, -0.108, 0.28)
+ double3 xformOp:scale = (0.264, 0.008, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0.108, 0.28)
+ double3 xformOp:scale = (0.264, 0.008, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Xform "Motor"
+ {
+ def Cylinder "Frame" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.615
+ double radius = 0.127
+ color3f[] primvars:displayColor = [(0.60, 0.61, 0.60)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "DriveEndShield" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.050
+ double radius = 0.132
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.030, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "NonDriveEndShield" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.050
+ double radius = 0.132
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.595, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "FanCowl" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.080
+ double radius = 0.140
+ color3f[] primvars:displayColor = [(0.48, 0.49, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (0.660, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "FinTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0, 0.412)
+ double3 xformOp:scale = (0.615, 0.030, 0.024)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FinUpperLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, -0.098, 0.372)
+ double3 xformOp:rotateXYZ = (40, 0, 0)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "FinUpperRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0.098, 0.372)
+ double3 xformOp:rotateXYZ = (-40, 0, 0)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "FinLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, -0.132, 0.28)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FinRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0.132, 0.28)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "TerminalBox" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.52, 0.53, 0.52)]
+ rel material:binding =
+ double3 xformOp:translate = (0.230, 0, 0.452)
+ double3 xformOp:scale = (0.120, 0.120, 0.080)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "CableGland" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.040
+ double radius = 0.016
+ color3f[] primvars:displayColor = [(0.30, 0.31, 0.33)]
+ rel material:binding =
+ double3 xformOp:translate = (0.230, -0.078, 0.440)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "EyeBolt" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.046
+ double radius = 0.012
+ color3f[] primvars:displayColor = [(0.62, 0.63, 0.65)]
+ rel material:binding =
+ double3 xformOp:translate = (0.470, 0, 0.428)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "Nameplate" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.86, 0.87, 0.88)]
+ rel material:binding =
+ double3 xformOp:translate = (0.150, -0.129, 0.318)
+ double3 xformOp:scale = (0.090, 0.004, 0.055)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.310, -0.105, 0.179)
+ double3 xformOp:scale = (0.300, 0.044, 0.052)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.310, 0.105, 0.179)
+ double3 xformOp:scale = (0.300, 0.044, 0.052)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ # Beacon hardware. Permanently mounted on a real skid, so it sits
+ # outside the alarm-gated prim below - only the lamp itself is gated.
+ def Xform "Beacon"
+ {
+ double3 xformOp:translate = (0.10, -0.32, 0.12)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+
+ def Cylinder "Post" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.520
+ double radius = 0.016
+ color3f[] primvars:displayColor = [(0.30, 0.31, 0.33)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.260)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Housing" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.050
+ double radius = 0.050
+ color3f[] primvars:displayColor = [(0.20, 0.21, 0.23)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.545)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ # The lamp itself is permanently mounted; its emissive colour is
+ # driven by discharge pressure, so its glow tracks the duty point
+ # and shows at a glance that the pump is running. It borrows the
+ # material the emissive binding targets under StatusLight.
+ def Sphere "Lamp" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double radius = 0.050
+ color3f[] primvars:displayColor = [(0.90, 0.20, 0.16)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.585)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Xform "StatusLight"
+ {
+ double3 xformOp:translate = (0.10, -0.32, 0.12)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+
+ token visibility = "inherited"
+
+ # Alarm halo - only this is gated by the supervision alarm, so a
+ # cleared alarm leaves the beacon hardware and lamp on show.
+ def Sphere "Halo" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double radius = 0.085
+ color3f[] primvars:displayColor = [(1.0, 0.35, 0.10)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.585)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Material "Mat"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.1, 0.1, 0.1)
+ color3f inputs:emissiveColor = (0, 0, 0)
+ float inputs:roughness = 0.25
+ token outputs:surface
+ }
+ }
+ }
+ }
+ }
+
+ def Xform "Line1"
+ {
+ def Scope "Pumps"
+ {
+ def Xform "P_201"
+ {
+ double3 xformOp:translate = (0, 1.5, 0)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Xform "P_202"
+ {
+ double3 xformOp:translate = (0, 2.7, 0)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Xform "P_203"
+ {
+ double3 xformOp:translate = (0, 3.9, 0)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Xform "RemotePump"
+ {
+ double3 xformOp:translate = (0, -1.5, 0)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+}
diff --git a/samples/PumpDeviceIntegrationServer/Assets/generate_pump_assets.py b/samples/PumpDeviceIntegrationServer/Assets/generate_pump_assets.py
new file mode 100644
index 0000000000..c9a9c0db93
--- /dev/null
+++ b/samples/PumpDeviceIntegrationServer/Assets/generate_pump_assets.py
@@ -0,0 +1,143 @@
+"""Regenerates the referenced pump assets from the P101 master in Plant.usda.
+
+pump.usda and remote-pump.usda are the component assets a ProductionLine
+references once per aggregated pump. They must carry the same geometry as
+/Plant/Pumps/P101 so every pump in the line renders at full fidelity, but they
+have to be self-contained: material paths are rewritten to a local Looks scope.
+
+Run from the repository root:
+ python samples/PumpDeviceIntegrationServer/Assets/generate_pump_assets.py
+"""
+
+import pathlib
+import re
+
+HERE = pathlib.Path(__file__).parent
+PLANT = HERE / "Plant.usda"
+
+# The P101 children that make up the machine itself. Beacon and StatusLight are
+# plant-side signalling equipment, not part of the pump component asset.
+WANTED = [
+ "Baseplate",
+ "Body",
+ "Casing",
+ "Suction",
+ "Discharge",
+ "PowerEnd",
+ "Bearing",
+ "Impeller",
+ "CouplingGuard",
+ "Motor",
+]
+
+# KSB signal blue (RAL 5005) -> OEM moss green (RAL 6011), so the pump federated
+# from the remote server is obvious at a glance.
+GREEN = {
+ "(0.02, 0.24, 0.60)": "(0.24, 0.36, 0.20)",
+ "(0.02, 0.22, 0.55)": "(0.22, 0.33, 0.18)",
+ "(0.02, 0.20, 0.50)": "(0.20, 0.30, 0.16)",
+}
+
+
+def block(lines, start):
+ """Returns the line range of a brace-delimited prim block starting at `start`."""
+ depth = 0
+ index = start
+ seen = False
+ while index < len(lines):
+ depth += lines[index].count("{") - lines[index].count("}")
+ if "{" in lines[index]:
+ seen = True
+ if seen and depth == 0:
+ return start, index + 1
+ index += 1
+ raise ValueError(f"unterminated block at line {start}")
+
+
+def extract(text, name, indent):
+ """Extracts the named prim block at the given indent, dedented to column 4."""
+ lines = text.splitlines()
+ pattern = re.compile(rf'^{" " * indent}def \w+ "{re.escape(name)}"')
+ for index, line in enumerate(lines):
+ if pattern.match(line):
+ first, last = block(lines, index)
+ body = lines[first:last]
+ trim = indent - 4
+ return "\n".join(line[trim:] if line.startswith(" " * trim) else line
+ for line in body)
+ raise ValueError(f"prim {name!r} not found at indent {indent}")
+
+
+def looks(text):
+ """Extracts the shared Looks scope and rehomes it under /Pump."""
+ scope = extract(text, "Looks", 4)
+ return scope.replace(" once per aggregated pump
+ (spec 5.12, CompositionArc = Instance/Reference). The cross-server component
+ references an equivalent OEM asset (remote-pump.usda) on another server (5.14).
+
+ Generated by generate_pump_assets.py from the /Plant/Pumps/P101 master in
+ Plant.usda, so every aggregated pump renders at the same fidelity as the hero
+ pump: an EN 733 size 65-200 end-suction centrifugal pump (Grundfos NK /
+ KSB Etanorm proportions) driven by an IEC 160M motor on a 1.80 x 0.46 m
+ baseplate, shaft centreline 0.160 m above the baseplate. Do not edit by hand -
+ edit P101 and re-run the generator.
+
+ The plant-side signalling equipment (Beacon, StatusLight) is deliberately not
+ part of the component asset."""
+
+REMOTE_DOC = """OEM pump asset served by a *different* OPC UA server, referenced
+ across the federation boundary by the cross-server binding
+ (spec 5.14). Geometrically identical to pump.usda so the composed line reads
+ consistently, but wearing the OEM's own livery - moss green (RAL 6011)
+ castings instead of KSB signal blue.
+
+ Generated by generate_pump_assets.py from the /Plant/Pumps/P101 master in
+ Plant.usda. Do not edit by hand - edit P101 and re-run the generator."""
+
+
+def main():
+ plant = PLANT.read_text(encoding="utf-8")
+ (HERE / "pump.usda").write_text(
+ build(plant, PUMP_DOC, recolour=False), encoding="utf-8", newline="\r\n")
+ (HERE / "remote-pump.usda").write_text(
+ build(plant, REMOTE_DOC, recolour=True), encoding="utf-8", newline="\r\n")
+ print("wrote pump.usda and remote-pump.usda")
+
+
+if __name__ == "__main__":
+ main()
diff --git a/samples/PumpDeviceIntegrationServer/Assets/pump.usda b/samples/PumpDeviceIntegrationServer/Assets/pump.usda
new file mode 100644
index 0000000000..d39fb9c344
--- /dev/null
+++ b/samples/PumpDeviceIntegrationServer/Assets/pump.usda
@@ -0,0 +1,989 @@
+#usda 1.0
+(
+ doc = """Reusable single-pump USD asset for the OPC UA - OpenUSD Bindings
+ composition demo. A ProductionLine aggregates 1..n pumps by *referencing*
+ (and instancing) this asset's default prim once per aggregated pump
+ (spec 5.12, CompositionArc = Instance/Reference). The cross-server component
+ references an equivalent OEM asset (remote-pump.usda) on another server (5.14).
+
+ Generated by generate_pump_assets.py from the /Plant/Pumps/P101 master in
+ Plant.usda, so every aggregated pump renders at the same fidelity as the hero
+ pump: an EN 733 size 65-200 end-suction centrifugal pump (Grundfos NK /
+ KSB Etanorm proportions) driven by an IEC 160M motor on a 1.80 x 0.46 m
+ baseplate, shaft centreline 0.160 m above the baseplate. Do not edit by hand -
+ edit P101 and re-run the generator.
+
+ The plant-side signalling equipment (Beacon, StatusLight) is deliberately not
+ part of the component asset."""
+ defaultPrim = "Pump"
+ metersPerUnit = 1
+ upAxis = "Z"
+)
+
+def Xform "Pump" (
+ kind = "component"
+)
+{
+ def Scope "Looks"
+ {
+ def Material "PumpBlue"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.02, 0.24, 0.60)
+ float inputs:metallic = 0.1
+ float inputs:roughness = 0.45
+ token outputs:surface
+ }
+ }
+
+ def Material "MotorGrey"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.60, 0.61, 0.60)
+ float inputs:metallic = 0.2
+ float inputs:roughness = 0.50
+ token outputs:surface
+ }
+ }
+
+ def Material "Galvanised"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.68, 0.70, 0.72)
+ float inputs:metallic = 0.8
+ float inputs:roughness = 0.35
+ token outputs:surface
+ }
+ }
+
+ def Material "DarkSteel"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.16, 0.17, 0.19)
+ float inputs:metallic = 0.6
+ float inputs:roughness = 0.55
+ token outputs:surface
+ }
+ }
+
+ def Material "Bronze"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.72, 0.53, 0.24)
+ float inputs:metallic = 0.9
+ float inputs:roughness = 0.30
+ token outputs:surface
+ }
+ }
+ }
+
+ def Xform "Baseplate"
+ {
+ def Cube "Plate" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.20, 0.21, 0.23)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.08, 0, 0.06)
+ double3 xformOp:scale = (1.80, 0.46, 0.12)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "PumpPad" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.62, 0, 0.135)
+ double3 xformOp:scale = (0.44, 0.40, 0.03)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "MotorRailA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (0.31, -0.105, 0.1365)
+ double3 xformOp:scale = (0.62, 0.07, 0.033)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "MotorRailB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (0.31, 0.105, 0.1365)
+ double3 xformOp:scale = (0.62, 0.07, 0.033)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "LiftingLugA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.24, 0.25, 0.27)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.86, -0.24, 0.10)
+ double3 xformOp:scale = (0.10, 0.014, 0.05)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "LiftingLugB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.24, 0.25, 0.27)]
+ rel material:binding =
+ double3 xformOp:translate = (0.70, 0.24, 0.10)
+ double3 xformOp:scale = (0.10, 0.014, 0.05)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Cylinder "Body" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.110
+ double radius = 0.1775
+ color3f[] primvars:displayColor = [(0.02, 0.24, 0.60)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+
+ def Material "Mat"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.02, 0.24, 0.60)
+ float inputs:metallic = 0.1
+ float inputs:roughness = 0.45
+ token outputs:surface
+ }
+ }
+ }
+
+ def Xform "Casing"
+ {
+ def Cylinder "VoluteShoulder" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.070
+ double radius = 0.196
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.014, 0.293)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "Foot" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.1875)
+ double3 xformOp:scale = (0.11, 0.30, 0.135)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootPad" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.02, 0.20, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.159)
+ double3 xformOp:scale = (0.16, 0.36, 0.018)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "DrainPlug" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.022
+ double radius = 0.016
+ color3f[] primvars:displayColor = [(0.72, 0.53, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.700, 0, 0.115)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Xform "Suction"
+ {
+ def Cylinder "Flange" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.020
+ double radius = 0.100
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Neck" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.055
+ double radius = 0.051
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.822, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.360)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltBottom" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.200)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, -0.080, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0.080, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Xform "Discharge"
+ {
+ def Cylinder "Neck" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.150
+ double radius = 0.043
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.455)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Flange" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.018
+ double radius = 0.0925
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltFront" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.5725, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltBack" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.7175, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.0725, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, -0.0725, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Xform "Gauge"
+ {
+ def Cylinder "Stem" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.055
+ double radius = 0.007
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.070, 0.470)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Dial" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.022
+ double radius = 0.041
+ color3f[] primvars:displayColor = [(0.90, 0.90, 0.88)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.108, 0.470)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+ }
+
+ def Xform "PowerEnd"
+ {
+ def Cylinder "SealHousing" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.060
+ double radius = 0.046
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.560, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Lantern" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.040
+ double radius = 0.058
+ color3f[] primvars:displayColor = [(0.02, 0.20, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.510, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Bracket" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.220
+ double radius = 0.0625
+ color3f[] primvars:displayColor = [(0.02, 0.22, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.380, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "BracketFoot" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.02, 0.20, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.360, 0, 0.1875)
+ double3 xformOp:scale = (0.16, 0.20, 0.105)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "SightGlass" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.020
+ double radius = 0.015
+ color3f[] primvars:displayColor = [(0.85, 0.86, 0.62)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.330, 0.064, 0.245)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Cylinder "Bearing" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.036
+ double radius = 0.052
+ color3f[] primvars:displayColor = [(0.30, 0.31, 0.33)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.300, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Xform "Impeller"
+ {
+ double3 xformOp:translate = (0, 0, 0.28)
+ double xformOp:rotateY = 90
+ double xformOp:rotateZ = 0
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateY", "xformOp:rotateZ"
+ ]
+
+ custom double inputs:speedSetpoint = 0
+
+ def Cylinder "Shaft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.760
+ double radius = 0.019
+ color3f[] primvars:displayColor = [(0.62, 0.63, 0.65)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.300)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Shroud" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.012
+ double radius = 0.099
+ color3f[] primvars:displayColor = [(0.66, 0.48, 0.22)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.664)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Hub" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.034
+ double radius = 0.030
+ color3f[] primvars:displayColor = [(0.66, 0.48, 0.22)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.645)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "VaneA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.052, 0.014, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 22)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.014, 0.052, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 82)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneC" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.038, 0.038, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 142)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneD" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.052, -0.014, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 202)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneE" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.014, -0.052, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 262)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneF" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.038, -0.038, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 322)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cylinder "CouplingHubPump" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.052
+ double radius = 0.070
+ color3f[] primvars:displayColor = [(0.34, 0.35, 0.37)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.196)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "CouplingSpider" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.014
+ double radius = 0.058
+ color3f[] primvars:displayColor = [(0.92, 0.66, 0.06)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.163)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "CouplingHubMotor" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.052
+ double radius = 0.070
+ color3f[] primvars:displayColor = [(0.34, 0.35, 0.37)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.130)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "CouplingKey" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.94, 0.94, 0.95)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.168)
+ double3 xformOp:scale = (0.020, 0.140, 0.010)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Xform "CouplingGuard"
+ {
+ def Cylinder "RingPump" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.012
+ double radius = 0.110
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.264, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "RingMotor" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.012
+ double radius = 0.110
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.006, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "BarTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0, 0.388)
+ double3 xformOp:scale = (0.264, 0.030, 0.008)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarBottom" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0, 0.172)
+ double3 xformOp:scale = (0.264, 0.030, 0.008)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, -0.108, 0.28)
+ double3 xformOp:scale = (0.264, 0.008, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0.108, 0.28)
+ double3 xformOp:scale = (0.264, 0.008, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Xform "Motor"
+ {
+ def Cylinder "Frame" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.615
+ double radius = 0.127
+ color3f[] primvars:displayColor = [(0.60, 0.61, 0.60)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "DriveEndShield" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.050
+ double radius = 0.132
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.030, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "NonDriveEndShield" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.050
+ double radius = 0.132
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.595, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "FanCowl" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.080
+ double radius = 0.140
+ color3f[] primvars:displayColor = [(0.48, 0.49, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (0.660, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "FinTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0, 0.412)
+ double3 xformOp:scale = (0.615, 0.030, 0.024)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FinUpperLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, -0.098, 0.372)
+ double3 xformOp:rotateXYZ = (40, 0, 0)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "FinUpperRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0.098, 0.372)
+ double3 xformOp:rotateXYZ = (-40, 0, 0)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "FinLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, -0.132, 0.28)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FinRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0.132, 0.28)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "TerminalBox" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.52, 0.53, 0.52)]
+ rel material:binding =
+ double3 xformOp:translate = (0.230, 0, 0.452)
+ double3 xformOp:scale = (0.120, 0.120, 0.080)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "CableGland" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.040
+ double radius = 0.016
+ color3f[] primvars:displayColor = [(0.30, 0.31, 0.33)]
+ rel material:binding =
+ double3 xformOp:translate = (0.230, -0.078, 0.440)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "EyeBolt" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.046
+ double radius = 0.012
+ color3f[] primvars:displayColor = [(0.62, 0.63, 0.65)]
+ rel material:binding =
+ double3 xformOp:translate = (0.470, 0, 0.428)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "Nameplate" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.86, 0.87, 0.88)]
+ rel material:binding =
+ double3 xformOp:translate = (0.150, -0.129, 0.318)
+ double3 xformOp:scale = (0.090, 0.004, 0.055)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.310, -0.105, 0.179)
+ double3 xformOp:scale = (0.300, 0.044, 0.052)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.310, 0.105, 0.179)
+ double3 xformOp:scale = (0.300, 0.044, 0.052)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+}
diff --git a/samples/PumpDeviceIntegrationServer/Assets/remote-pump.usda b/samples/PumpDeviceIntegrationServer/Assets/remote-pump.usda
new file mode 100644
index 0000000000..cf57beaa41
--- /dev/null
+++ b/samples/PumpDeviceIntegrationServer/Assets/remote-pump.usda
@@ -0,0 +1,982 @@
+#usda 1.0
+(
+ doc = """OEM pump asset served by a *different* OPC UA server, referenced
+ across the federation boundary by the cross-server binding
+ (spec 5.14). Geometrically identical to pump.usda so the composed line reads
+ consistently, but wearing the OEM's own livery - moss green (RAL 6011)
+ castings instead of KSB signal blue.
+
+ Generated by generate_pump_assets.py from the /Plant/Pumps/P101 master in
+ Plant.usda. Do not edit by hand - edit P101 and re-run the generator."""
+ defaultPrim = "Pump"
+ metersPerUnit = 1
+ upAxis = "Z"
+)
+
+def Xform "Pump" (
+ kind = "component"
+)
+{
+ def Scope "Looks"
+ {
+ def Material "PumpBlue"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.24, 0.36, 0.20)
+ float inputs:metallic = 0.1
+ float inputs:roughness = 0.45
+ token outputs:surface
+ }
+ }
+
+ def Material "MotorGrey"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.60, 0.61, 0.60)
+ float inputs:metallic = 0.2
+ float inputs:roughness = 0.50
+ token outputs:surface
+ }
+ }
+
+ def Material "Galvanised"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.68, 0.70, 0.72)
+ float inputs:metallic = 0.8
+ float inputs:roughness = 0.35
+ token outputs:surface
+ }
+ }
+
+ def Material "DarkSteel"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.16, 0.17, 0.19)
+ float inputs:metallic = 0.6
+ float inputs:roughness = 0.55
+ token outputs:surface
+ }
+ }
+
+ def Material "Bronze"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.72, 0.53, 0.24)
+ float inputs:metallic = 0.9
+ float inputs:roughness = 0.30
+ token outputs:surface
+ }
+ }
+ }
+
+ def Xform "Baseplate"
+ {
+ def Cube "Plate" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.20, 0.21, 0.23)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.08, 0, 0.06)
+ double3 xformOp:scale = (1.80, 0.46, 0.12)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "PumpPad" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.62, 0, 0.135)
+ double3 xformOp:scale = (0.44, 0.40, 0.03)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "MotorRailA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (0.31, -0.105, 0.1365)
+ double3 xformOp:scale = (0.62, 0.07, 0.033)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "MotorRailB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.26, 0.27, 0.29)]
+ rel material:binding =
+ double3 xformOp:translate = (0.31, 0.105, 0.1365)
+ double3 xformOp:scale = (0.62, 0.07, 0.033)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "LiftingLugA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.24, 0.25, 0.27)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.86, -0.24, 0.10)
+ double3 xformOp:scale = (0.10, 0.014, 0.05)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "LiftingLugB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.24, 0.25, 0.27)]
+ rel material:binding =
+ double3 xformOp:translate = (0.70, 0.24, 0.10)
+ double3 xformOp:scale = (0.10, 0.014, 0.05)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Cylinder "Body" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.110
+ double radius = 0.1775
+ color3f[] primvars:displayColor = [(0.24, 0.36, 0.20)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+
+ def Material "Mat"
+ {
+ token outputs:surface.connect =
+
+ def Shader "Surface"
+ {
+ uniform token info:id = "UsdPreviewSurface"
+ color3f inputs:diffuseColor = (0.24, 0.36, 0.20)
+ float inputs:metallic = 0.1
+ float inputs:roughness = 0.45
+ token outputs:surface
+ }
+ }
+ }
+
+ def Xform "Casing"
+ {
+ def Cylinder "VoluteShoulder" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.070
+ double radius = 0.196
+ color3f[] primvars:displayColor = [(0.22, 0.33, 0.18)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.014, 0.293)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "Foot" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.22, 0.33, 0.18)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.1875)
+ double3 xformOp:scale = (0.11, 0.30, 0.135)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootPad" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.20, 0.30, 0.16)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.159)
+ double3 xformOp:scale = (0.16, 0.36, 0.018)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "DrainPlug" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.022
+ double radius = 0.016
+ color3f[] primvars:displayColor = [(0.72, 0.53, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.700, 0, 0.115)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Xform "Suction"
+ {
+ def Cylinder "Flange" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.020
+ double radius = 0.100
+ color3f[] primvars:displayColor = [(0.22, 0.33, 0.18)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Neck" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.055
+ double radius = 0.051
+ color3f[] primvars:displayColor = [(0.22, 0.33, 0.18)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.822, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.360)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltBottom" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0, 0.200)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, -0.080, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.034
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.860, 0.080, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Xform "Discharge"
+ {
+ def Cylinder "Neck" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.150
+ double radius = 0.043
+ color3f[] primvars:displayColor = [(0.22, 0.33, 0.18)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.455)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Flange" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.018
+ double radius = 0.0925
+ color3f[] primvars:displayColor = [(0.22, 0.33, 0.18)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltFront" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.5725, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltBack" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.7175, 0, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.0725, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "BoltRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.032
+ double radius = 0.009
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, -0.0725, 0.539)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Xform "Gauge"
+ {
+ def Cylinder "Stem" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.055
+ double radius = 0.007
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.58)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.070, 0.470)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Dial" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.022
+ double radius = 0.041
+ color3f[] primvars:displayColor = [(0.90, 0.90, 0.88)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.645, 0.108, 0.470)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+ }
+
+ def Xform "PowerEnd"
+ {
+ def Cylinder "SealHousing" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.060
+ double radius = 0.046
+ color3f[] primvars:displayColor = [(0.22, 0.33, 0.18)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.560, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Lantern" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.040
+ double radius = 0.058
+ color3f[] primvars:displayColor = [(0.20, 0.30, 0.16)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.510, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Bracket" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.220
+ double radius = 0.0625
+ color3f[] primvars:displayColor = [(0.22, 0.33, 0.18)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.380, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "BracketFoot" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.20, 0.30, 0.16)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.360, 0, 0.1875)
+ double3 xformOp:scale = (0.16, 0.20, 0.105)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "SightGlass" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.020
+ double radius = 0.015
+ color3f[] primvars:displayColor = [(0.85, 0.86, 0.62)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.330, 0.064, 0.245)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+ }
+
+ def Cylinder "Bearing" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.036
+ double radius = 0.052
+ color3f[] primvars:displayColor = [(0.30, 0.31, 0.33)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.300, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Xform "Impeller"
+ {
+ double3 xformOp:translate = (0, 0, 0.28)
+ double xformOp:rotateY = 90
+ double xformOp:rotateZ = 0
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateY", "xformOp:rotateZ"
+ ]
+
+ custom double inputs:speedSetpoint = 0
+
+ def Cylinder "Shaft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.760
+ double radius = 0.019
+ color3f[] primvars:displayColor = [(0.62, 0.63, 0.65)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.300)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Shroud" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.012
+ double radius = 0.099
+ color3f[] primvars:displayColor = [(0.66, 0.48, 0.22)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.664)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "Hub" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.034
+ double radius = 0.030
+ color3f[] primvars:displayColor = [(0.66, 0.48, 0.22)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, 0.645)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "VaneA" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.052, 0.014, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 22)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneB" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.014, 0.052, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 82)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneC" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.038, 0.038, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 142)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneD" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.052, -0.014, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 202)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneE" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.014, -0.052, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 262)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "VaneF" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.70, 0.52, 0.24)]
+ rel material:binding =
+ double3 xformOp:translate = (0.038, -0.038, 0.646)
+ double3 xformOp:rotateXYZ = (0, 0, 322)
+ double3 xformOp:scale = (0.086, 0.010, 0.026)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cylinder "CouplingHubPump" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.052
+ double radius = 0.070
+ color3f[] primvars:displayColor = [(0.34, 0.35, 0.37)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.196)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "CouplingSpider" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.014
+ double radius = 0.058
+ color3f[] primvars:displayColor = [(0.92, 0.66, 0.06)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.163)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "CouplingHubMotor" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.052
+ double radius = 0.070
+ color3f[] primvars:displayColor = [(0.34, 0.35, 0.37)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.130)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "CouplingKey" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.94, 0.94, 0.95)]
+ rel material:binding =
+ double3 xformOp:translate = (0, 0, -0.168)
+ double3 xformOp:scale = (0.020, 0.140, 0.010)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Xform "CouplingGuard"
+ {
+ def Cylinder "RingPump" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.012
+ double radius = 0.110
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.264, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "RingMotor" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.012
+ double radius = 0.110
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.006, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "BarTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0, 0.388)
+ double3 xformOp:scale = (0.264, 0.030, 0.008)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarBottom" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0, 0.172)
+ double3 xformOp:scale = (0.264, 0.030, 0.008)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, -0.108, 0.28)
+ double3 xformOp:scale = (0.264, 0.008, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "BarRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.66, 0.68, 0.70)]
+ rel material:binding =
+ double3 xformOp:translate = (-0.135, 0.108, 0.28)
+ double3 xformOp:scale = (0.264, 0.008, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+
+ def Xform "Motor"
+ {
+ def Cylinder "Frame" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.615
+ double radius = 0.127
+ color3f[] primvars:displayColor = [(0.60, 0.61, 0.60)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "DriveEndShield" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.050
+ double radius = 0.132
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.030, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "NonDriveEndShield" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.050
+ double radius = 0.132
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.595, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "FanCowl" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "X"
+ double height = 0.080
+ double radius = 0.140
+ color3f[] primvars:displayColor = [(0.48, 0.49, 0.50)]
+ rel material:binding =
+ double3 xformOp:translate = (0.660, 0, 0.28)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "FinTop" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0, 0.412)
+ double3 xformOp:scale = (0.615, 0.030, 0.024)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FinUpperLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, -0.098, 0.372)
+ double3 xformOp:rotateXYZ = (40, 0, 0)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "FinUpperRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0.098, 0.372)
+ double3 xformOp:rotateXYZ = (-40, 0, 0)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = [
+ "xformOp:translate", "xformOp:rotateXYZ", "xformOp:scale"
+ ]
+ }
+
+ def Cube "FinLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, -0.132, 0.28)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FinRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.56, 0.57, 0.56)]
+ rel material:binding =
+ double3 xformOp:translate = (0.3125, 0.132, 0.28)
+ double3 xformOp:scale = (0.615, 0.024, 0.030)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "TerminalBox" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.52, 0.53, 0.52)]
+ rel material:binding =
+ double3 xformOp:translate = (0.230, 0, 0.452)
+ double3 xformOp:scale = (0.120, 0.120, 0.080)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cylinder "CableGland" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Y"
+ double height = 0.040
+ double radius = 0.016
+ color3f[] primvars:displayColor = [(0.30, 0.31, 0.33)]
+ rel material:binding =
+ double3 xformOp:translate = (0.230, -0.078, 0.440)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cylinder "EyeBolt" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ uniform token axis = "Z"
+ double height = 0.046
+ double radius = 0.012
+ color3f[] primvars:displayColor = [(0.62, 0.63, 0.65)]
+ rel material:binding =
+ double3 xformOp:translate = (0.470, 0, 0.428)
+ uniform token[] xformOpOrder = ["xformOp:translate"]
+ }
+
+ def Cube "Nameplate" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.86, 0.87, 0.88)]
+ rel material:binding =
+ double3 xformOp:translate = (0.150, -0.129, 0.318)
+ double3 xformOp:scale = (0.090, 0.004, 0.055)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootLeft" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.310, -0.105, 0.179)
+ double3 xformOp:scale = (0.300, 0.044, 0.052)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+
+ def Cube "FootRight" (
+ prepend apiSchemas = ["MaterialBindingAPI"]
+ )
+ {
+ double size = 1.0
+ color3f[] primvars:displayColor = [(0.55, 0.56, 0.55)]
+ rel material:binding =
+ double3 xformOp:translate = (0.310, 0.105, 0.179)
+ double3 xformOp:scale = (0.300, 0.044, 0.052)
+ uniform token[] xformOpOrder = ["xformOp:translate", "xformOp:scale"]
+ }
+ }
+}
diff --git a/samples/PumpDeviceIntegrationServer/EventIds.cs b/samples/PumpDeviceIntegrationServer/EventIds.cs
index fe39a29e99..aad4dd9e08 100644
--- a/samples/PumpDeviceIntegrationServer/EventIds.cs
+++ b/samples/PumpDeviceIntegrationServer/EventIds.cs
@@ -41,5 +41,7 @@ namespace Pumps
internal static class PumpDeviceIntegrationServerEventIds
{
public const int PumpNodeManager = 0;
+ public const int OpenUsdComposition = 10;
+ public const int OpenUsdRepresentation = 20;
}
}
diff --git a/samples/PumpDeviceIntegrationServer/OpenUsdComposition.cs b/samples/PumpDeviceIntegrationServer/OpenUsdComposition.cs
new file mode 100644
index 0000000000..02564eb07b
--- /dev/null
+++ b/samples/PumpDeviceIntegrationServer/OpenUsdComposition.cs
@@ -0,0 +1,292 @@
+/* ========================================================================
+ * 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.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Opc.Ua;
+using Opc.Ua.OpenUsd;
+using Opc.Ua.OpenUsd.Server;
+using Opc.Ua.Pumps;
+using Opc.Ua.Server;
+using Opc.Ua.Server.NodeManager;
+
+namespace Pumps
+{
+ ///
+ /// Wires the draft OPC UA โ OpenUSD Bindings composition/aggregation model
+ /// (spec ยง5.12โ5.14) onto the server: the pump composed 1:1 of Impeller and
+ /// Bearing component Objects (child prims), and a ProductionLine aggregating
+ /// 1..n pumps (Many, instanceable) with a dynamically added/removed pump
+ /// (model-change events) and a cross-server component (federation).
+ ///
+ public partial class PumpNodeManager
+ {
+ private BaseObjectState? m_productionLine;
+ private FolderState? m_linePumps;
+ private NodeId m_dynamicPumpNodeId;
+ private const string LinePrimPath = "/Plant/Line1";
+
+ // 1:1 (Child): create Impeller + Bearing component Objects on the pump, each
+ // with its own representation, and declare One bindings.
+ private void AttachPumpComponents(PumpState pump, OpenUsdRepresentationState pumpRep, ushort ns)
+ {
+ (BaseObjectState _, OpenUsdRepresentationState impellerRep) = CreateRepresentedComponent(
+ pump, "Impeller", pump.BrowseName.NamespaceIndex, "/Plant/Pumps/P101/Impeller", ns);
+ (BaseObjectState _, OpenUsdRepresentationState bearingRep) = CreateRepresentedComponent(
+ pump, "Bearing", pump.BrowseName.NamespaceIndex, "/Plant/Pumps/P101/Bearing", ns);
+
+ CreateComponentBinding(pumpRep, ns, "ImpellerComponent",
+ new Guid("a1b2c3d4-0001-4000-8000-000000000001"),
+ OpenUsdCardinalityEnum.One, OpenUsdCompositionArcEnum.Child,
+ "/Plant/Pumps/P101/Impeller", componentRepresentation: impellerRep.NodeId);
+ CreateComponentBinding(pumpRep, ns, "BearingComponent",
+ new Guid("a1b2c3d4-0001-4000-8000-000000000002"),
+ OpenUsdCardinalityEnum.One, OpenUsdCompositionArcEnum.Child,
+ "/Plant/Pumps/P101/Bearing", componentRepresentation: bearingRep.NodeId);
+ }
+
+ // 1..n + dynamic + cross-server: a ProductionLine aggregating pumps.
+ private async ValueTask MaterialiseProductionLineAsync(CancellationToken cancellationToken)
+ {
+ if (m_plantStage == null)
+ {
+ return;
+ }
+ try
+ {
+ ushort ns = (ushort)Server.NamespaceUris.GetIndex(Opc.Ua.OpenUsd.Namespaces.OpenUSD);
+ NodeState? deviceSet = PredefinedNodes.FindById(NodeId.Create(
+ Opc.Ua.Di.Objects.DeviceSet, DiNamespaceUri, Server.NamespaceUris));
+ if (deviceSet == null)
+ {
+ return;
+ }
+
+ var line = new BaseObjectState(deviceSet)
+ {
+ SymbolicName = "ProductionLine",
+ BrowseName = new QualifiedName("ProductionLine", ns),
+ DisplayName = new LocalizedText("ProductionLine"),
+ ReferenceTypeId = ReferenceTypeIds.HasComponent,
+ TypeDefinitionId = Opc.Ua.ObjectTypeIds.BaseObjectType
+ };
+ deviceSet.AddChild(line);
+ line.NodeId = SystemContext.NodeIdFactory.New(SystemContext, line);
+
+ OpenUsdRepresentationState lineRep = SystemContext.CreateInstanceOfOpenUsdRepresentationType(
+ line, new QualifiedName("OpenUsdRepresentation", ns));
+ lineRep.ReferenceTypeId = ReferenceTypeIds.HasComponent;
+ line.AddChild(lineRep);
+ lineRep.NodeId = SystemContext.NodeIdFactory.New(SystemContext, lineRep);
+ lineRep.CreateOrReplaceStage(SystemContext, null!).Value = m_plantStage.NodeId;
+ lineRep.CreateOrReplacePrimPath(SystemContext, null!).Value = LinePrimPath;
+
+ var pumps = new FolderState(line)
+ {
+ SymbolicName = "Pumps",
+ BrowseName = new QualifiedName("Pumps", ns),
+ DisplayName = new LocalizedText("Pumps"),
+ ReferenceTypeId = ReferenceTypeIds.Organizes,
+ TypeDefinitionId = Opc.Ua.ObjectTypeIds.FolderType
+ };
+ line.AddChild(pumps);
+ pumps.NodeId = SystemContext.NodeIdFactory.New(SystemContext, pumps);
+ m_linePumps = pumps;
+
+ // Two static aggregated pumps (1..n baseline).
+ CreateRepresentedComponent(pumps, "P-201", ns,
+ LinePrimPath + "/Pumps/P_201", ns, ReferenceTypeIds.Organizes);
+ CreateRepresentedComponent(pumps, "P-202", ns,
+ LinePrimPath + "/Pumps/P_202", ns, ReferenceTypeIds.Organizes);
+
+ // Many : aggregate the Pumps as instanceable references, dynamic.
+ CreateComponentBinding(lineRep, ns, "PumpsAggregation",
+ new Guid("a1b2c3d4-0002-4000-8000-000000000001"),
+ OpenUsdCardinalityEnum.Many, OpenUsdCompositionArcEnum.Instance,
+ LinePrimPath + "/Pumps",
+ assetReference: "@pump.usda@", dynamic: true,
+ changeEventSource: Opc.Ua.ObjectIds.Server);
+
+ // Cross-server : an OEM pump on another server (federation).
+ CreateComponentBinding(lineRep, ns, "RemotePumpComponent",
+ new Guid("a1b2c3d4-0003-4000-8000-000000000001"),
+ OpenUsdCardinalityEnum.One, OpenUsdCompositionArcEnum.Reference,
+ LinePrimPath + "/RemotePump",
+ assetReference: "@remote-pump.usda@",
+ componentServerUri: RemoteServerUri, componentEndpointUrl: RemoteEndpointUrl);
+
+ SystemContext.AssignInstanceChildNodeIds(line);
+ await AddPredefinedNodeAsync(SystemContext, line, cancellationToken).ConfigureAwait(false);
+
+ // Register the line representation in the discovery registry.
+ FolderState? registry = m_openUsdRoot?.Representations;
+ if (registry != null)
+ {
+ registry.AddReference(ReferenceTypeIds.Organizes, false, lineRep.NodeId);
+ lineRep.AddReference(ReferenceTypeIds.Organizes, true, registry.NodeId);
+ }
+
+ // Dynamic composition: emit model-change events on runtime add/remove.
+ ModelChangeEmissionEnabled = true;
+ _ = RunDynamicCompositionAsync(ns);
+
+ m_productionLine = line;
+ m_logger.LogInformation("Materialised ProductionLine (aggregates 1..n pumps).");
+ }
+ catch (Exception ex)
+ {
+ m_logger.LogError(ex, "Failed to materialise the ProductionLine.");
+ }
+ }
+
+ // The cross-server component points at this same server process (a loopback
+ // stand-in for an OEM sub-asset server); a test overrides RemoteEndpointUrl.
+ private static string RemoteServerUri => "urn:localhost:OPCFoundation:PumpDeviceIntegrationServer";
+ private string RemoteEndpointUrl { get; set; } =
+ "opc.tcp://localhost:62810/PumpDeviceIntegrationServer";
+
+ // Dynamic demo (ยง5.13): repeatedly add a pump (emits a GeneralModelChange),
+ // hold, then remove it (emits again), so a connector observes both the add and
+ // the remove regardless of when it connects. Bounded so the server quiesces.
+ private async Task RunDynamicCompositionAsync(ushort ns)
+ {
+ try
+ {
+ for (int i = 0; i < 3; i++)
+ {
+ await Task.Delay(4000).ConfigureAwait(false);
+ m_dynamicPumpNodeId = await AddLinePumpAsync("P-203",
+ LinePrimPath + "/Pumps/P_203", ns).ConfigureAwait(false);
+ await Task.Delay(4000).ConfigureAwait(false);
+ if (!m_dynamicPumpNodeId.IsNull)
+ {
+ await DeleteNodeAsync(SystemContext, m_dynamicPumpNodeId, CancellationToken.None)
+ .ConfigureAwait(false);
+ m_logger.RemovedLinePump(m_dynamicPumpNodeId);
+ m_dynamicPumpNodeId = NodeId.Null;
+ }
+ }
+ // Final add: leave P-203 in place so the composed stage renders it.
+ await Task.Delay(2000).ConfigureAwait(false);
+ m_dynamicPumpNodeId = await AddLinePumpAsync("P-203",
+ LinePrimPath + "/Pumps/P_203", ns).ConfigureAwait(false);
+ m_logger.LogInformation("Dynamic composition: P-203 left added (final state).");
+ }
+ catch (Exception ex)
+ {
+ m_logger.LogWarning(ex, "Dynamic composition demo failed.");
+ }
+ }
+
+ private async Task AddLinePumpAsync(string name, string primPath, ushort ns)
+ {
+ if (m_linePumps == null)
+ {
+ return NodeId.Null;
+ }
+ var pump = new BaseObjectState(null)
+ {
+ SymbolicName = name,
+ BrowseName = new QualifiedName(name, ns),
+ DisplayName = new LocalizedText(name),
+ TypeDefinitionId = Opc.Ua.ObjectTypeIds.BaseObjectType
+ };
+ OpenUsdRepresentationState rep = SystemContext.CreateInstanceOfOpenUsdRepresentationType(
+ pump, new QualifiedName("OpenUsdRepresentation", ns));
+ rep.ReferenceTypeId = ReferenceTypeIds.HasComponent;
+ pump.AddChild(rep);
+ rep.CreateOrReplaceStage(SystemContext, null!).Value = m_plantStage!.NodeId;
+ rep.CreateOrReplacePrimPath(SystemContext, null!).Value = primPath;
+
+ NodeId newId = await CreateNodeAsync(SystemContext, m_linePumps.NodeId,
+ ReferenceTypeIds.Organizes, new QualifiedName(name, ns), pump, CancellationToken.None)
+ .ConfigureAwait(false);
+ m_logger.AddedLinePump(name, newId);
+ return newId;
+ }
+
+ private (BaseObjectState, OpenUsdRepresentationState) CreateRepresentedComponent(
+ NodeState parent, string name, ushort objNs, string primPath, ushort openUsdNs,
+ NodeId refType = default)
+ {
+ var obj = new BaseObjectState(parent)
+ {
+ SymbolicName = name,
+ BrowseName = new QualifiedName(name, objNs),
+ DisplayName = new LocalizedText(name),
+ ReferenceTypeId = refType.IsNull ? ReferenceTypeIds.HasComponent : refType,
+ TypeDefinitionId = Opc.Ua.ObjectTypeIds.BaseObjectType
+ };
+ parent.AddChild(obj);
+ obj.NodeId = SystemContext.NodeIdFactory.New(SystemContext, obj);
+
+ OpenUsdRepresentationState rep = SystemContext.CreateInstanceOfOpenUsdRepresentationType(
+ obj, new QualifiedName("OpenUsdRepresentation", openUsdNs));
+ rep.ReferenceTypeId = ReferenceTypeIds.HasComponent;
+ obj.AddChild(rep);
+ rep.NodeId = SystemContext.NodeIdFactory.New(SystemContext, rep);
+ rep.CreateOrReplaceStage(SystemContext, null!).Value = m_plantStage!.NodeId;
+ rep.CreateOrReplacePrimPath(SystemContext, null!).Value = primPath;
+ SystemContext.AssignInstanceChildNodeIds(obj);
+ return (obj, rep);
+ }
+
+ // Thin adapter over the reusable Opc.Ua.OpenUsd.Server authoring API: the
+ // component/composition-binding logic lives in the SDK
+ // (OpenUsdRepresentationAuthoring.AddComponentBinding), not in this sample.
+ private void CreateComponentBinding(
+ OpenUsdRepresentationState rep, ushort ns, string name, Guid bindingDefinitionId,
+ OpenUsdCardinalityEnum cardinality, OpenUsdCompositionArcEnum arc, string targetPrimPath,
+ NodeId componentRepresentation = default, string? assetReference = null,
+ bool dynamic = false, NodeId changeEventSource = default,
+ string? componentServerUri = null, string? componentEndpointUrl = null,
+ NodeId componentTypeDefinition = default)
+ {
+ _ = rep.AddComponentBinding(
+ SystemContext, ns, name, bindingDefinitionId, cardinality, arc, targetPrimPath,
+ componentRepresentation, assetReference, dynamic, changeEventSource,
+ componentServerUri, componentEndpointUrl, componentTypeDefinition);
+ }
+ }
+
+ internal static partial class OpenUsdCompositionLog
+ {
+ [LoggerMessage(EventId = PumpDeviceIntegrationServerEventIds.OpenUsdComposition + 1,
+ Level = LogLevel.Information,
+ Message = "Dynamic composition: removed line pump (NodeId={NodeId}); model-change emitted.")]
+ public static partial void RemovedLinePump(this ILogger logger, NodeId nodeId);
+
+ [LoggerMessage(EventId = PumpDeviceIntegrationServerEventIds.OpenUsdComposition + 2,
+ Level = LogLevel.Information,
+ Message = "Dynamic composition: added line pump '{Name}' (NodeId={NodeId}); model-change emitted.")]
+ public static partial void AddedLinePump(this ILogger logger, string name, NodeId nodeId);
+ }
+}
diff --git a/samples/PumpDeviceIntegrationServer/OpenUsdRepresentation.cs b/samples/PumpDeviceIntegrationServer/OpenUsdRepresentation.cs
new file mode 100644
index 0000000000..8ded3ce907
--- /dev/null
+++ b/samples/PumpDeviceIntegrationServer/OpenUsdRepresentation.cs
@@ -0,0 +1,477 @@
+/* ========================================================================
+ * 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.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging;
+using Opc.Ua;
+using Opc.Ua.OpenUsd;
+using Opc.Ua.OpenUsd.Server;
+using Opc.Ua.Pumps;
+
+namespace Pumps
+{
+ ///
+ /// Wires the draft OPC UA โ OpenUSD Bindings companion model onto the Pumps
+ /// server: the well-known OpenUSD facility (stage + representation registries),
+ /// a PlantStage descriptor, and an OpenUsdRepresentation AddIn on Pump #1 with
+ /// three read-only live bindings (Part 2, UaToUsdTelemetry).
+ ///
+ public partial class PumpNodeManager
+ {
+ private OpenUsdRootState? m_openUsdRoot;
+ private OpenUsdStageState? m_plantStage;
+ private BaseDataVariableState? m_alarmActiveVar;
+ private BaseDataVariableState? m_speedSetpointVar;
+ private BaseDataVariableState? m_shaftAngleVar;
+
+ ///
+ /// Rate at which the server samples the hand-built OpenUSD signals, in
+ /// milliseconds. Matches the simulation tick so every change is observed.
+ ///
+ private const double SignalSamplingIntervalMilliseconds = 250;
+
+ ///
+ /// Registers the Variables that are hand-built onto the pump rather than
+ /// materialised by the generated factory. A generated instance state only
+ /// reports its declared members, so these children are never picked up by
+ /// the walk that AddPredefinedNodeAsync(pump) performs: they browse
+ /// and read correctly but are never sampled, which leaves any binding that
+ /// uses them frozen at its start-up value.
+ ///
+ private async ValueTask RegisterOpenUsdSignalsAsync(CancellationToken cancellationToken)
+ {
+ foreach (BaseDataVariableState? signal in new[]
+ {
+ m_alarmActiveVar, m_shaftAngleVar, m_speedSetpointVar
+ })
+ {
+ if (signal != null)
+ {
+ await AddPredefinedNodeAsync(SystemContext, signal, cancellationToken)
+ .ConfigureAwait(false);
+ }
+ }
+ // Push the shaft angle from the same loop that drives the generated
+ // measurement Variables, so it uses the one path proven to raise
+ // data-change notifications.
+ if (m_shaftAngleVar != null)
+ {
+ TrackSignal(m_shaftAngleVar, () => ShaftAngleDegrees);
+ }
+ }
+
+ private const string PlantRootLayerIdentifier = "asset-repo/Plant.usd";
+ private const string PumpPrimPath = "/Plant/Pumps/P101";
+
+ private async ValueTask MaterialiseOpenUsdFacilityAsync(
+ CancellationToken cancellationToken)
+ {
+ try
+ {
+ ushort ns = (ushort)Server.NamespaceUris.GetIndex(Opc.Ua.OpenUsd.Namespaces.OpenUSD);
+
+ OpenUsdRootState root = SystemContext.CreateInstanceOfOpenUsdRootType(
+ null!, new QualifiedName("OpenUSD", ns));
+ root.NodeId = new NodeId("OpenUSD", ns);
+
+ FolderState stages = root.Stages
+ ?? root.CreateOrReplaceStages(SystemContext, null!);
+ _ = root.Representations
+ ?? root.CreateOrReplaceRepresentations(SystemContext, null!);
+
+ m_plantStage = SystemContext.CreateInstanceOfOpenUsdStageType(
+ stages, new QualifiedName("PlantStage", ns));
+ stages.AddChild(m_plantStage);
+ m_plantStage.CreateOrReplaceRootLayerIdentifier(SystemContext, null!)
+ .Value = PlantRootLayerIdentifier;
+
+ // ยง5.2 Twin-BOM content integrity: the digest is computed over the
+ // *resolved root-layer content*, never over the identifier string, so a
+ // connector can detect tampering of the bytes it actually composes.
+ List servedAssets = LoadServedAssets();
+ byte[] rootLayerBytes = servedAssets
+ .Find(a => a.Kind == OpenUsdAssetKindEnum.RootLayer)!.Bytes;
+ byte[] digest;
+#pragma warning disable CA1850 // Prefer static HashData (net48/netstandard2.0 compatibility)
+ using (var sha = System.Security.Cryptography.SHA256.Create())
+ {
+ digest = sha.ComputeHash(rootLayerBytes);
+ }
+#pragma warning restore CA1850
+ m_plantStage.CreateOrReplaceRootLayerDigest(
+ SystemContext,
+ SystemContext.CreateOpenUsdStageType_RootLayerDigest(m_plantStage, forInstance: true))
+ .Value = (ByteString)digest;
+ m_plantStage.CreateOrReplaceRootLayerDigestAlgorithm(
+ SystemContext,
+ SystemContext.CreateOpenUsdStageType_RootLayerDigestAlgorithm(m_plantStage, forInstance: true))
+ .Value = OpenUsdDigestAlgorithmEnum.Sha256;
+
+ // Per the companion spec ยง4.2 the OpenUSD facility SHALL be a
+ // component of the Server Object (i=2253) so that any conformant
+ // connector can Browse Server -> OpenUSD -> Representations. Record
+ // the inverse reference here; the matching forward reference from
+ // the Server Object (owned by the core node manager) is added via
+ // the externalReferences dictionary in LinkOpenUsdRootToServer.
+ root.AddReference(ReferenceTypeIds.HasComponent, true, Opc.Ua.ObjectIds.Server);
+
+ // ยง5.15 asset content delivery (OU-AssetDelivery): serve this stage's
+ // artist-authored USD layer closure so a connector can render the twin
+ // with no external asset resolver.
+ UsdAssetDelivery.AttachStageAssets(SystemContext, m_plantStage, ns, servedAssets);
+
+ SystemContext.AssignInstanceChildNodeIds(root);
+ await AddPredefinedNodeAsync(SystemContext, root, cancellationToken)
+ .ConfigureAwait(false);
+
+ m_openUsdRoot = root;
+ m_logger.MaterialisedOpenUsdFacility(root.NodeId, m_plantStage.NodeId);
+ }
+ catch (Exception ex)
+ {
+ m_plantStage = null;
+ m_openUsdRoot = null;
+ m_logger.LogError(ex, "Failed to materialise the OpenUSD facility.");
+ }
+ }
+
+ // Loads the embedded artist-authored USD layers this server serves (spec ยง5.15).
+ private static List LoadServedAssets()
+ {
+ return new List
+ {
+ new ServedAsset("Plant.usda", OpenUsdAssetKindEnum.RootLayer, ReadEmbeddedAsset("Plant.usda")),
+ new ServedAsset("pump.usda", OpenUsdAssetKindEnum.Reference, ReadEmbeddedAsset("pump.usda")),
+ new ServedAsset("remote-pump.usda", OpenUsdAssetKindEnum.Reference, ReadEmbeddedAsset("remote-pump.usda")),
+ };
+ }
+
+ private static byte[] ReadEmbeddedAsset(string resourceName)
+ {
+ using Stream? s = typeof(PumpNodeManager).Assembly.GetManifestResourceStream(resourceName);
+ if (s == null)
+ {
+ return Array.Empty();
+ }
+ using var ms = new MemoryStream();
+ s.CopyTo(ms);
+ return ms.ToArray();
+ }
+
+ ///
+ public override async ValueTask CreateAddressSpaceAsync(
+ IDictionary> externalReferences,
+ CancellationToken cancellationToken = default)
+ {
+ // base.CreateAddressSpaceAsync loads predefined nodes and runs
+ // OnAddressSpaceReadyAsync, which materialises the OpenUSD facility.
+ await base.CreateAddressSpaceAsync(externalReferences, cancellationToken)
+ .ConfigureAwait(false);
+
+ // Now that the root exists, add the forward HasComponent reference from
+ // the Server Object (i=2253, owned by the core node manager) to it.
+ LinkOpenUsdRootToServer(externalReferences);
+ }
+
+ // Adds the Server -> OpenUSD forward reference into the externalReferences
+ // dictionary the master node manager applies across managers, so the
+ // well-known facility is browsable from the Server Object (spec ยง4.2).
+ private void LinkOpenUsdRootToServer(
+ IDictionary> externalReferences)
+ {
+ if (m_openUsdRoot == null)
+ {
+ return;
+ }
+ if (!externalReferences.TryGetValue(Opc.Ua.ObjectIds.Server, out IList? references)
+ || references == null)
+ {
+ externalReferences[Opc.Ua.ObjectIds.Server] = references = new List();
+ }
+ references.Add(new NodeStateReference(
+ ReferenceTypeIds.HasComponent, false, m_openUsdRoot.NodeId));
+ m_logger.LogInformation(
+ "Linked OpenUSD facility under the Server Object (i=2253).");
+ }
+
+ // Call before AddPredefinedNodeAsync(pump), so the binding source
+ // NodeIds captured here are the per-instance ones the generated
+ // CreateOrReplace/AddXxx helpers assigned.
+ private void AttachOpenUsdRepresentation(PumpState pump)
+ {
+ if (m_plantStage == null)
+ {
+ return;
+ }
+ ushort ns = (ushort)Server.NamespaceUris.GetIndex(Opc.Ua.OpenUsd.Namespaces.OpenUSD);
+
+ OpenUsdRepresentationState rep = SystemContext
+ .CreateInstanceOfOpenUsdRepresentationType(
+ pump, new QualifiedName("OpenUsdRepresentation", ns));
+ // The instance factory leaves ReferenceTypeId = Null; set HasComponent
+ // so the AddIn is browsable from the represented object.
+ rep.ReferenceTypeId = ReferenceTypeIds.HasComponent;
+ pump.AddChild(rep);
+ rep.NodeId = SystemContext.NodeIdFactory.New(SystemContext, rep);
+
+ rep.CreateOrReplaceStage(SystemContext, null!).Value = m_plantStage.NodeId;
+ rep.CreateOrReplacePrimPath(SystemContext, null!).Value = PumpPrimPath;
+
+ MeasurementsState? m = pump.Operational?.Measurements;
+ NodeId bearingTemp = m?.BearingTemperature?.NodeId ?? NodeId.Null;
+ NodeId diffPressure = m?.DifferentialPressure?.NodeId ?? NodeId.Null;
+
+ // 0.2 UaAlarmToUsd: a supervision alarm active-state drives the status
+ // light visibility. A dedicated Boolean variable exposes the alarm
+ // aspect the simulation toggles (see AdvanceSimulation).
+ m_alarmActiveVar = CreatePumpVariable(
+ pump, "AlarmActive", Opc.Ua.DataTypeIds.Boolean, new Variant(false), writable: false);
+ // Serve the live flag from the simulation. The Variable is registered
+ // in its own right by RegisterOpenUsdSignalsAsync, which is what lets
+ // a monitored item sample it.
+ m_alarmActiveVar.MinimumSamplingInterval = SignalSamplingIntervalMilliseconds;
+ m_alarmActiveVar.OnSimpleReadValue =
+ (ISystemContext context, NodeState node, ref Variant value) =>
+ {
+ value = new Variant(AlarmActive);
+ return ServiceResult.Good;
+ };
+
+ // Shaft angular position. This is what makes the twin *look* like it
+ // is running: MassFlow is a rate, so binding it straight to a rotation
+ // op only ever produces a fixed fraction of a degree. Integrating it
+ // into an angle gives the impeller and coupling a continuous spin.
+ m_shaftAngleVar = CreatePumpVariable(
+ pump, "ShaftAngle", Opc.Ua.DataTypeIds.Double, new Variant(0.0), writable: false);
+ m_shaftAngleVar.MinimumSamplingInterval = SignalSamplingIntervalMilliseconds;
+ m_shaftAngleVar.OnSimpleReadValue =
+ (ISystemContext context, NodeState node, ref Variant value) =>
+ {
+ value = new Variant(ShaftAngleDegrees);
+ return ServiceResult.Good;
+ };
+
+ // Shaft position -> impeller rotation. MassFlow is a *rate*, so binding
+ // it straight to a rotation op pins the shaft at a fraction of a degree;
+ // the simulation integrates it into an angle instead. The scale slows
+ // the render to a legible ~45 degrees per second - a real 2900 rpm shaft
+ // would alias into a stroboscopic blur at any practical sampling rate.
+ CreateBinding(rep, ns, "ShaftSpin",
+ new Guid("6e63cf2c-f2de-4f78-a8f8-f0ccdbb7647a"),
+ m_shaftAngleVar.NodeId, "/Plant/Pumps/P101/Impeller", "xformOp:rotateZ", "double",
+ OpenUsdRenderTargetKindEnum.Rotation, 0.0025,
+ sourceSemanticId: MassFlowSemanticId);
+ // BearingTemperature is published in Kelvin (OPC 40223), but the
+ // DisplayColor render target ramps blue -> red over 20..100 degrees
+ // Celsius, so the binding declares the -273.15 shift.
+ CreateBinding(rep, ns, "BearingTempColor",
+ new Guid("b1a1f6f0-5c2b-5a1e-9f3a-2b7c4d8e0011"),
+ bearingTemp, "/Plant/Pumps/P101/Body/Mat/Surface", "inputs:diffuseColor", "color3f",
+ OpenUsdRenderTargetKindEnum.DisplayColor, 1.0,
+ offset: -273.15);
+
+ // DifferentialPressure is published in Pascal; the EmissiveColor
+ // render target brightens over 0..6 bar, so scale Pa -> bar.
+ CreateBinding(rep, ns, "DiffPressureEmissive",
+ new Guid("c2b2a7e1-6d3c-5b2f-a04b-3c8d5e9f1122"),
+ diffPressure, "/Plant/Pumps/P101/StatusLight/Mat/Surface", "inputs:emissiveColor", "color3f",
+ OpenUsdRenderTargetKindEnum.EmissiveColor, 0.00001);
+
+ CreateBinding(rep, ns, "AlarmActiveVisibility",
+ new Guid("d3c3b8f2-7e4d-5c30-b15c-4d9e6a0b2233"),
+ m_alarmActiveVar.NodeId, "/Plant/Pumps/P101/StatusLight", "visibility", "token",
+ OpenUsdRenderTargetKindEnum.Visibility, 1.0,
+ bindingTypeId: Opc.Ua.OpenUsd.ObjectTypes.OpenUsdAlarmBindingType,
+ alarmAspect: OpenUsdAlarmAspectEnum.ActiveState);
+
+ // 0.2 UsdToUaCommand (opt-in): a writable speed setpoint Variable is the
+ // command target. The binding is Controllable and present, but a
+ // connector only issues the write when explicitly enabled AND authorized
+ // (single-writer, fail-closed). Enabled=true means "declared", NOT
+ // "auto-actuated" โ the opt-in lives on the connector, not on Enabled.
+ // ยง5.10/ยง9 authorization posture: the command target is *capable* of being
+ // written (AccessLevel), but the write right is withheld by default and
+ // granted only to an authenticated (non-anonymous) session โ a Server
+ // "withholds by default" the RolePermissions a connector must hold before
+ // issuing any command.
+ m_speedSetpointVar = CreatePumpVariable(
+ pump, "SpeedSetpoint", Opc.Ua.DataTypeIds.Double, new Variant(0.0), writable: true);
+ m_speedSetpointVar.OnReadUserAccessLevel = OnReadCommandTargetUserAccessLevel;
+ CreateBinding(rep, ns, "SpeedSetpointCommand",
+ new Guid("e4d4c9a3-8f5e-5d41-c26d-5e0f7b1c3344"),
+ NodeId.Null, "/Plant/Pumps/P101/Impeller", "inputs:speedSetpoint", "double",
+ kind: null, 1.0,
+ bindingTypeId: Opc.Ua.OpenUsd.ObjectTypes.OpenUsdCommandBindingType,
+ signalRole: OpenUsdSignalRoleEnum.Controllable,
+ commandTargetNodeId: m_speedSetpointVar.NodeId,
+ commandTriggerPropertyName: "inputs:speedSetpoint");
+
+ // Composition (ยง5.12): the pump is composed of an Impeller and a Bearing,
+ // each a component Object with its own representation, mapped 1:1 to a child
+ // prim (arc=Child). This adds bindings on the pump representation.
+ AttachPumpComponents(pump, rep, ns);
+
+ SystemContext.AssignInstanceChildNodeIds(rep);
+ }
+
+ // ECLASS-style IRDI for "volume flow rate" โ a portable semantic id a
+ // connector can use to resolve the source across vendors (0.2 SemanticSource).
+ private const string MassFlowSemanticId = "0173-1#02-AAO677#002";
+
+ ///
+ /// ยง5.10/ยง9: the write right a command target requires is withheld by
+ /// default โ an anonymous session sees a read-only UserAccessLevel and its
+ /// write is rejected. Only a session that authenticated (and therefore holds a
+ /// Role beyond Anonymous) sees CurrentWrite.
+ ///
+ private static ServiceResult OnReadCommandTargetUserAccessLevel(
+ ISystemContext context, NodeState node, ref byte value)
+ {
+ value = IsAuthenticatedSession(context)
+ ? AccessLevels.CurrentReadOrWrite
+ : AccessLevels.CurrentRead;
+ return ServiceResult.Good;
+ }
+
+ private static bool IsAuthenticatedSession(ISystemContext context)
+ {
+ IUserIdentity? identity = (context as ISessionSystemContext)?.UserIdentity;
+ if (identity == null || identity.TokenType == UserTokenType.Anonymous)
+ {
+ return false;
+ }
+ ArrayOf roles = identity.GrantedRoleIds;
+ for (int i = 0; i < roles.Count; i++)
+ {
+ if (roles[i] != Opc.Ua.ObjectIds.WellKnownRole_Anonymous)
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ // Creates a simple Variable child on the pump (used for the 0.2 command
+ // setpoint and alarm-active demo signals), assigning a per-instance NodeId
+ // immediately: the node is hand-built, so no generated helper assigns one.
+ private BaseDataVariableState CreatePumpVariable(
+ PumpState pump, string name, NodeId dataType, Variant initialValue, bool writable)
+ {
+ byte access = writable
+ ? AccessLevels.CurrentReadOrWrite
+ : AccessLevels.CurrentRead;
+ var v = new BaseDataVariableState(pump)
+ {
+ SymbolicName = name,
+ BrowseName = new QualifiedName(name, pump.BrowseName.NamespaceIndex),
+ DisplayName = new LocalizedText(name),
+ ReferenceTypeId = ReferenceTypeIds.HasComponent,
+ TypeDefinitionId = VariableTypeIds.BaseDataVariableType,
+ DataType = dataType,
+ ValueRank = ValueRanks.Scalar,
+ AccessLevel = access,
+ UserAccessLevel = access,
+ Value = initialValue,
+ };
+ pump.AddChild(v);
+ v.NodeId = SystemContext.NodeIdFactory.New(SystemContext, v);
+ return v;
+ }
+
+ private void OrganiseRepresentation(PumpState pump)
+ {
+ FolderState? registry = m_openUsdRoot?.Representations;
+ if (registry == null)
+ {
+ return;
+ }
+ foreach (BaseInstanceState child in EnumerateChildren(pump))
+ {
+ if (child is OpenUsdRepresentationState rep)
+ {
+ registry.AddReference(ReferenceTypeIds.Organizes, false, rep.NodeId);
+ rep.AddReference(ReferenceTypeIds.Organizes, true, registry.NodeId);
+ }
+ }
+ }
+
+ private System.Collections.Generic.List EnumerateChildren(NodeState parent)
+ {
+ var children = new System.Collections.Generic.List();
+ parent.GetChildren(SystemContext, children);
+ return children;
+ }
+
+ // Thin adapter over the reusable Opc.Ua.OpenUsd.Server authoring API: binds the
+ // plant stage and forwards. The binding-authoring logic lives in the SDK
+ // (OpenUsdRepresentationAuthoring.AddLiveBinding), not in this sample.
+ private void CreateBinding(
+ OpenUsdRepresentationState rep, ushort ns, string name,
+ Guid bindingDefinitionId, NodeId sourceNodeId, string targetPrimPath,
+ string targetPropertyName, string targetUsdTypeName,
+ OpenUsdRenderTargetKindEnum? kind, double scale,
+ uint bindingTypeId = Opc.Ua.OpenUsd.ObjectTypes.OpenUsdValueChangeBindingType,
+ OpenUsdSignalRoleEnum signalRole = OpenUsdSignalRoleEnum.Observable,
+ string? sourceSemanticId = null,
+ OpenUsdAlarmAspectEnum? alarmAspect = null,
+ NodeId commandTargetNodeId = default,
+ string? commandTriggerPropertyName = null,
+ double offset = 0.0)
+ {
+ OpenUsdLiveBindingState binding = rep.AddLiveBinding(
+ SystemContext, ns, m_plantStage!.NodeId, name, bindingDefinitionId, sourceNodeId,
+ targetPrimPath, targetPropertyName, targetUsdTypeName, kind, scale,
+ bindingTypeId, signalRole, sourceSemanticId, alarmAspect,
+ commandTargetNodeId, commandTriggerPropertyName);
+
+ // 5.8 applies Scale then Offset. AddLiveBinding only sets Scale, so an
+ // additive term (such as the Kelvin -> Celsius shift the DisplayColor
+ // render target expects) has to be authored here.
+ if (offset != 0.0)
+ {
+ binding.CreateOrReplaceOffset(
+ SystemContext,
+ SystemContext.CreateOpenUsdLiveBindingType_Offset(binding, forInstance: true))
+ .Value = offset;
+ }
+ }
+ }
+
+ internal static partial class OpenUsdRepresentationLog
+ {
+ [LoggerMessage(EventId = PumpDeviceIntegrationServerEventIds.OpenUsdRepresentation + 1,
+ Level = LogLevel.Information,
+ Message = "Materialised OpenUSD facility (root {RootId}, PlantStage {StageId}).")]
+ public static partial void MaterialisedOpenUsdFacility(this ILogger logger, NodeId rootId, NodeId stageId);
+ }
+}
diff --git a/samples/PumpDeviceIntegrationServer/PumpDeviceIntegrationServer.csproj b/samples/PumpDeviceIntegrationServer/PumpDeviceIntegrationServer.csproj
index a5885fe90a..e83ee45f02 100644
--- a/samples/PumpDeviceIntegrationServer/PumpDeviceIntegrationServer.csproj
+++ b/samples/PumpDeviceIntegrationServer/PumpDeviceIntegrationServer.csproj
@@ -17,6 +17,8 @@
+
+
@@ -28,6 +30,13 @@
+
+
+ Plant.usda
+ pump.usda
+ remote-pump.usda
+
+
+
+ http://opcfoundation.org/UA/OpenUSD/
+
+
+
+
+
+
+
+ i=1
+ i=7
+ i=11
+ i=12
+ i=14
+ i=15
+ i=17
+ i=20
+ i=21
+ i=540
+ i=887
+ i=47
+ i=46
+ i=45
+ i=35
+ i=40
+ i=37
+ i=17603
+ i=78
+ i=80
+ i=11508
+
+
+ OpenUsdRenderTargetKindEnum
+ Classifies the USD render target a live value drives (advisory routing hint).
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3902
+
+ A geodetic anchor coordinate (latitude/longitude/height) on a USD georeference or globe-anchor schema; see the geospatial rule (Bindings spec ยง5.8) and Annex D.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3002
+
+ TranslationRotationScaleTransformVisibilityDisplayColorEmissiveColorOpacityCustomGeoreference
+
+
+ OpenUsdBadQualityActionEnum
+ What the connector does with a non-Good source value.
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3903
+
+ Do not update the target.Keep the last Good value.Remove the authored opinion (reveal a weaker layer).Author a configured fallback value.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3003
+
+ SkipHoldLastClearOpinionFallback
+
+
+ OpenUsdBindingStateEnum
+ Runtime lifecycle state of a live binding, exposed for diagnostics.
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3904
+
+
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3004
+
+ DisabledUnresolvedReadyActiveDegradedError
+
+
+ OpenUsdLiveBindingType
+ Abstract base for one read-only live binding: a source OPC UA Variable value drives one target USD attribute. The binding intent is expressed by the concrete subtype (ValueChange/Alarm/History/Command), not by an enum. The declaration is portable; the runtime connector resolves and applies it. The effective runtime identity is (represented object, BindingDefinitionId).
+ OpenUSD Binding
+
+ i=58
+ ns=1;i=6001
+ ns=1;i=6002
+ ns=1;i=6003
+ ns=1;i=6004
+ ns=1;i=6005
+ ns=1;i=6006
+ ns=1;i=6007
+ ns=1;i=6008
+ ns=1;i=6009
+ ns=1;i=6010
+ ns=1;i=6011
+ ns=1;i=6012
+ ns=1;i=6013
+ ns=1;i=6014
+ ns=1;i=6015
+ ns=1;i=6016
+ ns=1;i=6017
+ ns=1;i=6018
+ ns=1;i=6019
+ ns=1;i=6031
+ ns=1;i=6032
+ ns=1;i=6067
+
+
+
+ BindingDefinitionId
+ Stable declaration identifier used for override/tombstone matching across type and instance levels. NOT a runtime instance key.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ Enabled
+ False acts as a tombstone that suppresses an inherited binding.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ SourceNodeId
+ Absolute NodeId of the source Variable (instance-level). Optional; prefer SourceBrowsePath for type-level declarations.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ SourceBrowsePath
+ RelativePath to the source Variable, resolved from the represented object. Preferred, instance-portable form.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ AttributeId
+ Source attribute id; default 13 (Value). Telemetry binds Value only.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ TargetStage
+ NodeId of the stage holding the target prim: an OpenUsdStageType instance, or a materialized Part 2 UsdStageType instance when the target is an in-server materialized scene.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ TargetPrimPath
+ Prim path of the target: absolute, or relative to the representation PrimPath.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ TargetPropertyName
+ USD attribute name on the target prim, e.g. 'xformOp:rotateZ' or 'primvars:displayColor'.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ TargetUsdTypeName
+ Expected USD Sdf value type name, e.g. 'double', 'float', 'bool', 'color3f'.
+
+ i=78
+ i=68
+ ns=1;i=1004
+
+
+
+ RenderTargetKind
+ Advisory routing hint for the render target category.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ ValueSemanticUri
+ Value semantics: scalar/angle/length/point/vector/quaternion/matrix. The transform profile uses RSL CartesianFrameAngleOrientationType semantics.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ Scale
+ Linear conversion scale: target = Scale * converted + Offset.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ Offset
+ Linear conversion offset.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ SourceEngineeringUnits
+ Asserted source engineering units (UNECE).
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ TargetEngineeringUnits
+ Requested target engineering units.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ BadQualityAction
+ How the connector treats a non-Good source value. Default Skip.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ SamplingIntervalHint
+ Requested sampling interval hint in milliseconds. A HINT only; the actual MonitoredItem parameters are negotiated per client Subscription.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ State
+ Runtime lifecycle state (diagnostic).
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ LastError
+ Last operation error text (diagnostic).
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ OpenUsdStageType
+ Describes one OpenUSD stage available to the server. Identity is the root layer identifier plus, where used, the session layer and resolver context; the stage Object NodeId is its same-server identity.
+ OpenUSD Binding
+
+ i=58
+ ns=1;i=6020
+ ns=1;i=6021
+ ns=1;i=6022
+ ns=1;i=6023
+ ns=1;i=6038
+ ns=1;i=6039
+ ns=1;i=6040
+ ns=1;i=6041
+ ns=1;i=6065
+
+
+
+ RootLayerIdentifier
+ Opaque authored root-layer / resolver identifier (NOT necessarily a URI).
+
+ i=78
+ i=68
+ ns=1;i=1002
+
+
+
+ SessionLayerIdentifier
+ Opaque session-layer identifier, if any. Contributes to stage identity.
+
+ i=80
+ i=68
+ ns=1;i=1002
+
+
+
+ ResolverContext
+ Opaque resolver-context descriptor (e.g. a named context profile).
+
+ i=80
+ i=68
+ ns=1;i=1002
+
+
+
+ ResolvedRootLayerUri
+ Informative current resolution as a URI, when representable. Not authoritative.
+
+ i=80
+ i=68
+ ns=1;i=1002
+
+
+
+ OpenUsdRepresentationType
+ AddIn that binds a domain Object to a canonical composed USD prim path on a specific stage. Mounted with HasAddIn; carries live bindings as children.
+ OpenUSD Binding
+
+ i=58
+ ns=1;i=6024
+ ns=1;i=6025
+ ns=1;i=6026
+ ns=1;i=6027
+ ns=1;i=6059
+
+
+
+ DefaultInstanceBrowseName
+ Default BrowseName for instances of this AddIn (Part 3 AddIn convention).
+
+ i=68
+ ns=1;i=1003
+
+ 1OpenUsdRepresentation
+
+
+ Stage
+ NodeId of the OpenUsdStageType instance this representation targets.
+
+ i=78
+ i=68
+ ns=1;i=1003
+
+
+
+ PrimPath
+ Canonical, absolute, composed-stage prim path (SdfPath) for this instance. Instance-level: a reusable type cannot supply an absolute instance path.
+
+ i=78
+ i=68
+ ns=1;i=1003
+
+
+
+ <Binding>
+ A live binding whose source resolves relative to the represented object.
+
+ i=11508
+ ns=1;i=1004
+ ns=1;i=1003
+
+
+
+ OpenUsdRootType
+ Root of the server-wide OpenUSD facility. Contains the stage registry and the representation registry for deterministic discovery.
+ OpenUSD Binding
+
+ i=58
+ ns=1;i=6028
+ ns=1;i=6029
+
+
+
+ Stages
+ Registry of OpenUsdStageType instances available to the server.
+
+ i=78
+ i=61
+ ns=1;i=1001
+
+
+
+ Representations
+ Registry that Organizes every OpenUsdRepresentation AddIn in the server, so a connector can enumerate all representations from one place.
+
+ i=78
+ i=61
+ ns=1;i=1001
+
+
+
+ IOpenUsdRepresentedType
+ Optional interface advertising that a domain ObjectType participates in OpenUSD representation. Applied with HasInterface; informative for browsing.
+ OpenUSD Binding
+
+ i=17602
+ ns=1;i=6030
+
+
+
+ <OpenUsdRepresentation>
+ Placeholder for the representation AddIn on an implementing instance.
+
+ i=11508
+ ns=1;i=1003
+ ns=1;i=1010
+
+
+
+ OpenUsdSignalRoleEnum
+ Role of the bound signal, mirroring the asset-definition observable/controllable tagging. Only Controllable signals are eligible for a command binding.
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3905
+
+ Read-only; drives USD but cannot be commanded.May be commanded via an opt-in, authorized command binding.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3005
+
+ ObservableControllable
+
+
+ OpenUsdAlarmAspectEnum
+ For OpenUsdAlarmBindingType bindings: which A&C condition aspect drives the target attribute.
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3906
+
+ Condition ActiveState (boolean).Condition Severity (numeric).Condition AckedState (boolean).Condition EnabledState (boolean).
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3006
+
+ ActiveStateSeverityAckedStateEnabledState
+
+
+ OpenUsdDigestAlgorithmEnum
+ Digest algorithm for OpenUsdStageType.RootLayerDigest (Twin BOM content integrity).
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3907
+
+ SHA-256.SHA-384.SHA-512.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3007
+
+ NoneSha256Sha384Sha512
+
+
+ SignalRole
+ Observable (read-only, default) or Controllable (eligible for a command binding).
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ SourceSemanticId
+ Semantic identifier (e.g. ECLASS / IEC CDD IRDI) of the source signal; resolved against the source's semantic annotations for cross-vendor portability.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
+ OpenUsdValueChangeBindingType
+ A source UA Variable Value change drives a USD attribute (the default binding). Adds no members beyond the abstract base; binds the source Value (AttributeId 13).
+ OpenUSD Binding
+
+ ns=1;i=1004
+
+
+
+ OpenUsdAlarmBindingType
+ Read-only OPC UA A&C condition aspect (Part 9) drives a USD attribute.
+ OpenUSD Binding
+
+ ns=1;i=1004
+ ns=1;i=6033
+
+
+
+ AlarmAspect
+ Which A&C condition aspect drives the target (ActiveState/Severity/AckedState/EnabledState).
+
+ i=80
+ i=68
+ ns=1;i=1008
+
+
+
+ OpenUsdHistoryBindingType
+ Read-only history (Part 11 HistoryRead) authored as USD time samples.
+ OpenUSD Binding
+
+ ns=1;i=1004
+ ns=1;i=6034
+
+
+
+ TimeSampled
+ Author values as USD time samples (playback) rather than the latest default.
+
+ i=80
+ i=68
+ ns=1;i=1009
+
+
+
+ OpenUsdCommandBindingType
+ Opt-in, authorized USD-side intent drives an OPC UA write / Method call (USD -> UA).
+ OpenUSD Binding
+
+ ns=1;i=1004
+ ns=1;i=6035
+ ns=1;i=6036
+ ns=1;i=6037
+
+
+
+ CommandTargetNodeId
+ The Variable to write, or the Object on which to Call CommandMethodId.
+
+ i=78
+ i=68
+ ns=1;i=1011
+
+
+
+ CommandMethodId
+ Optional Method to invoke instead of a Variable write.
+
+ i=80
+ i=68
+ ns=1;i=1011
+
+
+
+ CommandTriggerPropertyName
+ The USD attribute whose change is interpreted as the command intent/value.
+
+ i=78
+ i=68
+ ns=1;i=1011
+
+
+
+ RootLayerDigest
+ Cryptographic digest of the resolved root layer, for content-integrity verification (Twin BOM). Verify before composing the stage.
+
+ i=80
+ i=68
+ ns=1;i=1002
+
+
+
+ RootLayerDigestAlgorithm
+ Digest algorithm for RootLayerDigest (default SHA-256).
+
+ i=80
+ i=68
+ ns=1;i=1002
+
+
+
+ Signature
+ Optional detached signature over the digest / stage identity, for provenance.
+
+ i=80
+ i=68
+ ns=1;i=1002
+
+
+
+ ProvenanceUri
+ Optional URI locating provenance / a signed Twin BOM manifest for the stage.
+
+ i=80
+ i=68
+ ns=1;i=1002
+
+
+
+ OpenUsdCardinalityEnum
+ Cardinality of a component binding: 1:1 or 1..n.
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3908
+
+ Exactly one component (1:1).Zero or more components of a type (1..n).
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3008
+
+ OneMany
+
+
+ OpenUsdCompositionArcEnum
+ USD composition arc used to place a component prim under its parent.
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3909
+
+ Inline nested prim under the parent prim.Prim that references the component's external asset.Prim that payloads (deferred-load) the component's external asset.Instanceable reference (instanceable=true), for efficient 1..n.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3009
+
+ ChildReferencePayloadInstance
+
+
+ OpenUsdComponentBindingType
+ One composition/aggregation binding: maps an OPC UA component relationship of the represented Object onto a USD composition arc, so a connector assembles the component prim(s). Carried as a <Component> child of a representation.
+ OpenUSD Binding
+
+ i=58
+ ns=1;i=6042
+ ns=1;i=6043
+ ns=1;i=6044
+ ns=1;i=6045
+ ns=1;i=6046
+ ns=1;i=6047
+ ns=1;i=6048
+ ns=1;i=6049
+ ns=1;i=6050
+ ns=1;i=6051
+ ns=1;i=6052
+ ns=1;i=6053
+ ns=1;i=6054
+ ns=1;i=6055
+ ns=1;i=6056
+ ns=1;i=6057
+ ns=1;i=6058
+ ns=1;i=6066
+
+
+
+ BindingDefinitionId
+ Stable declaration id for override/tombstone matching.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ Enabled
+ False acts as a tombstone that suppresses an inherited component binding.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ Cardinality
+ One (1:1) or Many (1..n).
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ CompositionArc
+ How the component maps into the parent prim: Child, Reference, Payload, Instance.
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ ComponentReferenceType
+ Aggregating ReferenceType from the represented Object to its component(s); default HasComponent.
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ ComponentBrowsePath
+ RelativePath to the component Object (One) or to the container from which components are enumerated (Many).
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ ComponentTypeDefinition
+ Expected component ObjectType; selects children for Many and locates each component's own representation.
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ TargetPrimPath
+ Child prim (One) or parent scope prim (Many), relative to the parent representation PrimPath (or absolute).
+
+ i=78
+ i=68
+ ns=1;i=1005
+
+
+
+ TargetPrimNameSource
+ For Many: how to name each instance prim (BrowseName default, a source-property RelativePath, or a {...} template).
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ ComponentAssetReference
+ For Reference/Payload/Instance: the external USD asset + default prim, e.g. @pump.usda@</Pump>.
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ ComponentRepresentation
+ NodeId of the component's own OpenUsdRepresentation AddIn (its sub-bindings compose under the component prim).
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ Dynamic
+ The component set may change at runtime (reconciled from model-change events).
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ ChangeEventSource
+ Node whose GeneralModelChange/SemanticChange events signal recomposition; default the Server Object (i=2253).
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ ComponentServerUri
+ For a component on another server: the remote Server's application/namespace URI.
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ ComponentEndpointUrl
+ For a component on another server: the remote endpoint URL (else discovered).
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ State
+ Runtime lifecycle state (diagnostic).
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ LastError
+ Last operation error text (diagnostic).
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ <Component>
+ A component/aggregation binding composing this Object's components into the USD prim tree.
+
+ i=11508
+ ns=1;i=1005
+ ns=1;i=1003
+
+
+
+ OpenUsdAssetKindEnum
+ Role of a served USD asset within a stage's served layer closure.
+ OpenUSD Binding DataTypes
+
+ i=29
+ ns=1;i=3910
+
+ The stage's root/base layer (exactly one served RootLayer per stage).A sublayer contributing to the root layer's composition.An asset introduced by a reference arc (e.g. a component's asset).An asset introduced by a payload arc (deferred-loaded).A texture / image asset referenced by a material.A packaged asset bundle (e.g. USDZ) carrying the whole closure.
+
+
+ EnumStrings
+
+ i=78
+ i=68
+ ns=1;i=3010
+
+ RootLayerSubLayerReferencePayloadTexturePackage
+
+
+ OpenUsdAssetType
+ One served USD asset/layer: authored content the server delivers through the address space so a connector can fetch it and compose the stage locally, with no external resolver. OpenUsdAssetType subtypes the Part 5 FileType, so the asset's bytes are streamed directly through the node's own Open/Read/Close; AssetIdentifier is the resolver identifier / relative path used to place the asset in the local cache so that @...@ references resolve.
+ OpenUSD Binding
+
+ i=11575
+ ns=1;i=6060
+ ns=1;i=6061
+ ns=1;i=6062
+ ns=1;i=6063
+ ns=1;i=6064
+
+
+
+ AssetIdentifier
+ Resolver identifier / relative path of this asset, matching the stage RootLayerIdentifier or a ComponentAssetReference asset path; used for @...@ resolution and cache placement.
+
+ i=78
+ i=68
+ ns=1;i=1006
+
+
+
+ AssetKind
+ Role of this asset within the stage's served layer closure.
+
+ i=78
+ i=68
+ ns=1;i=1006
+
+
+
+ MediaType
+ IANA media type of the content, e.g. 'model/vnd.usda', 'model/vnd.usdz+zip', 'image/png'.
+
+ i=80
+ i=68
+ ns=1;i=1006
+
+
+
+ Digest
+ Cryptographic digest of this asset's resolved content, for per-layer integrity verification. A connector verifies it before composing the asset.
+
+ i=80
+ i=68
+ ns=1;i=1006
+
+
+
+ DigestAlgorithm
+ Digest algorithm for Digest (default SHA-256).
+
+ i=80
+ i=68
+ ns=1;i=1006
+
+
+
+ Assets
+ Optional registry of OpenUsdAssetType instances forming this stage's served layer closure (exactly one RootLayer). Present only when the server delivers its geometry; a connector that finds it fetches and composes the stage locally, else it resolves RootLayerIdentifier externally as before.
+
+ i=80
+ i=61
+ ns=1;i=1002
+
+
+
+ ComponentAssetNode
+ NodeId of the OpenUsdAssetType (under the stage's Assets folder) serving this component's asset, when the server delivers it. Complements ComponentAssetReference.
+
+ i=80
+ i=68
+ ns=1;i=1005
+
+
+
+ TargetNodeId
+ Optional direct NodeId of the target when it is an in-server materialized Variable (a Part 2 UsdAttributeType). When present it takes precedence over the TargetPrimPath/TargetPropertyName pair, which remain the portable descriptors.
+
+ i=80
+ i=68
+ ns=1;i=1004
+
+
+
diff --git a/src/Opc.Ua.OpenUsd/NugetREADME.md b/src/Opc.Ua.OpenUsd/NugetREADME.md
new file mode 100644
index 0000000000..e6959de7d7
--- /dev/null
+++ b/src/Opc.Ua.OpenUsd/NugetREADME.md
@@ -0,0 +1,18 @@
+# Opc.Ua.OpenUsd
+
+Draft **OPC UA โ OpenUSD Bindings** companion information model for the OPC UA .NET Standard stack.
+
+It carries the shared `Opc.Ua.OpenUsdBinding` NodeSet and the source-generated model
+(type NodeIds, `NodeState` classes and enumerations) in the `Opc.Ua.OpenUsd` namespace:
+
+- `OpenUsdRootType` / `OpenUsdStageType` / `OpenUsdRepresentationType`
+- the abstract `OpenUsdLiveBindingType` and its intent subtypes
+ (`OpenUsdValueChangeBindingType`, `OpenUsdAlarmBindingType`, `OpenUsdHistoryBindingType`,
+ `OpenUsdCommandBindingType`)
+- `OpenUsdComponentBindingType` (composition/aggregation) and `OpenUsdAssetType : FileType`
+ (Part 5 asset content delivery)
+
+Server-side reusable functionality lives in **Opc.Ua.OpenUsd.Server**; the generic,
+dependency-injectable connector lives in **Opc.Ua.OpenUsd.Client**.
+
+This is a draft companion model published for review; NodeIds and shapes may change.
diff --git a/src/Opc.Ua.OpenUsd/Opc.Ua.OpenUsd.csproj b/src/Opc.Ua.OpenUsd/Opc.Ua.OpenUsd.csproj
new file mode 100644
index 0000000000..cac217efc6
--- /dev/null
+++ b/src/Opc.Ua.OpenUsd/Opc.Ua.OpenUsd.csproj
@@ -0,0 +1,55 @@
+
+
+ $(AssemblyPrefix).OpenUsd
+ $(LibTargetFrameworks)
+ $(PackagePrefix).Opc.Ua.OpenUsd
+ Opc.Ua.OpenUsd
+ $(NoWarn);CS1591
+ enable
+ Draft OPC UA - OpenUSD Bindings companion information model (representation, live/alarm/history/command bindings, composition, content integrity and asset content delivery), source-generated from the shared NodeSet2.xml.
+ true
+ NugetREADME.md
+ MIT
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+ $(PackageId).Debug
+
+
+
+
+
+
+ Analyzer
+ false
+
+
+
+
+
+
+
+
+ Opc.Ua.OpenUsd
+
+
+
+
+
+ v105
+ true
+
+
+
+
+
diff --git a/src/Opc.Ua.OpenUsd/Properties/AssemblyInfo.cs b/src/Opc.Ua.OpenUsd/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7798c9bd57
--- /dev/null
+++ b/src/Opc.Ua.OpenUsd/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+/* ========================================================================
+ * 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;
+
+[assembly: CLSCompliant(false)]
diff --git a/src/Opc.Ua.OpenUsdScene.Server/NugetREADME.md b/src/Opc.Ua.OpenUsdScene.Server/NugetREADME.md
new file mode 100644
index 0000000000..a918d0acff
--- /dev/null
+++ b/src/Opc.Ua.OpenUsdScene.Server/NugetREADME.md
@@ -0,0 +1,49 @@
+# Opc.Ua.OpenUsdScene.Server
+
+Server-side functionality for the draft **OPC UA โ OpenUSD Scene Materialization (Part 2)**
+specification: it materializes a composed USD stage into an OPC UA address space, and reads it
+back out again.
+
+Where the Bindings model (Part 1) keeps the USD scene *outside* OPC UA and binds process values
+to it, Scene Materialization puts the scene *inside* the address space โ the prim hierarchy
+becomes the node hierarchy, so browsing the server is browsing the scene.
+
+## What it does
+
+| API | Purpose |
+|---|---|
+| `ISystemContext.MaterializeUsdStage(...)` | Materializes a `UsdStage` as a `UsdStageType` Object with the full prim tree (ยง7.1) |
+| `ISystemContext.ExportUsdStage(...)` | Reads a materialized stage back into a scene document (ยง7.2) |
+| `ISystemContext.EnsureStagesFolder(...)` | Finds or creates the discovery folder stages live under (ยง4.3) |
+| `UsdMaterializationResult.TryResolveBindingTarget(...)` | Resolves the attribute a Part 1 binding targets (ยง10) |
+
+## Conformance units
+
+`UsdMaterializationOptions` switches map one-to-one onto the specification's conformance units
+(ยง12), so a Server materializes only what it needs. **Scene Structure** is the always-on
+baseline; `MaterializeComposition`, `MaterializeAppliedSchemas`, `MaterializeMetadata`,
+`DualAuthorPortableGeoreference` and `HistorizeLiveAttributes` select the rest.
+
+## Notable behaviour
+
+- **Typed prims.** A prim whose `typeName` is a known UsdGeom or UsdShade schema is
+ materialized as the matching ObjectType subtype (ยง5.3). An **unknown** typed schema is never
+ dropped โ it stays a `UsdPrimType` carrying its `TypeName` token, so an exporter reproduces
+ it faithfully (ยง8.4). The same applies to unknown API schemas and unknown value types.
+- **Reversible typing.** Every attribute records the exact `SdfValueTypeName` in `UsdTypeName`,
+ which is what makes the many-to-one ยง6.2 DataType mapping reversible.
+- **Georeferencing.** When a Cesium georeference or globe-anchor schema is recognised, the
+ portable `UsdGeoreferenceApiType` / `UsdGlobeAnchorApiType` is dual-authored with the same
+ values, so a generic client reads the anchor from one well-known type (ยง5.8, Annex B). A
+ partial anchor is not published โ a wrong position is worse than none.
+- **Live attributes.** An attribute flagged live is materialized for Mode A (ยง9): the Value is
+ server-maintained, and where retained it is exposed through HistoricalAccess. Driving those
+ values is a Server-side responsibility โ an external Part 1 connector authors into a USD sink
+ and cannot write an in-server Variable.
+
+## Related packages
+
+- `Opc.Ua.OpenUsdScene` โ the companion information model plus the `.usda` reader/writer
+- `Opc.Ua.OpenUsd` / `Opc.Ua.OpenUsd.Server` โ the Part 1 Bindings model
+
+> This package tracks a **draft** specification; APIs, model URI and NodeIds may change.
diff --git a/src/Opc.Ua.OpenUsdScene.Server/Opc.Ua.OpenUsdScene.Server.csproj b/src/Opc.Ua.OpenUsdScene.Server/Opc.Ua.OpenUsdScene.Server.csproj
new file mode 100644
index 0000000000..625ecdfb81
--- /dev/null
+++ b/src/Opc.Ua.OpenUsdScene.Server/Opc.Ua.OpenUsdScene.Server.csproj
@@ -0,0 +1,28 @@
+
+
+ $(AssemblyPrefix).OpenUsdScene.Server
+ $(LibTargetFrameworks)
+ $(PackagePrefix).Opc.Ua.OpenUsdScene.Server
+ Opc.Ua.OpenUsdScene.Server
+ $(NoWarn);CS1591
+ enable
+ Server-side reusable functionality for the draft OPC UA - OpenUSD Scene Materialization companion model: materializes a composed USD stage into the address space as a browsable prim tree with typed prims, attributes, relationships, composition provenance, applied API schemas and georeferencing, and exports it back to .usda.
+ true
+ NugetREADME.md
+ MIT
+ true
+ true
+
+
+
+
+
+ $(PackageId).Debug
+
+
+
+
+
+
+
+
diff --git a/src/Opc.Ua.OpenUsdScene.Server/Properties/AssemblyInfo.cs b/src/Opc.Ua.OpenUsdScene.Server/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..7798c9bd57
--- /dev/null
+++ b/src/Opc.Ua.OpenUsdScene.Server/Properties/AssemblyInfo.cs
@@ -0,0 +1,32 @@
+/* ========================================================================
+ * 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;
+
+[assembly: CLSCompliant(false)]
diff --git a/src/Opc.Ua.OpenUsdScene.Server/UsdSceneDiscovery.cs b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneDiscovery.cs
new file mode 100644
index 0000000000..a216726109
--- /dev/null
+++ b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneDiscovery.cs
@@ -0,0 +1,410 @@
+/* ========================================================================
+ * 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;
+using System.Collections.Generic;
+
+namespace Opc.Ua.OpenUsdScene.Server
+{
+ ///
+ /// Discovery and Part 1 interoperability helpers for materialized scenes
+ /// (draft OPC UA โ OpenUSD Scene Materialization ยง4.3, ยง10).
+ ///
+ ///
+ /// A Server should expose its materialized stages under one well-known folder so a client
+ /// starts at a single entry point and browses HasComponent into the prim tree:
+ /// Part 1's Server/OpenUSD/Stages when the Bindings model is also implemented, or
+ /// a standalone Server/OpenUSDScene/Stages otherwise.
+ ///
+ public static class UsdSceneDiscovery
+ {
+ ///
+ /// The standalone discovery root BrowseName.
+ ///
+ public const string OpenUsdSceneRootName = "OpenUSDScene";
+
+ ///
+ /// The BrowseName of the stages folder under either discovery root.
+ ///
+ public const string StagesFolderName = "Stages";
+
+ ///
+ /// The BrowseName of Part 1's TargetNodeId live-binding member (Part 1 ยง; the
+ /// NodeId form of a binding target). Named by string so Part 2 authors it without a
+ /// compile-time dependency on the Part 1 model (ยง4.2 โ the two models are independent).
+ ///
+ public const string TargetNodeIdName = "TargetNodeId";
+
+ ///
+ /// Finds โ or creates โ the folder materialized stages are organized under.
+ ///
+ /// The server system context.
+ /// The Server Object.
+ /// The OpenUSD Scene companion namespace index.
+ /// Part 1's Server/OpenUSD/Stages folder when
+ /// the Bindings model is also implemented; pass null for a standalone Server.
+ /// When supplied it is used as-is, so one connector discovers both the external-stage
+ /// bindings and the in-server materialized stages (ยง10).
+ /// The stages folder.
+ public static FolderState EnsureStagesFolder(
+ this ISystemContext context,
+ NodeState server,
+ ushort ns,
+ FolderState? part1StagesFolder = null)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+ if (server is null)
+ {
+ throw new ArgumentNullException(nameof(server));
+ }
+ if (part1StagesFolder != null)
+ {
+ return part1StagesFolder;
+ }
+
+ FolderState root = EnsureFolder(context, server, OpenUsdSceneRootName, ns);
+ return EnsureFolder(context, root, StagesFolderName, ns);
+ }
+
+ ///
+ /// Resolves the materialized attribute Variable a Part 1 live binding targets (ยง10).
+ ///
+ ///
+ /// Part 1 โฅ 0.3.0 carries the target two ways and a Server should author both, so
+ /// path-resolving and NodeId-resolving connectors agree: the optional
+ /// TargetNodeId names the Variable directly, and the mandatory
+ /// TargetPrimPath/TargetPropertyName pair resolves to the same Variable.
+ /// This helper resolves the path form against a materialization result; a caller that
+ /// already has a NodeId does not need it.
+ ///
+ /// The materialization to resolve against.
+ /// The absolute prim path, for example
+ /// /Plant/Pumps/P101/Pump/Impeller.
+ /// The attribute name, for example
+ /// xformOp:rotateZ.
+ /// The resolved attribute Variable.
+ /// true when the target resolved.
+ public static bool TryResolveBindingTarget(
+ this UsdMaterializationResult result,
+ string targetPrimPath,
+ string targetPropertyName,
+ out UsdAttributeState? attribute)
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+ attribute = null;
+ if (string.IsNullOrEmpty(targetPrimPath) || string.IsNullOrEmpty(targetPropertyName))
+ {
+ return false;
+ }
+ string key = targetPrimPath + "." + targetPropertyName;
+ if (result.AttributesByPath.TryGetValue(key, out UsdAttributeState? found))
+ {
+ attribute = found;
+ return true;
+ }
+ return false;
+ }
+
+ ///
+ /// The NodeId a Part 1 binding should author into its TargetNodeId member for
+ /// the given materialized attribute (ยง10).
+ ///
+ /// The materialization to resolve against.
+ /// The absolute prim path.
+ /// The attribute name.
+ /// The NodeId, or a null NodeId when the target does not resolve.
+ public static NodeId ResolveBindingTargetNodeId(
+ this UsdMaterializationResult result,
+ string targetPrimPath,
+ string targetPropertyName)
+ {
+ return result.TryResolveBindingTarget(
+ targetPrimPath, targetPropertyName, out UsdAttributeState? attribute)
+ && attribute != null
+ ? attribute.NodeId
+ : NodeId.Null;
+ }
+
+ ///
+ /// Stage-aware target resolution: resolves the path form only when the binding names
+ /// this stage, so a Server hosting several materialized stages never returns a
+ /// same-path attribute from the wrong stage (ยง10).
+ ///
+ /// The materialization to resolve against.
+ /// The NodeId of the UsdStageType the binding targets
+ /// (Part 1's TargetStage). A null NodeId fails closed โ the stage-aware form
+ /// requires a stage; use the two-argument overload for a single-stage Server.
+ /// The absolute prim path.
+ /// The attribute name.
+ /// The resolved attribute Variable.
+ /// true when the binding names this stage and the target resolves.
+ public static bool TryResolveBindingTarget(
+ this UsdMaterializationResult result,
+ NodeId targetStage,
+ string targetPrimPath,
+ string targetPropertyName,
+ out UsdAttributeState? attribute)
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+ attribute = null;
+ // A binding that carries a TargetStage must match this stage exactly; otherwise the
+ // target belongs to a different stage and this result must not answer for it.
+ if (targetStage.IsNull || result.Stage.NodeId != targetStage)
+ {
+ return false;
+ }
+ return result.TryResolveBindingTarget(
+ targetPrimPath, targetPropertyName, out attribute);
+ }
+
+ ///
+ /// Stage-aware target resolution across every materialized stage a Server hosts: the
+ /// binding's TargetStage selects which stage answers, then the path resolves
+ /// within it (ยง10).
+ ///
+ /// Every materialized stage the Server hosts.
+ /// The NodeId of the stage the binding targets.
+ /// The absolute prim path.
+ /// The attribute name.
+ /// The resolved attribute Variable.
+ /// true when exactly the named stage resolves the target.
+ public static bool TryResolveBindingTarget(
+ this IEnumerable results,
+ NodeId targetStage,
+ string targetPrimPath,
+ string targetPropertyName,
+ out UsdAttributeState? attribute)
+ {
+ if (results is null)
+ {
+ throw new ArgumentNullException(nameof(results));
+ }
+ attribute = null;
+ foreach (UsdMaterializationResult result in results)
+ {
+ if (result != null &&
+ result.TryResolveBindingTarget(
+ targetStage, targetPrimPath, targetPropertyName, out attribute))
+ {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ ///
+ /// The NodeId a Part 1 binding should author into its TargetNodeId member,
+ /// disambiguated by the binding's TargetStage across every hosted stage (ยง10).
+ ///
+ /// Every materialized stage the Server hosts.
+ /// The NodeId of the stage the binding targets.
+ /// The absolute prim path.
+ /// The attribute name.
+ /// The NodeId, or a null NodeId when nothing resolves in the named stage.
+ public static NodeId ResolveBindingTargetNodeId(
+ this IEnumerable results,
+ NodeId targetStage,
+ string targetPrimPath,
+ string targetPropertyName)
+ {
+ return results.TryResolveBindingTarget(
+ targetStage, targetPrimPath, targetPropertyName, out UsdAttributeState? attribute)
+ && attribute != null
+ ? attribute.NodeId
+ : NodeId.Null;
+ }
+
+ ///
+ /// Authors Part 1's TargetNodeId onto a supplied binding node so a Server can
+ /// publish both target forms in one call: the caller has already set the
+ /// mandatory TargetPrimPath/TargetPropertyName pair, and this resolves the
+ /// same attribute to a NodeId and writes it as the optional TargetNodeId so
+ /// NodeId-resolving and path-resolving connectors agree (ยง10).
+ ///
+ ///
+ /// The binding is typed only as a and the member is addressed by
+ /// BrowseName, so Part 2 authors the Part 1 member without a compile-time dependency on
+ /// the Part 1 model (ยง4.2 โ "Neither model requires the other"). It fails closed: when
+ /// the path does not resolve to a materialized attribute, nothing is authored and it
+ /// returns false, so a binding never gains a TargetNodeId naming a node
+ /// that is not in the address space.
+ ///
+ /// The materialization the target lives in.
+ /// The server system context.
+ /// The Part 1 live-binding Object to author onto.
+ /// The absolute prim path.
+ /// The attribute name.
+ /// The namespace index of the Part 1 Bindings
+ /// companion, which owns the TargetNodeId BrowseName.
+ /// true when the target resolved and TargetNodeId was authored.
+ public static bool TryAuthorBindingTargetNodeId(
+ this UsdMaterializationResult result,
+ ISystemContext context,
+ NodeState binding,
+ string targetPrimPath,
+ string targetPropertyName,
+ ushort part1NamespaceIndex)
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+ if (binding is null)
+ {
+ throw new ArgumentNullException(nameof(binding));
+ }
+
+ NodeId target = result.ResolveBindingTargetNodeId(targetPrimPath, targetPropertyName);
+ if (target.IsNull)
+ {
+ return false;
+ }
+ SetTargetNodeId(context, binding, part1NamespaceIndex, target);
+ return true;
+ }
+
+ ///
+ /// Stage-aware overload of
+ /// :
+ /// the binding's TargetStage selects which of the Server's stages the target is
+ /// resolved in before the TargetNodeId is authored (ยง10).
+ ///
+ /// Every materialized stage the Server hosts.
+ /// The server system context.
+ /// The Part 1 live-binding Object to author onto.
+ /// The NodeId of the stage the binding targets.
+ /// The absolute prim path.
+ /// The attribute name.
+ /// The namespace index of the Part 1 Bindings
+ /// companion.
+ /// true when the target resolved and TargetNodeId was authored.
+ public static bool TryAuthorBindingTargetNodeId(
+ this IEnumerable results,
+ ISystemContext context,
+ NodeState binding,
+ NodeId targetStage,
+ string targetPrimPath,
+ string targetPropertyName,
+ ushort part1NamespaceIndex)
+ {
+ if (results is null)
+ {
+ throw new ArgumentNullException(nameof(results));
+ }
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+ if (binding is null)
+ {
+ throw new ArgumentNullException(nameof(binding));
+ }
+
+ NodeId target = results.ResolveBindingTargetNodeId(
+ targetStage, targetPrimPath, targetPropertyName);
+ if (target.IsNull)
+ {
+ return false;
+ }
+ SetTargetNodeId(context, binding, part1NamespaceIndex, target);
+ return true;
+ }
+
+ ///
+ /// Writes into the binding's TargetNodeId member,
+ /// reusing the member when it already exists (a generated Part 1 binding) or creating a
+ /// PropertyType Variable of DataType NodeId when it does not.
+ ///
+ private static void SetTargetNodeId(
+ ISystemContext context, NodeState binding, ushort part1NamespaceIndex, NodeId target)
+ {
+ var browseName = new QualifiedName(TargetNodeIdName, part1NamespaceIndex);
+ if (binding.FindChild(context, browseName) is BaseVariableState existing)
+ {
+ existing.Value = Variant.From(target);
+ return;
+ }
+
+ var property = new PropertyState(binding)
+ {
+ BrowseName = browseName,
+ DisplayName = new LocalizedText(TargetNodeIdName),
+ TypeDefinitionId = Opc.Ua.VariableTypeIds.PropertyType,
+ ReferenceTypeId = Opc.Ua.ReferenceTypeIds.HasProperty,
+ DataType = Opc.Ua.DataTypeIds.NodeId,
+ ValueRank = ValueRanks.Scalar,
+ AccessLevel = AccessLevels.CurrentRead,
+ UserAccessLevel = AccessLevels.CurrentRead,
+ Value = Variant.From(target)
+ };
+ binding.AddChild(property);
+ property.NodeId = context.NodeIdFactory.New(context, property);
+ }
+
+ private static FolderState EnsureFolder(
+ ISystemContext context, NodeState parent, string name, ushort ns)
+ {
+ var browseName = new QualifiedName(name, ns);
+ var children = new List();
+ parent.GetChildren(context, children);
+ foreach (BaseInstanceState child in children)
+ {
+ if (child is FolderState existing && existing.BrowseName == browseName)
+ {
+ return existing;
+ }
+ }
+
+ var folder = new FolderState(parent)
+ {
+ BrowseName = browseName,
+ DisplayName = new LocalizedText(name),
+ TypeDefinitionId = Opc.Ua.ObjectTypeIds.FolderType,
+ ReferenceTypeId = Opc.Ua.ReferenceTypeIds.HasComponent,
+ EventNotifier = EventNotifiers.None
+ };
+ parent.AddChild(folder);
+ folder.NodeId = context.NodeIdFactory.New(context, folder);
+ return folder;
+ }
+ }
+}
diff --git a/src/Opc.Ua.OpenUsdScene.Server/UsdSceneExporter.cs b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneExporter.cs
new file mode 100644
index 0000000000..b4114bdb75
--- /dev/null
+++ b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneExporter.cs
@@ -0,0 +1,503 @@
+/* ========================================================================
+ * 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;
+using System.Collections.Generic;
+using Opc.Ua.OpenUsdScene.Conversion;
+using Opc.Ua.OpenUsdScene.Scene;
+
+namespace Opc.Ua.OpenUsdScene.Server
+{
+ ///
+ /// Reads a materialized address space back into a scene document, the inverse of
+ /// (draft OPC UA โ OpenUSD Scene Materialization ยง7.2).
+ /// Combined with a .usda writer this closes the ยง7.4 round trip.
+ ///
+ public static class UsdSceneExporter
+ {
+ ///
+ /// Exports a materialized scene, recovering time samples from the materialization result so
+ /// sampled attributes round-trip (ยง7.2). This is the recommended overload after a
+ /// materialization, since the samples are held by the result rather than readable from the
+ /// nodes.
+ ///
+ /// The server system context.
+ /// The materialization result to export.
+ /// The reconstructed composed scene.
+ public static UsdStage ExportUsdStage(
+ this ISystemContext context, UsdMaterializationResult result)
+ {
+ if (result is null)
+ {
+ throw new ArgumentNullException(nameof(result));
+ }
+ return ExportUsdStageCore(
+ context, result.Stage, result.SamplesByNode, result.ConnectionsByNode);
+ }
+
+ ///
+ /// Exports a materialized UsdStageType Object into a scene document.
+ ///
+ /// The server system context.
+ /// The materialized stage.
+ ///
+ /// The time samples recovered per attribute node (from
+ /// ), or null when the caller has
+ /// none โ sampled attributes then export their default only. Keyed by node identity because
+ /// materialized attributes deliberately share the model's placeholder NodeId (ยง7.2).
+ ///
+ ///
+ /// Connections are recovered from each attribute's ConnectionPaths member, which the
+ /// materializer authors on the node itself, so this overload is now lossless for
+ /// connections too: authored order, multiplicity, and targets outside the materialized
+ /// subtree all survive. The browsable UsdConnection edges are used only as a
+ /// fallback for a stage that was not materialized by this library, where they cannot
+ /// recover authored order or an out-of-subtree target.
+ ///
+ /// The reconstructed composed scene.
+ public static UsdStage ExportUsdStage(
+ this ISystemContext context,
+ UsdStageState stageNode,
+ IReadOnlyDictionary>? timeSamples = null)
+ {
+ return ExportUsdStageCore(context, stageNode, timeSamples, null);
+ }
+
+ private static UsdStage ExportUsdStageCore(
+ ISystemContext context,
+ UsdStageState stageNode,
+ IReadOnlyDictionary>? timeSamples,
+ IReadOnlyDictionary>? connectionPaths)
+ {
+ if (context is null)
+ {
+ throw new ArgumentNullException(nameof(context));
+ }
+ if (stageNode is null)
+ {
+ throw new ArgumentNullException(nameof(stageNode));
+ }
+
+ var stage = new UsdStage(stageNode.BrowseName.Name ?? string.Empty)
+ {
+ DefaultPrim = ReadString(stageNode.DefaultPrim) ?? string.Empty,
+ UpAxis = ReadString(stageNode.UpAxis) ?? "Z",
+ MetersPerUnit = ReadDouble(stageNode.MetersPerUnit) ?? 1.0,
+ KilogramsPerUnit = ReadDouble(stageNode.KilogramsPerUnit),
+ TimeCodesPerSecond = ReadDouble(stageNode.TimeCodesPerSecond),
+ StartTimeCode = ReadDouble(stageNode.StartTimeCode),
+ EndTimeCode = ReadDouble(stageNode.EndTimeCode),
+ RootLayerIdentifier = ReadString(stageNode.RootLayerIdentifier) ?? string.Empty,
+ Documentation = ReadString(stageNode.Documentation) ?? string.Empty
+ };
+
+ // Two-pass export: an attribute connection can point at any attribute in the
+ // stage (ยง5.4), so the whole prim tree and a NodeId -> SdfPath index are built
+ // first, then connections are resolved once every attribute's path is known โ
+ // mirroring how the materializer resolves references in a second pass.
+ var attributePaths = new Dictionary();
+ var pendingConnections =
+ new List<(UsdAttribute Attribute, UsdAttributeState Node)>();
+
+ foreach (UsdPrimState child in ChildrenOfType(context, stageNode))
+ {
+ string childPath = "/" + (child.BrowseName.Name ?? string.Empty);
+ stage.AddRootPrim(ExportPrim(
+ context, child, childPath, attributePaths, pendingConnections,
+ timeSamples, connectionPaths));
+ }
+
+ ResolveExportedConnections(context, pendingConnections, attributePaths, connectionPaths);
+ return stage;
+ }
+
+ private static UsdPrim ExportPrim(
+ ISystemContext context,
+ UsdPrimState primNode,
+ string primPath,
+ Dictionary attributePaths,
+ List<(UsdAttribute Attribute, UsdAttributeState Node)> pendingConnections,
+ IReadOnlyDictionary>? timeSamples,
+ IReadOnlyDictionary>? connectionPaths)
+ {
+ string name = primNode.BrowseName.Name ?? string.Empty;
+ var prim = new UsdPrim(name, ReadString(primNode.TypeName) ?? string.Empty)
+ {
+ Specifier = ReadEnum(primNode.Specifier, UsdSpecifierEnum.Def),
+ Kind = ReadEnum(primNode.Kind, UsdPrimKindEnum.Unspecified),
+ Active = ReadBool(primNode.Active) ?? true,
+ Instanceable = ReadBool(primNode.Instanceable) ?? false,
+ Documentation = ReadString(primNode.Documentation) ?? string.Empty
+ };
+
+ foreach (UsdAttributeState attributeNode in
+ ChildrenOfType(context, primNode))
+ {
+ UsdAttribute attribute = ExportAttribute(attributeNode, timeSamples);
+ prim.Attributes.Add(attribute);
+ // Index the attribute by NodeId so any connection targeting it can be
+ // mapped back to ., the same key the materializer
+ // resolved connections against.
+ string attributePath =
+ primPath + "." + (attributeNode.BrowseName.Name ?? string.Empty);
+ attributePaths[attributeNode.NodeId] = attributePath;
+ pendingConnections.Add((attribute, attributeNode));
+ }
+ foreach (UsdRelationshipState relationshipNode in
+ ChildrenOfType(context, primNode))
+ {
+ prim.Relationships.Add(ExportRelationship(relationshipNode));
+ }
+
+ ExportComposition(context, primNode, prim);
+ ExportVariantSets(context, primNode, prim, timeSamples, connectionPaths);
+ ExportAppliedSchemas(context, primNode, prim);
+ ExportMetadata(context, primNode, prim);
+
+ foreach (UsdPrimState childNode in ChildrenOfType(context, primNode))
+ {
+ string childPath = primPath + "/" + (childNode.BrowseName.Name ?? string.Empty);
+ prim.AddChild(ExportPrim(
+ context, childNode, childPath, attributePaths, pendingConnections,
+ timeSamples, connectionPaths));
+ }
+ return prim;
+ }
+
+ private static UsdAttribute ExportAttribute(
+ UsdAttributeState attributeNode,
+ IReadOnlyDictionary>? timeSamples)
+ {
+ string name = attributeNode.BrowseName.Name ?? string.Empty;
+ // The UsdTypeName annotation is what makes the ยง6.2 mapping reversible: it records
+ // the exact SdfValueTypeName even where several USD types share one DataType.
+ string typeName = ReadString(attributeNode.UsdTypeName) ?? string.Empty;
+ var attribute = new UsdAttribute(name, typeName)
+ {
+ Variability = ReadEnum(attributeNode.Variability, UsdVariabilityEnum.Varying),
+ Custom = ReadBool(attributeNode.Custom) ?? false,
+ Interpolation = ReadString(attributeNode.Interpolation),
+ // ยง9 Mode A is a per-attribute property independent of whether the Server
+ // retains history: the materializer grants CurrentWrite to every live
+ // attribute but only sets Historizing when it also keeps the timeline.
+ // Infer liveness from the write access, so a Mode A attribute that is not
+ // historized still exports as live.
+ Live = (attributeNode.AccessLevel & Opc.Ua.AccessLevels.CurrentWrite) != 0,
+ // BoxingBehavior.Legacy unwraps an ArrayOf/MatrixOf to the T[]/T[,]
+ // shapes Decoerce consumes, so array, tuple and matrix values survive the
+ // round trip instead of being stringified as an opaque struct (ยง7.2).
+ Value = UsdValueCoercion.Decoerce(
+ attributeNode.Value.AsBoxedObject(Variant.BoxingBehavior.Legacy))
+ };
+
+ // ยง7.2: time samples are held by the materialization result, not the node, so recover
+ // them from the caller's node-keyed map. The authored default stays in Value above;
+ // the two are independent (an attribute may have either, both or neither).
+ if (timeSamples != null &&
+ timeSamples.TryGetValue(attributeNode, out IReadOnlyList? samples))
+ {
+ foreach (UsdTimeSample sample in samples)
+ {
+ attribute.TimeSamples[sample.TimeCode] = sample.Value;
+ }
+ }
+ return attribute;
+ }
+
+ private static void ResolveExportedConnections(
+ ISystemContext context,
+ List<(UsdAttribute Attribute, UsdAttributeState Node)> pending,
+ Dictionary attributePaths,
+ IReadOnlyDictionary>? connectionPaths)
+ {
+ NodeId connectionTypeId = ExpandedNodeId.ToNodeId(
+ Opc.Ua.OpenUsdScene.ReferenceTypeIds.UsdConnection, context.NamespaceUris);
+ var references = new List();
+ foreach ((UsdAttribute attribute, UsdAttributeState node) in pending)
+ {
+ // Authoritative: ConnectionPaths is authored on the node itself and carries every
+ // connection in order, including a target outside the materialized subtree that
+ // has no browsable edge. It is the connection counterpart of a relationship's
+ // TargetPaths (ยง5.4, ยง5.5, ยง7.4), so โ unlike the side channel below โ it works
+ // for a bare stage node too.
+ if (node.ConnectionPaths?.Value is { } declared && declared.Count > 0)
+ {
+ foreach (string path in declared)
+ {
+ attribute.Connections.Add(path);
+ }
+ continue;
+ }
+
+ // Compatibility: a materialization result may still carry the recorded order for
+ // a stage materialized before ConnectionPaths existed.
+ if (connectionPaths != null &&
+ connectionPaths.TryGetValue(node, out IReadOnlyList? authored))
+ {
+ foreach (string path in authored)
+ {
+ attribute.Connections.Add(path);
+ }
+ continue;
+ }
+
+ // Fallback (a bare stage node with no recorded side channel): rebuild from the
+ // browsable UsdConnection edges. Reference enumeration order is not the authored
+ // order, so sort by target SdfPath to make the exported sequence deterministic;
+ // targets outside the subtree cannot be recovered on this path.
+ references.Clear();
+ node.GetReferences(context, references, connectionTypeId, false);
+ var resolved = new List();
+ foreach (IReference reference in references)
+ {
+ NodeId targetId =
+ ExpandedNodeId.ToNodeId(reference.TargetId, context.NamespaceUris);
+ if (!targetId.IsNull &&
+ attributePaths.TryGetValue(targetId, out string? path))
+ {
+ resolved.Add(path);
+ }
+ }
+ resolved.Sort(StringComparer.Ordinal);
+ foreach (string path in resolved)
+ {
+ attribute.Connections.Add(path);
+ }
+ }
+ }
+
+ private static UsdRelationship ExportRelationship(UsdRelationshipState relationshipNode)
+ {
+ var relationship = new UsdRelationship(relationshipNode.BrowseName.Name ?? string.Empty)
+ {
+ Custom = ReadBool(relationshipNode.Custom) ?? false
+ };
+ // TargetPaths is authoritative: it also covers targets that lie outside the
+ // materialized subtree and therefore have no browsable edge (ยง5.5).
+ if (relationshipNode.TargetPaths?.Value is { } paths)
+ {
+ foreach (string path in paths)
+ {
+ relationship.Targets.Add(path);
+ }
+ }
+ return relationship;
+ }
+
+ private static void ExportComposition(
+ ISystemContext context, UsdPrimState primNode, UsdPrim prim)
+ {
+ if (primNode.Composition == null)
+ {
+ return;
+ }
+ foreach (UsdCompositionArcState arcNode in
+ ChildrenOfType(context, primNode.Composition))
+ {
+ var arc = new UsdCompositionArc(ReadEnum(arcNode.ArcKind, UsdArcKindEnum.Reference))
+ {
+ AssetPath = ReadString(arcNode.AssetPath) ?? string.Empty,
+ PrimPath = ReadString(arcNode.PrimPath) ?? string.Empty,
+ ListPosition = ReadEnum(arcNode.ListPosition, UsdListOpTypeEnum.Explicit),
+ VariantSet = ReadString(arcNode.VariantSet) ?? string.Empty,
+ VariantSelection = ReadString(arcNode.VariantSelection) ?? string.Empty
+ };
+ prim.Composition.Add(arc);
+ }
+ }
+
+ private static void ExportVariantSets(
+ ISystemContext context, UsdPrimState primNode, UsdPrim prim,
+ IReadOnlyDictionary>? timeSamples,
+ IReadOnlyDictionary>? connectionPaths)
+ {
+ if (primNode.VariantSets == null)
+ {
+ return;
+ }
+ foreach (UsdVariantSetState setNode in
+ ChildrenOfType(context, primNode.VariantSets))
+ {
+ var variantSet = new UsdVariantSet(
+ ReadString(setNode.SetName) ?? setNode.BrowseName.Name ?? string.Empty,
+ ReadString(setNode.Selection) ?? string.Empty);
+
+ // ยง5.6: recover every authored branch (a UsdPrimType child of the set)
+ // as a prim-shaped branch body, exporting each with the same machinery as a
+ // top-level prim. A branch is self-contained, so its attribute index and
+ // connections resolve within the branch alone.
+ foreach (UsdPrimState branchNode in ChildrenOfType(context, setNode))
+ {
+ var branchPaths = new Dictionary();
+ var branchPending =
+ new List<(UsdAttribute Attribute, UsdAttributeState Node)>();
+ string branchPath = "/" + (branchNode.BrowseName.Name ?? string.Empty);
+ UsdPrim branch = ExportPrim(
+ context, branchNode, branchPath, branchPaths, branchPending,
+ timeSamples, connectionPaths);
+ ResolveExportedConnections(
+ context, branchPending, branchPaths, connectionPaths);
+ variantSet.Variants.Add(branch);
+ }
+
+ prim.VariantSets.Add(variantSet);
+ }
+ }
+
+ private static void ExportAppliedSchemas(
+ ISystemContext context, UsdPrimState primNode, UsdPrim prim)
+ {
+ if (primNode.AppliedSchemas == null)
+ {
+ return;
+ }
+ foreach (UsdApiSchemaState schemaNode in
+ ChildrenOfType(context, primNode.AppliedSchemas))
+ {
+ // The portable georeference AddIns are additive provenance dual-authored by
+ // the materializer; they carry no opinion the vendor schema does not, so they
+ // are not re-emitted (Annex B.4).
+ if (schemaNode is UsdGeoreferenceApiState or UsdGlobeAnchorApiState)
+ {
+ continue;
+ }
+ string schemaName = ReadString(schemaNode.SchemaName)
+ ?? schemaNode.BrowseName.Name
+ ?? string.Empty;
+ var schema = new UsdApiSchema(schemaName);
+ if (schemaNode is UsdCollectionAPIState collection)
+ {
+ schema.ExpansionRule = ReadString(collection.ExpansionRule) ?? string.Empty;
+ }
+ prim.ApiSchemas.Add(schema);
+ }
+ }
+
+ private static void ExportMetadata(
+ ISystemContext context, UsdPrimState primNode, UsdPrim prim)
+ {
+ if (primNode.Metadata == null)
+ {
+ return;
+ }
+ ReadMetadataFolder(context, primNode.Metadata, prim.Metadata);
+ }
+
+ ///
+ /// Recovers a Metadata/ folder into a metadata dictionary (ยง6.3), the inverse of the
+ /// materializer's typed authoring. A leaf Property is read back in its own type โ Legacy
+ /// boxing unwraps an ArrayOf<T>/MatrixOf<T> and Decoerce
+ /// reverses the ยง6.2 coercion, so a value round-trips as itself rather than an opaque box
+ /// (the same fix as the attribute-value path). A nested Metadata folder is recovered as a
+ /// nested dictionary, so structured customData keeps its authored nesting to any depth.
+ ///
+ private static void ReadMetadataFolder(
+ ISystemContext context, NodeState folder, IDictionary into)
+ {
+ var children = new List();
+ folder.GetChildren(context, children);
+ foreach (BaseInstanceState child in children)
+ {
+ string key = child.BrowseName.Name ?? string.Empty;
+ if (key.Length == 0)
+ {
+ continue;
+ }
+ switch (child)
+ {
+ case BaseVariableState property:
+ into[key] = UsdValueCoercion.Decoerce(
+ property.Value.AsBoxedObject(Variant.BoxingBehavior.Legacy));
+ break;
+ case FolderState subFolder:
+ var nested = new Dictionary(StringComparer.Ordinal);
+ ReadMetadataFolder(context, subFolder, nested);
+ into[key] = nested;
+ break;
+ }
+ }
+ }
+
+ private static IEnumerable ChildrenOfType(ISystemContext context, NodeState parent)
+ where T : NodeState
+ {
+ var children = new List();
+ parent.GetChildren(context, children);
+ foreach (BaseInstanceState child in children)
+ {
+ if (child is T typed)
+ {
+ yield return typed;
+ }
+ }
+ }
+
+ private static string? ReadString(BaseVariableState? node)
+ {
+ if (node == null)
+ {
+ return null;
+ }
+ return node.Value.TryGetValue(out string value) ? value : null;
+ }
+
+ private static double? ReadDouble(BaseVariableState? node)
+ {
+ if (node == null)
+ {
+ return null;
+ }
+ return node.Value.TryGetValue(out double value) ? value : null;
+ }
+
+ private static bool? ReadBool(BaseVariableState? node)
+ {
+ if (node == null)
+ {
+ return null;
+ }
+ return node.Value.TryGetValue(out bool value) ? value : null;
+ }
+
+ private static TEnum ReadEnum(BaseVariableState? node, TEnum fallback)
+ where TEnum : struct, Enum
+ {
+ if (node == null)
+ {
+ return fallback;
+ }
+ if (node.Value.TryGetValue(out int raw) &&
+ Enum.IsDefined(typeof(TEnum), raw))
+ {
+ return (TEnum)Enum.ToObject(typeof(TEnum), raw);
+ }
+ return node.Value.TryGetValue(out TEnum typed) ? typed : fallback;
+ }
+ }
+}
diff --git a/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.Properties.cs b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.Properties.cs
new file mode 100644
index 0000000000..2922244d79
--- /dev/null
+++ b/src/Opc.Ua.OpenUsdScene.Server/UsdSceneMaterializer.Properties.cs
@@ -0,0 +1,811 @@
+/* ========================================================================
+ * 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;
+using System.Collections.Generic;
+using System.Globalization;
+using Opc.Ua.OpenUsdScene.Conversion;
+using Opc.Ua.OpenUsdScene.Scene;
+
+namespace Opc.Ua.OpenUsdScene.Server
+{
+ public static partial class UsdSceneMaterializer
+ {
+ private const string kCesiumLatitude = "cesium:anchor:latitude";
+ private const string kCesiumLongitude = "cesium:anchor:longitude";
+ private const string kCesiumHeight = "cesium:anchor:height";
+ private const string kCesiumGeoreference = "CesiumGeoreferencePrim";
+ private const string kCesiumGlobeAnchor = "CesiumGlobeAnchorAPI";
+
+ ///
+ /// WGS84 โ the CRS Cesium anchors are expressed in.
+ ///
+ private const int kEpsgWgs84 = 4326;
+
+ ///
+ /// Whether a prim is a *typed* Cesium georeference or globe-anchor prim
+ /// (def CesiumGeoreferencePrim โฆ / def CesiumGlobeAnchorAPI โฆ; ยง5.8,
+ /// Annex B.1). Such a prim needs its AppliedSchemas/ folder created โ and the
+ /// portable anchor dual-authored โ even when it carries no applied API schema.
+ ///
+ private static bool IsGeoreferenceTypedPrim(UsdPrim prim)
+ {
+ return string.Equals(prim.TypeName, kCesiumGeoreference, StringComparison.Ordinal) ||
+ string.Equals(prim.TypeName, kCesiumGlobeAnchor, StringComparison.Ordinal);
+ }
+
+ private static UsdAttributeState MaterializeAttribute(
+ ISystemContext context,
+ UsdPrimState primNode,
+ UsdAttribute attribute,
+ ushort ns,
+ UsdMaterializationOptions options)
+ {
+ UsdAttributeState node = primNode.AddxUsdAttribute_(
+ context, new QualifiedName(attribute.Name, ns));
+
+ UsdValueTypeMapping mapping = UsdValueTypeMap.Map(
+ attribute.TypeName, context.NamespaceUris);
+ uint components = UsdValueTypeMap.ComponentCount(attribute.TypeName);
+
+ node.DataType = mapping.DataTypeId;
+ node.ValueRank = mapping.ValueRank;
+ if (mapping.ArrayDimensions != null)
+ {
+ node.ArrayDimensions = mapping.ArrayDimensions;
+ }
+
+ if (UsdValueCoercion.TryCoerce(attribute.Value, mapping, components, out Variant variant))
+ {
+ node.Value = variant;
+ }
+
+ // The exact SdfValueTypeName is always retained, which is what makes the ยง6.2
+ // mapping reversible even where several USD types share one OPC UA DataType.
+ node.CreateOrReplaceUsdTypeName(context, null!).Value = attribute.TypeName;
+ node.CreateOrReplaceVariability(context, null!).Value = attribute.Variability;
+ if (attribute.Custom)
+ {
+ node.CreateOrReplaceCustom(context, null!).Value = true;
+ }
+ if (!string.IsNullOrEmpty(attribute.Namespace))
+ {
+ node.CreateOrReplaceNamespace(context, null!).Value = attribute.Namespace;
+ }
+ if (!string.IsNullOrEmpty(attribute.Interpolation))
+ {
+ node.CreateOrReplaceInterpolation(context, null!).Value = attribute.Interpolation;
+ }
+
+ if (attribute.Live)
+ {
+ // Mode A (ยง9): the Value is server-maintained and time-varying, so a
+ // Subscription delivers changes and โ where the Server retains it โ
+ // HistoryRead exposes the value timeline.
+ node.AccessLevel = AccessLevels.CurrentReadOrWrite;
+ node.UserAccessLevel = AccessLevels.CurrentReadOrWrite;
+ if (options.HistorizeLiveAttributes)
+ {
+ node.Historizing = true;
+ node.AccessLevel |= AccessLevels.HistoryRead;
+ node.UserAccessLevel |= AccessLevels.HistoryRead;
+ }
+ }
+
+ // ยง7.1 step 3: an attribute that carries time samples materializes its authored
+ // default as Value (already set above) and exposes the samples through
+ // HistoricalAccess. Time samples make the node historizing independently of Live โ
+ // the samples ARE the retained timeline โ so mark it read + HistoryRead. The samples
+ // themselves are recorded by the caller into the materialization result, from where a
+ // hosting Server binds them to its historian and the exporter recovers them (ยง7.2).
+ if (attribute.TimeSamples.Count > 0)
+ {
+ node.Historizing = true;
+ node.AccessLevel |= AccessLevels.CurrentRead | AccessLevels.HistoryRead;
+ node.UserAccessLevel |= AccessLevels.CurrentRead | AccessLevels.HistoryRead;
+ }
+
+ return node;
+ }
+
+ private static UsdRelationshipState MaterializeRelationship(
+ ISystemContext context,
+ UsdPrimState primNode,
+ UsdRelationship relationship,
+ ushort ns)
+ {
+ UsdRelationshipState node = primNode.AddxUsdRelationship_(
+ context, new QualifiedName(relationship.Name, ns));
+
+ var paths = new string[relationship.Targets.Count];
+ relationship.Targets.CopyTo(paths, 0);
+ node.CreateOrReplaceTargetPaths(context, null!).Value = paths;
+ if (relationship.Custom)
+ {
+ node.CreateOrReplaceCustom(context, null!).Value = true;
+ }
+ return node;
+ }
+
+ private static void ResolveRelationshipTargets(
+ ISystemContext context,
+ List<(UsdRelationshipState Node, UsdRelationship Source)> pending,
+ Dictionary prims,
+ Dictionary attributes)
+ {
+ foreach ((UsdRelationshipState node, UsdRelationship source) in pending)
+ {
+ var resolved = new List(source.Targets.Count);
+ foreach (string path in source.Targets)
+ {
+ NodeState? target = null;
+ if (prims.TryGetValue(path, out UsdPrimState? prim))
+ {
+ target = prim;
+ }
+ else if (attributes.TryGetValue(path, out UsdAttributeState? attribute))
+ {
+ target = attribute;
+ }
+ if (target == null)
+ {
+ // ยง5.5 / ยง7.1 step 4 require the targets to stay ordered, so Targets
+ // must correspond positionally to TargetPaths. A target outside the
+ // materialized subtree keeps full fidelity through TargetPaths; here it
+ // takes a null-NodeId placeholder so the two arrays remain index-aligned
+ // (entry i of one describes the same target as entry i of the other) and
+ // only resolvable targets additionally become browsable edges.
+ resolved.Add(NodeId.Null);
+ continue;
+ }
+ resolved.Add(target.NodeId);
+ node.AddReference(
+ ExpandedNodeId.ToNodeId(
+ Opc.Ua.OpenUsdScene.ReferenceTypeIds.UsdRelationshipTarget,
+ context.NamespaceUris),
+ false,
+ target.NodeId);
+ }
+ node.CreateOrReplaceTargets(context, null!).Value = resolved.ToArray();
+ }
+ }
+
+ private static void ResolveConnections(
+ ISystemContext context,
+ List<(UsdAttributeState Node, UsdAttribute Source)> pending,
+ Dictionary attributes)
+ {
+ NodeId connectionTypeId = ExpandedNodeId.ToNodeId(
+ Opc.Ua.OpenUsdScene.ReferenceTypeIds.UsdConnection, context.NamespaceUris);
+ foreach ((UsdAttributeState node, UsdAttribute source) in pending)
+ {
+ // ConnectionPaths is the connection counterpart of UsdRelationshipType's
+ // TargetPaths (ยง5.4/ยง5.5): it carries every authored SdfPath in order, so a
+ // connection whose target lies outside the materialized subtree โ and therefore
+ // has no browsable edge โ still survives an export, and authored order and
+ // multiplicity are preserved on the node itself rather than out of band.
+ var authored = new string[source.Connections.Count];
+ source.Connections.CopyTo(authored, 0);
+ node.CreateOrReplaceConnectionPaths(context, null!).Value = authored;
+
+ // Materialized attributes deliberately share the model's placeholder NodeId
+ // (xUsdAttribute_, i=6023), so two connections authored on one attribute can
+ // resolve to the same target NodeId. A forward reference is keyed by
+ // (type, isInverse, target), so adding that target twice would throw; dedupe the
+ // browsable edges by target NodeId. These edges exist only for ยง5.4
+ // browsability โ ConnectionPaths above is what an export relies on.
+ var linked = new HashSet();
+ foreach (string path in source.Connections)
+ {
+ if (attributes.TryGetValue(path, out UsdAttributeState? target) &&
+ linked.Add(target.NodeId))
+ {
+ node.AddReference(connectionTypeId, false, target.NodeId);
+ }
+ }
+ }
+ }
+
+ private static void MaterializeComposition(
+ ISystemContext context, UsdPrimState primNode, UsdPrim prim, ushort ns)
+ {
+ FolderState folder = primNode.CreateOrReplaceComposition(context, null!);
+ EnsureNodeId(context, folder);
+
+ int index = 0;
+ foreach (UsdCompositionArc arc in prim.Composition)
+ {
+ // string.Create(IFormatProvider, ...) is .NET 6+; concatenation with an
+ // invariant-culture index keeps the Server compiling on net472/net48/
+ // netstandard2.1 while staying culture-independent.
+ string name = arc.ArcKind.ToString() + "_" +
+ index.ToString(CultureInfo.InvariantCulture);
+ index++;
+ UsdCompositionArcState node = context.CreateInstanceOfUsdCompositionArcType(
+ folder, new QualifiedName(name, ns));
+ Attach(context, folder, node, Opc.Ua.ReferenceTypeIds.HasComponent);
+
+ node.CreateOrReplaceArcKind(context, null!).Value = arc.ArcKind;
+ if (!string.IsNullOrEmpty(arc.AssetPath))
+ {
+ node.CreateOrReplaceAssetPath(context, null!).Value = arc.AssetPath;
+ }
+ if (!string.IsNullOrEmpty(arc.PrimPath))
+ {
+ node.CreateOrReplacePrimPath(context, null!).Value = arc.PrimPath;
+ }
+ node.CreateOrReplaceListPosition(context, null!).Value = arc.ListPosition;
+ if (!string.IsNullOrEmpty(arc.VariantSet))
+ {
+ node.CreateOrReplaceVariantSet(context, null!).Value = arc.VariantSet;
+ }
+ if (!string.IsNullOrEmpty(arc.VariantSelection))
+ {
+ node.CreateOrReplaceVariantSelection(context, null!).Value = arc.VariantSelection;
+ }
+ }
+ }
+
+ private static void MaterializeVariantSets(
+ ISystemContext context, UsdPrimState primNode, UsdPrim prim, ushort ns,
+ UsdMaterializationOptions options, UsdMaterializationRecorder recorder)
+ {
+ FolderState folder = primNode.CreateOrReplaceVariantSets(context, null!);
+ EnsureNodeId(context, folder);
+
+ foreach (UsdVariantSet variantSet in prim.VariantSets)
+ {
+ UsdVariantSetState node = context.CreateInstanceOfUsdVariantSetType(
+ folder, new QualifiedName(variantSet.SetName, ns));
+ Attach(context, folder, node, Opc.Ua.ReferenceTypeIds.HasComponent);
+ node.CreateOrReplaceSetName(context, null!).Value = variantSet.SetName;
+ node.CreateOrReplaceSelection(context, null!).Value = variantSet.Selection;
+
+ // ยง5.6: a UsdVariantSetType exposes its variant *branches* as
+ // OptionalPlaceholder children (UsdPrimType). Every captured branch is authored โ
+ // not only the selection โ so the full branch structure the model defines is
+ // materialized. A set that recorded only a resolved Selection (no captured
+ // branches) authors no branch rather than inventing one from the selection name
+ // (fail closed); its selection is still carried by the Selection property above.
+ foreach (UsdPrim branch in variantSet.Variants)
+ {
+ UsdPrimState branchNode = node.AddxVariant_(
+ context, new QualifiedName(branch.Name, ns));
+ // AddxVariant_ leaves the type's placeholder NodeId (i=6055); force a fresh
+ // per-instance NodeId so branches on different sets never collide, matching
+ // how the CreateInstanceOf* factories mint instance ids.
+ context.AssignInstanceNodeId(branchNode);
+
+ // A branch body is prim-shaped and self-contained: its relationships and
+ // connections address paths *within the branch*, so it is populated and
+ // resolved against an isolated local index. That keeps branch-internal edges
+ // working while never leaking branch paths into the composed-scene index the
+ // ยง7.4 round trip and Part 1 bindings resolve against. Sampled and connected
+ // branch attributes are still recorded into the shared recorder (node-keyed)
+ // so they round-trip through the exporter.
+ var branchPrims = new Dictionary(StringComparer.Ordinal);
+ var branchAttributes =
+ new Dictionary(StringComparer.Ordinal);
+ var branchRelationships = new List<(UsdRelationshipState, UsdRelationship)>();
+ var branchConnections = new List<(UsdAttributeState, UsdAttribute)>();
+
+ PopulatePrimNode(
+ context, branchNode, branch, "/" + branch.Name, ns, options,
+ branchPrims, branchAttributes, branchRelationships, branchConnections,
+ recorder);
+
+ ResolveRelationshipTargets(
+ context, branchRelationships, branchPrims, branchAttributes);
+ ResolveConnections(context, branchConnections, branchAttributes);
+ }
+ }
+ }
+
+ private static void MaterializeMetadata(
+ ISystemContext context, UsdPrimState primNode, UsdPrim prim, ushort ns)
+ {
+ FolderState folder = primNode.CreateOrReplaceMetadata(context, null!);
+ EnsureNodeId(context, folder);
+ MaterializeMetadataEntries(context, folder, prim.Metadata, ns);
+ }
+
+ ///
+ /// Populates a Metadata/ folder from a set of authored entries (ยง6.3). Each scalar
+ /// or array entry becomes a Property carrying its value in its own type โ chosen per value
+ /// rather than stringified โ so a client reads the authored kind and the exporter recovers
+ /// it. A nested dictionary becomes a nested Metadata folder, so structured customData
+ /// is browsable as authored; this recurses to whatever depth the source nests.
+ ///
+ private static void MaterializeMetadataEntries(
+ ISystemContext context,
+ NodeState folder,
+ IEnumerable> entries,
+ ushort ns)
+ {
+ foreach (KeyValuePair entry in entries)
+ {
+ if (string.IsNullOrEmpty(entry.Key))
+ {
+ continue;
+ }
+
+ if (TryAsNestedDictionary(
+ entry.Value, out IEnumerable> nested))
+ {
+ var subFolder = new FolderState(folder)
+ {
+ BrowseName = new QualifiedName(entry.Key, ns),
+ DisplayName = new LocalizedText(entry.Key),
+ TypeDefinitionId = Opc.Ua.ObjectTypeIds.FolderType,
+ ReferenceTypeId = Opc.Ua.ReferenceTypeIds.Organizes
+ };
+ folder.AddChild(subFolder);
+ subFolder.NodeId = context.NodeIdFactory.New(context, subFolder);
+ MaterializeMetadataEntries(context, subFolder, nested, ns);
+ continue;
+ }
+
+ var property = new PropertyState(folder)
+ {
+ BrowseName = new QualifiedName(entry.Key, ns),
+ DisplayName = new LocalizedText(entry.Key),
+ TypeDefinitionId = Opc.Ua.VariableTypeIds.PropertyType,
+ ReferenceTypeId = Opc.Ua.ReferenceTypeIds.HasProperty,
+ DataType = Opc.Ua.DataTypeIds.BaseDataType,
+ ValueRank = ValueRanks.Scalar
+ };
+ if (entry.Value != null)
+ {
+ if (TryCoerceMetadataValue(
+ entry.Value, out Variant variant, out NodeId dataType, out int valueRank))
+ {
+ // A recognised value keeps its type through the round trip, so the
+ // exporter recovers the authored object rather than an opaque string (ยง6.3).
+ property.Value = variant;
+ property.DataType = dataType;
+ property.ValueRank = valueRank;
+ }
+ else
+ {
+ // A value of no representable type is carried as its invariant textual
+ // form rather than dropped โ the ยง6.3 last resort, not a typed guess.
+ property.Value = Variant.From(
+ entry.Value as string ??
+ Convert.ToString(entry.Value, CultureInfo.InvariantCulture) ??
+ string.Empty);
+ property.DataType = Opc.Ua.DataTypeIds.String;
+ }
+ }
+ folder.AddChild(property);
+ property.NodeId = context.NodeIdFactory.New(context, property);
+ }
+ }
+
+ ///
+ /// Whether a metadata value is a nested dictionary (structured customData), and if so
+ /// its entries. Only string-keyed dictionaries qualify; anything else is a leaf value.
+ ///
+ private static bool TryAsNestedDictionary(
+ object? value, out IEnumerable> entries)
+ {
+ switch (value)
+ {
+ case IReadOnlyDictionary readOnly:
+ entries = readOnly;
+ return true;
+ case IDictionary readWrite:
+ entries = readWrite;
+ return true;
+ default:
+ entries = Array.Empty>();
+ return false;
+ }
+ }
+
+ ///
+ /// Chooses a Variant, DataType and ValueRank for a leaf metadata value from its CLR type,
+ /// so a scalar keeps its exact type and an array keeps its (inferred) element type through
+ /// the materializeโexport round trip (ยง6.3). Returns false for a scalar whose type is
+ /// not representable, so the caller carries its textual form instead of guessing.
+ ///
+ private static bool TryCoerceMetadataValue(
+ object value, out Variant variant, out NodeId dataType, out int valueRank)
+ {
+ valueRank = ValueRanks.Scalar;
+ switch (value)
+ {
+ case bool v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Boolean; return true;
+ case sbyte v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.SByte; return true;
+ case byte v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Byte; return true;
+ case short v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Int16; return true;
+ case ushort v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.UInt16; return true;
+ case int v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Int32; return true;
+ case uint v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.UInt32; return true;
+ case long v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Int64; return true;
+ case ulong v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.UInt64; return true;
+ case float v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Float; return true;
+ case double v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.Double; return true;
+ case string v: variant = Variant.From(v); dataType = Opc.Ua.DataTypeIds.String; return true;
+ case System.Collections.IEnumerable sequence:
+ return TryCoerceMetadataArray(sequence, out variant, out dataType, out valueRank);
+ default:
+ variant = default;
+ dataType = Opc.Ua.DataTypeIds.String;
+ return false;
+ }
+ }
+
+ ///
+ /// Coerces a metadata array or list into a typed 1-D Variant, inferring the element type
+ /// from the first non-null element. A homogeneous numeric or boolean array keeps its
+ /// element type; an empty or mixed sequence is carried as a string array so it round-trips
+ /// as a sequence rather than being dropped (ยง6.3, fail closed โ no numeric guess).
+ ///
+ private static bool TryCoerceMetadataArray(
+ System.Collections.IEnumerable sequence,
+ out Variant variant,
+ out NodeId dataType,
+ out int valueRank)
+ {
+ valueRank = ValueRanks.OneDimension;
+ var items = new List