/* ========================================================================
* 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
{
///
/// 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.CreateRepresentation(
owner, m_cellStage!.NodeId, primPath, ns);
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);
}
}