/* ======================================================================== * 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); } }