Files
UA-.NETStandard/samples/Robotics/MinimalRobotServer/OpenUsdRepresentation.cs
Marc Schier 6795073d2d Mount OpenUSD representations with HasAddIn (#4239)
# Description

Mount every `OpenUsdRepresentation` AddIn with `HasAddIn` rather than
plain
`HasComponent`.

- Correct the `IOpenUsdRepresentedType` placeholder in the OpenUSD
NodeSet.
- Route Pump, Minimal Robot, and Intent sample authoring through the
shared
  `CreateRepresentation` helper.
- Remove redundant representation subtree rebasing from sample call
sites.
- Add model, in-process sample, and over-the-wire regression coverage.
- Update the OpenUSD documentation and sample diagrams.

`HasAddIn` is a subtype of `HasComponent`, so the incorrect reference
still
browsed and rendered successfully but failed companion-model
conformance.

## Related Issues

No related issue.

## Validation

- `Opc.Ua.OpenUsd.Tests`: 914 passed on .NET 10.
- Pump and Minimal Robot sample builds completed without warnings.
- Pump and Robot `RepresentationsAreMountedWithHasAddInAsync` E2E tests
passed.

## Checklist

- [ ] I have signed the
[CLA](https://opcfoundation.org/license/cla/ContributorLicenseAgreementv1.0.pdf)
and read the
[CONTRIBUTING](https://github.com/OPCFoundation/UA-.NETStandard/blob/master/CONTRIBUTING.md)
doc.
- [x] I have added tests that prove my fix is effective or that my
feature works and increased code coverage.
- [x] I have added all necessary documentation.
- [x] I have verified that my changes do not introduce (new) build or
analyzer warnings.
- [ ] I ran **all** tests locally using the **UA.slnx** solution against
at least .net **framework** and .net **10**, and all passed.
- [ ] I fixed **all** failing and flaky tests in the CI pipelines and
**all** CodeQL warnings.
- [x] I have addressed **all** PR feedback received.

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 19119d71-e1bc-41c9-ba0d-2d1dea437167
2026-08-12 11:31:55 +02:00

308 lines
14 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* ========================================================================
* 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;
namespace Robotics
{
/// <summary>
/// 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.
/// </summary>
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<ServedAsset> 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);
}
}
/// <summary>
/// Loads the embedded artist-authored USD layers this server serves (spec §5.15).
/// </summary>
/// <returns></returns>
private static List<ServedAsset> 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();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="owner"></param>
/// <param name="primPath"></param>
/// <param name="ns"></param>
/// <returns></returns>
private OpenUsdRepresentationState AttachRepresentation(
NodeState owner, string primPath, ushort ns)
{
OpenUsdRepresentationState rep = SystemContext.CreateRepresentation(
owner, m_cellStage!.NodeId, primPath, ns);
return rep;
}
/// <summary>
/// Adds the Organizes reference pair between the discovery registry and a
/// representation, so a generic connector discovers it without domain knowledge.
/// </summary>
/// <param name="rep"></param>
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);
}
/// <summary>
/// Instantiates the OpenUsdLiveBinding <Binding> 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.
/// </summary>
/// <param name="rep"></param>
/// <param name="ns"></param>
/// <param name="name"></param>
/// <param name="bindingDefinitionId"></param>
/// <param name="sourceNodeId"></param>
/// <param name="targetPrimPath"></param>
/// <param name="targetPropertyName"></param>
/// <param name="targetUsdTypeName"></param>
/// <param name="kind"></param>
/// <param name="scale"></param>
/// <param name="bindingTypeId"></param>
/// <param name="signalRole"></param>
/// <param name="sourceSemanticId"></param>
/// <param name="alarmAspect"></param>
/// <param name="commandTargetNodeId"></param>
/// <param name="commandTriggerPropertyName"></param>
/// <returns></returns>
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;
}
/// <summary>
/// Instantiates the OpenUsdComponentBinding <Component> 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.
/// </summary>
/// <param name="rep"></param>
/// <param name="ns"></param>
/// <param name="name"></param>
/// <param name="bindingDefinitionId"></param>
/// <param name="cardinality"></param>
/// <param name="arc"></param>
/// <param name="targetPrimPath"></param>
/// <param name="componentRepresentation"></param>
/// <param name="assetReference"></param>
/// <param name="dynamic"></param>
/// <param name="changeEventSource"></param>
/// <param name="componentTypeDefinition"></param>
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);
}
}