diff --git a/docs/OpenUsd.md b/docs/OpenUsd.md index 727ee8ef70..c1970795ba 100644 --- a/docs/OpenUsd.md +++ b/docs/OpenUsd.md @@ -210,10 +210,40 @@ same `HasAddIn` contract. `CreateRepresentation` is the supported authoring path `CreateInstanceOfOpenUsdRepresentationType` and a manually assigned reference type. The sample servers assert the reference type in their E2E suites. -`UsdAssetDelivery.AttachStageAssets(context, stage, openUsdNs, assets)` serves artist-authored USD layers through the -address space as read-only Part 5 files with SHA-256 digests, so a generic connector can fetch, verify, cache, and -render the twin with no external asset resolver. `assets` is an `ArrayOf` and the method returns the -created `OpenUsdAssetState` nodes as an `ArrayOf`. +The binding model tracks the OPC 12001-1 version 0.9.0 draft from `OPC12000-Metaverse` and now depends on the +stack's xRegistry companion model. The local `IOpenUsdRepresentedType` placeholder retains its `HasAddIn` +correction. Load `nodes.AddOpcUaXRegistry(context)` before `nodes.AddOpcUaOpenUsd(context)` and include both +model namespace URIs in the node manager. + +`UsdAssetDelivery.AttachStageAssets(context, stage, openUsdNs, assets, "pumps")` bootstraps an asset container in +`Server/OpenUSD/Artifacts`. The registry organizes typed asset groups; each group organizes stable logical +`OpenUsdAssetType` Resources. Each Resource owns a typed `Versions` folder organizing its first exact Version, +`1`. Both logical and exact files expose the same immutable bytes through read-only Part 5 streaming, with +SHA-256 digests and xRegistry identity metadata. The registry also exposes `SpecVersion` and a read-only +`Capabilities` document declaring `available.entities.mutable` as `false` and an empty `mutable` list. +Runtime registration, deletion, and default-Version switching are not implemented by +this bootstrap helper. + +The stage's `Assets` folder contains only explicit `Organizes` references to the logical Resources, never +duplicate or owned file instances. Stages using the same container and normalized `AssetIdentifier` share the +same Resource NodeId. Leading `./` segments are removed; resource BrowseNames use `XRegistryIdentifier`, including +collision disambiguation, rather than the previous underscore substitution. Conflicting bytes, asset kinds, +or content types for an existing Resource are rejected. Empty asset identifiers are rejected. + +`assets` is an `ArrayOf`; the return value is an `ArrayOf` containing the logical +Resources in input order. The four-argument overload allocates a container identifier from the stage's name, +disambiguates collisions with existing groups, and reuses the allocation for that stage instance. Use the +explicit-container overload for intentional sharing. Call either overload before the facility's final +NodeId assignment and registration. +Supply the stage's complete layer closure, with exactly one `RootLayer` whose `AssetIdentifier` equals +`RootLayerIdentifier`. The sample servers now publish their served `.usda` identifiers rather than unrelated +`asset-repo/*.usd` paths. + +Mount each Part 1 stage under `Server/OpenUSD/Stages` with `ReferenceTypeId = ReferenceTypeIds.Organizes` +before calling `AddChild`. The instance factory leaves the reference type unset; `AddChild` alone registers +the child but does not make it browsable. A representation's NodeId-valued `Stage` property does not replace +this hierarchy link. The registry hierarchy and the stage's non-owning `Assets` view let a hierarchy-only +address-space snapshot discover the stage and its shared asset closure. ## Samples diff --git a/samples/Di/PumpDeviceIntegrationServer/OpenUsdRepresentation.cs b/samples/Di/PumpDeviceIntegrationServer/OpenUsdRepresentation.cs index 7ad9426250..ad79172b73 100644 --- a/samples/Di/PumpDeviceIntegrationServer/OpenUsdRepresentation.cs +++ b/samples/Di/PumpDeviceIntegrationServer/OpenUsdRepresentation.cs @@ -176,7 +176,7 @@ namespace Pumps } } - private const string PlantRootLayerIdentifier = "asset-repo/Plant.usd"; + private const string PlantRootLayerIdentifier = "Plant.usda"; /// /// Prim scope the configured pumps are composed into. Each pump owns @@ -222,6 +222,7 @@ namespace Pumps m_plantStage = SystemContext.CreateInstanceOfOpenUsdStageType( stages, new QualifiedName("PlantStage", ns)); + m_plantStage.ReferenceTypeId = ReferenceTypeIds.Organizes; stages.AddChild(m_plantStage); m_plantStage.CreateOrReplaceRootLayerIdentifier(SystemContext, null!) .Value = PlantRootLayerIdentifier; @@ -259,7 +260,7 @@ namespace Pumps // §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); + UsdAssetDelivery.AttachStageAssets(SystemContext, m_plantStage, ns, servedAssets, "pumps"); SystemContext.AssignInstanceChildNodeIds(root); await AddPredefinedNodeAsync(SystemContext, root, cancellationToken) diff --git a/samples/Di/PumpDeviceIntegrationServer/PumpNodeManager.cs b/samples/Di/PumpDeviceIntegrationServer/PumpNodeManager.cs index 16197ae388..8317c64d1d 100644 --- a/samples/Di/PumpDeviceIntegrationServer/PumpNodeManager.cs +++ b/samples/Di/PumpDeviceIntegrationServer/PumpNodeManager.cs @@ -44,6 +44,7 @@ using Opc.Ua.Pumps; using Opc.Ua.Server; using Opc.Ua.Server.Fluent; using Opc.Ua.Server.NodeManager; +using Opc.Ua.XRegistry; namespace Pumps { @@ -99,7 +100,8 @@ namespace Pumps postSetupRunner, Opc.Ua.Pumps.Namespaces.Pumps, Opc.Ua.Machinery.Namespaces.Machinery, - Opc.Ua.OpenUsd.Namespaces.OpenUSD) + Opc.Ua.OpenUsd.Namespaces.OpenUSD, + Opc.Ua.XRegistry.Namespaces.xRegistry) { m_options = options?.Value ?? new PumpDeviceIntegrationOptions(); if (m_options.PumpCount is < 1 or > 100) @@ -173,6 +175,7 @@ namespace Pumps nodes.AddOpcUaDi(context); nodes.AddOpcUaMachinery(context); nodes.AddOpcUaPumps(context); + nodes.AddOpcUaXRegistry(context); nodes.AddOpcUaOpenUsd(context); return new ValueTask(nodes); } @@ -538,7 +541,8 @@ namespace Pumps Opc.Ua.Pumps.Namespaces.Pumps, Opc.Ua.Machinery.Namespaces.Machinery, Opc.Ua.Di.Namespaces.OpcUaDi, - Opc.Ua.OpenUsd.Namespaces.OpenUSD + Opc.Ua.OpenUsd.Namespaces.OpenUSD, + Opc.Ua.XRegistry.Namespaces.xRegistry }; /// diff --git a/samples/OpenUsd/GeneratorServer/GeneratorNodeManager.cs b/samples/OpenUsd/GeneratorServer/GeneratorNodeManager.cs index 829fdce939..0dfad4ed0b 100644 --- a/samples/OpenUsd/GeneratorServer/GeneratorNodeManager.cs +++ b/samples/OpenUsd/GeneratorServer/GeneratorNodeManager.cs @@ -46,6 +46,7 @@ using Opc.Ua.OpenUsd; using Opc.Ua.Server; using Opc.Ua.Server.Fluent; using Opc.Ua.Server.NodeManager; +using Opc.Ua.XRegistry; namespace Generators { @@ -148,7 +149,8 @@ namespace Generators // model reaches into IA. The reduced Machinery copy this // sample used to carry had that edge stripped out. Opc.Ua.IA.Namespaces.IA, - Opc.Ua.OpenUsd.Namespaces.OpenUSD) + Opc.Ua.OpenUsd.Namespaces.OpenUSD, + Opc.Ua.XRegistry.Namespaces.xRegistry) { m_options = options?.Value ?? new GeneratorDeviceIntegrationOptions(); if (m_options.GeneratorCount is < 1 or > 100) @@ -229,6 +231,7 @@ namespace Generators nodes.AddOpcUaIA(context); nodes.AddOpcUaMachinery(context); nodes.AddOpcUaGenerators(context); + nodes.AddOpcUaXRegistry(context); nodes.AddOpcUaOpenUsd(context); return new ValueTask(nodes); } @@ -564,6 +567,7 @@ namespace Generators Opc.Ua.Generators.Namespaces.Generators, Opc.Ua.Machinery.Namespaces.Machinery, Opc.Ua.OpenUsd.Namespaces.OpenUSD, + Opc.Ua.XRegistry.Namespaces.xRegistry, }.ToArrayOf(); /// diff --git a/samples/OpenUsd/GeneratorServer/OpenUsdRepresentation.cs b/samples/OpenUsd/GeneratorServer/OpenUsdRepresentation.cs index 16a0bef4bc..2cd18dc9a6 100644 --- a/samples/OpenUsd/GeneratorServer/OpenUsdRepresentation.cs +++ b/samples/OpenUsd/GeneratorServer/OpenUsdRepresentation.cs @@ -98,6 +98,7 @@ namespace Generators m_powerhouseStage = SystemContext.CreateInstanceOfOpenUsdStageType( stages, new QualifiedName("PowerhouseStage", ns)); + m_powerhouseStage.ReferenceTypeId = ReferenceTypeIds.Organizes; stages.AddChild(m_powerhouseStage); m_powerhouseStage.CreateOrReplaceRootLayerIdentifier(SystemContext, null!) .Value = PowerhouseRootLayerIdentifier; @@ -125,7 +126,7 @@ namespace Generators // matching forward reference is added through externalReferences. root.AddReference(ReferenceTypeIds.HasComponent, true, Opc.Ua.ObjectIds.Server); - UsdAssetDelivery.AttachStageAssets(SystemContext, m_powerhouseStage, ns, servedAssets); + UsdAssetDelivery.AttachStageAssets(SystemContext, m_powerhouseStage, ns, servedAssets, "generators"); SystemContext.AssignInstanceChildNodeIds(root); await AddPredefinedNodeAsync(SystemContext, root, cancellationToken) diff --git a/samples/OpenUsd/SiteCompositionServer/SiteNodeManager.cs b/samples/OpenUsd/SiteCompositionServer/SiteNodeManager.cs index 22ed5cd717..b6ca98bcb1 100644 --- a/samples/OpenUsd/SiteCompositionServer/SiteNodeManager.cs +++ b/samples/OpenUsd/SiteCompositionServer/SiteNodeManager.cs @@ -40,6 +40,7 @@ using Opc.Ua.OpenUsd.Server; using Opc.Ua.Server; using Opc.Ua.Server.Fluent; using Opc.Ua.Server.NodeManager; +using Opc.Ua.XRegistry; namespace SiteComposition { @@ -81,7 +82,7 @@ namespace SiteComposition /// public sealed partial class SiteNodeManager : FluentNodeManagerBase { - private const string SiteRootLayerIdentifier = "asset-repo/Site.usd"; + private const string SiteRootLayerIdentifier = "Site.usda"; private const string SitePrimPath = "/Site"; private const string PumpHallPrimPath = "/Site/PumpHall"; private const string PowerhousePrimPath = "/Site/Powerhouse"; @@ -108,7 +109,9 @@ namespace SiteComposition IServerInternal server, ApplicationConfiguration configuration, IOptions? options = null) - : base(server, configuration, Namespaces.Site, Opc.Ua.OpenUsd.Namespaces.OpenUSD) + : base( + server, configuration, Namespaces.Site, + Opc.Ua.OpenUsd.Namespaces.OpenUSD, Opc.Ua.XRegistry.Namespaces.xRegistry) { m_options = options?.Value ?? new SiteCompositionOptions(); m_log = server.Telemetry.CreateLogger(); @@ -125,6 +128,7 @@ namespace SiteComposition CancellationToken cancellationToken = default) { var nodes = new NodeStateCollection(); + nodes.AddOpcUaXRegistry(context); nodes.AddOpcUaOpenUsd(context); return new ValueTask(nodes); } @@ -313,6 +317,7 @@ namespace SiteComposition m_siteStage = SystemContext.CreateInstanceOfOpenUsdStageType( stages, new QualifiedName("SiteStage", ns)); + m_siteStage.ReferenceTypeId = ReferenceTypeIds.Organizes; stages.AddChild(m_siteStage); m_siteStage.CreateOrReplaceRootLayerIdentifier(SystemContext, null!) .Value = SiteRootLayerIdentifier; @@ -333,7 +338,7 @@ namespace SiteComposition .Value = OpenUsdDigestAlgorithmEnum.Sha256; root.AddReference(ReferenceTypeIds.HasComponent, true, Opc.Ua.ObjectIds.Server); - UsdAssetDelivery.AttachStageAssets(SystemContext, m_siteStage, ns, served); + UsdAssetDelivery.AttachStageAssets(SystemContext, m_siteStage, ns, served, "site"); SystemContext.AssignInstanceChildNodeIds(root); await AddPredefinedNodeAsync(SystemContext, root, cancellationToken) @@ -442,6 +447,7 @@ namespace SiteComposition { Namespaces.Site, Opc.Ua.OpenUsd.Namespaces.OpenUSD, + Opc.Ua.XRegistry.Namespaces.xRegistry, }.ToArrayOf(); /// diff --git a/samples/Robotics/BinPickingCell/BinPickingCell.csproj b/samples/Robotics/BinPickingCell/BinPickingCell.csproj index 4c3e6d51ad..03e9d77597 100644 --- a/samples/Robotics/BinPickingCell/BinPickingCell.csproj +++ b/samples/Robotics/BinPickingCell/BinPickingCell.csproj @@ -45,6 +45,7 @@ + + http://opcfoundation.org/UA/xRegistry/ http://opcfoundation.org/UA/OpenUSD/ - + + @@ -27,939 +29,1127 @@ i=35 i=40 i=37 - i=17604 i=17603 + i=17604 i=78 i=80 i=11508 - + OpenUsdRenderTargetKindEnum Classifies the USD render target a live value drives (advisory routing hint). - OpenUSD Binding DataTypes + OU-Binding + OU-Conversion-Transform + OU-Conversion-Geo i=29 - ns=1;i=3902 + ns=2;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. + A geodetic anchor coordinate (latitude/longitude/height) on a USD georeference or globe-anchor schema; see the Conversion clause and the Geospatial rendering annex of OPC 12001-1. - + EnumStrings i=78 i=68 - ns=1;i=3002 + ns=2;i=3002 TranslationRotationScaleTransformVisibilityDisplayColorEmissiveColorOpacityCustomGeoreference - + OpenUsdBadQualityActionEnum What the connector does with a non-Good source value. - OpenUSD Binding DataTypes + OU-Quality i=29 - ns=1;i=3903 + ns=2;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 + ns=2;i=3003 SkipHoldLastClearOpinionFallback - + OpenUsdBindingStateEnum Runtime lifecycle state of a live binding, exposed for diagnostics. - OpenUSD Binding DataTypes + OU-Diagnostics i=29 - ns=1;i=3904 + ns=2;i=3904 - + EnumStrings i=78 i=68 - ns=1;i=3004 + ns=2;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 + OU-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 + ns=2;i=6001 + ns=2;i=6002 + ns=2;i=6003 + ns=2;i=6004 + ns=2;i=6005 + ns=2;i=6006 + ns=2;i=6007 + ns=2;i=6008 + ns=2;i=6009 + ns=2;i=6010 + ns=2;i=6011 + ns=2;i=6012 + ns=2;i=6013 + ns=2;i=6014 + ns=2;i=6015 + ns=2;i=6016 + ns=2;i=6017 + ns=2;i=6018 + ns=2;i=6019 + ns=2;i=6031 + ns=2;i=6032 + ns=2;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 + ns=2;i=1004 - + Enabled False acts as a tombstone that suppresses an inherited binding. i=78 i=68 - ns=1;i=1004 + ns=2;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 + ns=2;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 + ns=2;i=1004 - + AttributeId Source attribute id; default 13 (Value). Telemetry binds Value only. i=80 i=68 - ns=1;i=1004 + ns=2;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 + ns=2;i=1004 - + TargetPrimPath Prim path of the target: absolute, or relative to the representation PrimPath. i=78 i=68 - ns=1;i=1004 + ns=2;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 + ns=2;i=1004 - + TargetUsdTypeName Expected USD Sdf value type name, e.g. 'double', 'float', 'bool', 'color3f'. i=78 i=68 - ns=1;i=1004 + ns=2;i=1004 - + RenderTargetKind Advisory routing hint for the render target category. i=80 i=68 - ns=1;i=1004 + ns=2;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 + ns=2;i=1004 - + Scale Linear conversion scale: target = Scale * converted + Offset. i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + Offset Linear conversion offset. i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + SourceEngineeringUnits Asserted source engineering units (UNECE). i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + TargetEngineeringUnits Requested target engineering units. i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + BadQualityAction How the connector treats a non-Good source value. Default Skip. i=80 i=68 - ns=1;i=1004 + ns=2;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 + ns=2;i=1004 - + State Runtime lifecycle state (diagnostic). i=80 i=68 - ns=1;i=1004 + ns=2;i=1004 - + LastError Last operation error text (diagnostic). i=80 i=68 - ns=1;i=1004 + ns=2;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 + OU-Stage + OU-Integrity 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 + ns=2;i=6020 + ns=2;i=6021 + ns=2;i=6022 + ns=2;i=6023 + ns=2;i=6038 + ns=2;i=6039 + ns=2;i=6040 + ns=2;i=6041 + ns=2;i=6065 - + RootLayerIdentifier Opaque authored root-layer / resolver identifier (NOT necessarily a URI). i=78 i=68 - ns=1;i=1002 + ns=2;i=1002 - + SessionLayerIdentifier Opaque session-layer identifier, if any. Contributes to stage identity. i=80 i=68 - ns=1;i=1002 + ns=2;i=1002 - + ResolverContext Opaque resolver-context descriptor (e.g. a named context profile). i=80 i=68 - ns=1;i=1002 + ns=2;i=1002 - + ResolvedRootLayerUri Informative current resolution as a URI, when representable. Not authoritative. i=80 i=68 - ns=1;i=1002 + ns=2;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 + OU-Representation + OU-RepresentationRegistry i=58 - ns=1;i=6024 - ns=1;i=6025 - ns=1;i=6026 - ns=1;i=6027 - ns=1;i=6059 + ns=2;i=6024 + ns=2;i=6025 + ns=2;i=6026 + ns=2;i=6027 + ns=2;i=6059 - + DefaultInstanceBrowseName Default BrowseName for instances of this AddIn (Part 3 AddIn convention). i=68 - ns=1;i=1003 + ns=2;i=1003 - 1OpenUsdRepresentation + 2OpenUsdRepresentation - + Stage NodeId of the OpenUsdStageType instance this representation targets. i=78 i=68 - ns=1;i=1003 + ns=2;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 + ns=2;i=1003 - + <Binding> A live binding whose source resolves relative to the represented object. i=11508 - ns=1;i=1004 - ns=1;i=1003 + ns=2;i=1004 + ns=2;i=1003 - + OpenUsdRootType Root of the server-wide OpenUSD facility. Contains the stage registry and the representation registry for deterministic discovery. - OpenUSD Binding + OU-Discovery i=58 - ns=1;i=6028 - ns=1;i=6029 + ns=2;i=6028 + ns=2;i=6029 + ns=2;i=6071 - + Stages Registry of OpenUsdStageType instances available to the server. i=78 i=61 - ns=1;i=1001 + ns=2;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 + ns=2;i=1001 - + IOpenUsdRepresentedType Optional interface advertising that a domain ObjectType participates in OpenUSD representation. Applied with HasInterface; informative for browsing. - OpenUSD Binding + OU-Representation i=17602 - ns=1;i=6030 + ns=2;i=6030 - + <OpenUsdRepresentation> Placeholder for the representation AddIn on an implementing instance. i=11508 - ns=1;i=1003 - ns=1;i=1010 + ns=2;i=1003 + ns=2;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 + OU-SemanticSource + OU-Command i=29 - ns=1;i=3905 + ns=2;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 + ns=2;i=3005 ObservableControllable - + OpenUsdAlarmAspectEnum For OpenUsdAlarmBindingType bindings: which A&C condition aspect drives the target attribute. - OpenUSD Binding DataTypes + OU-Alarm i=29 - ns=1;i=3906 + ns=2;i=3906 Condition ActiveState (boolean).Condition Severity (numeric).Condition AckedState (boolean).Condition EnabledState (boolean). - + EnumStrings i=78 i=68 - ns=1;i=3006 + ns=2;i=3006 ActiveStateSeverityAckedStateEnabledState - + OpenUsdDigestAlgorithmEnum Digest algorithm for OpenUsdStageType.RootLayerDigest (Twin BOM content integrity). - OpenUSD Binding DataTypes + OU-Integrity i=29 - ns=1;i=3907 + ns=2;i=3907 SHA-256.SHA-384.SHA-512. - + EnumStrings i=78 i=68 - ns=1;i=3007 + ns=2;i=3007 NoneSha256Sha384Sha512 - + SignalRole Observable (read-only, default) or Controllable (eligible for a command binding). i=80 i=68 - ns=1;i=1004 + ns=2;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 + ns=2;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 + OU-Binding + OU-Conversion-Scalar - ns=1;i=1004 + ns=2;i=1004 - + OpenUsdAlarmBindingType Read-only OPC UA A&C condition aspect (Part 9) drives a USD attribute. - OpenUSD Binding + OU-Alarm - ns=1;i=1004 - ns=1;i=6033 + ns=2;i=1004 + ns=2;i=6033 - + AlarmAspect Which A&C condition aspect drives the target (ActiveState/Severity/AckedState/EnabledState). i=80 i=68 - ns=1;i=1008 + ns=2;i=1008 - + OpenUsdHistoryBindingType Read-only history (Part 11 HistoryRead) authored as USD time samples. - OpenUSD Binding + OU-History - ns=1;i=1004 - ns=1;i=6034 + ns=2;i=1004 + ns=2;i=6034 - + TimeSampled Author values as USD time samples (playback) rather than the latest default. i=80 i=68 - ns=1;i=1009 + ns=2;i=1009 - + OpenUsdCommandBindingType Opt-in, authorized USD-side intent drives an OPC UA write / Method call (USD -> UA). - OpenUSD Binding + OU-Command - ns=1;i=1004 - ns=1;i=6035 - ns=1;i=6036 - ns=1;i=6037 + ns=2;i=1004 + ns=2;i=6035 + ns=2;i=6036 + ns=2;i=6037 - + CommandTargetNodeId The Variable to write, or the Object on which to Call CommandMethodId. i=78 i=68 - ns=1;i=1011 + ns=2;i=1011 - + CommandMethodId Optional Method to invoke instead of a Variable write. i=80 i=68 - ns=1;i=1011 + ns=2;i=1011 - + CommandTriggerPropertyName The USD attribute whose change is interpreted as the command intent/value. i=78 i=68 - ns=1;i=1011 + ns=2;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 + ns=2;i=1002 - + RootLayerDigestAlgorithm Digest algorithm for RootLayerDigest (default SHA-256). i=80 i=68 - ns=1;i=1002 + ns=2;i=1002 - + Signature Optional detached signature over the digest / stage identity, for provenance. i=80 i=68 - ns=1;i=1002 + ns=2;i=1002 - + ProvenanceUri Optional URI locating provenance / a signed Twin BOM manifest for the stage. i=80 i=68 - ns=1;i=1002 + ns=2;i=1002 - + OpenUsdCardinalityEnum Cardinality of a component binding: 1:1 or 1..n. - OpenUSD Binding DataTypes + OU-Composition i=29 - ns=1;i=3908 + ns=2;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 + ns=2;i=3008 OneMany - + OpenUsdCompositionArcEnum USD composition arc used to place a component prim under its parent. - OpenUSD Binding DataTypes + OU-Composition i=29 - ns=1;i=3909 + ns=2;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 + ns=2;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 + OU-Composition + OU-DynamicComposition + OU-CrossServerComposition 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 + ns=2;i=6042 + ns=2;i=6043 + ns=2;i=6044 + ns=2;i=6045 + ns=2;i=6046 + ns=2;i=6047 + ns=2;i=6048 + ns=2;i=6049 + ns=2;i=6050 + ns=2;i=6051 + ns=2;i=6052 + ns=2;i=6053 + ns=2;i=6054 + ns=2;i=6055 + ns=2;i=6056 + ns=2;i=6057 + ns=2;i=6058 + ns=2;i=6066 - + BindingDefinitionId Stable declaration id for override/tombstone matching. i=78 i=68 - ns=1;i=1005 + ns=2;i=1005 - + Enabled False acts as a tombstone that suppresses an inherited component binding. i=78 i=68 - ns=1;i=1005 + ns=2;i=1005 - + Cardinality One (1:1) or Many (1..n). i=78 i=68 - ns=1;i=1005 + ns=2;i=1005 - + CompositionArc How the component maps into the parent prim: Child, Reference, Payload, Instance. i=78 i=68 - ns=1;i=1005 + ns=2;i=1005 - + ComponentReferenceType Aggregating ReferenceType from the represented Object to its component(s); default HasComponent. i=80 i=68 - ns=1;i=1005 + ns=2;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 + ns=2;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 + ns=2;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 + ns=2;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 + ns=2;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 + ns=2;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 + ns=2;i=1005 - + Dynamic The component set may change at runtime (reconciled from model-change events). i=80 i=68 - ns=1;i=1005 + ns=2;i=1005 - + ChangeEventSource Node whose GeneralModelChange/SemanticChange events signal recomposition; default the Server Object (i=2253). i=80 i=68 - ns=1;i=1005 + ns=2;i=1005 - + ComponentServerUri For a component on another server: the remote Server's application/namespace URI. i=80 i=68 - ns=1;i=1005 + ns=2;i=1005 - + ComponentEndpointUrl For a component on another server: the remote endpoint URL (else discovered). i=80 i=68 - ns=1;i=1005 + ns=2;i=1005 - + State Runtime lifecycle state (diagnostic). i=80 i=68 - ns=1;i=1005 + ns=2;i=1005 - + LastError Last operation error text (diagnostic). i=80 i=68 - ns=1;i=1005 + ns=2;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 + ns=2;i=1005 + ns=2;i=1003 - + OpenUsdAssetKindEnum - Role of a served USD asset within a stage's served layer closure. - OpenUSD Binding DataTypes + Role of a served USD artifact within a stage's served layer closure. + OU-AssetDelivery + OU-SchemaPluginDelivery i=29 - ns=1;i=3910 + ns=2;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. + 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.A MaterialX document (.mtlx) defining a material/shader network.A volume or geometry cache asset (OpenVDB, Alembic).A USD plugin manifest (plugInfo.json) declaring a schema plugin.A generatedSchema.usda carrying codeless schema definitions.An asset manifest or metadata sidecar describing the container. - + EnumStrings i=78 i=68 - ns=1;i=3010 + ns=2;i=3010 - RootLayerSubLayerReferencePayloadTexturePackage + RootLayerSubLayerReferencePayloadTexturePackageMaterialXVolumeSchemaPluginGeneratedSchemaManifest - + 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 + One served USD artifact Resource or exact Version: 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 xRegistry ResourceType, which is itself a Part 5 FileType. A logical Resource delegates its file view to the selected default Version; each exact Version streams its own bytes through Open/Read/Close. AssetIdentifier is the authored USD resolver identifier, normalized relative to its asset container, and is the artifact's source identity; the inherited xRegistry ResourceId is the symbolic identifier constructed from it, a one-way construction that is never inverted, so the registry is addressable as an ArResolver backend without conflating the two identifier grammars. + OU-AssetDelivery + OU-ArtifactRegistry + OU-ArtifactFederation - i=11575 - ns=1;i=6060 - ns=1;i=6061 - ns=1;i=6062 - ns=1;i=6063 - ns=1;i=6064 + ns=1;i=63002 + ns=2;i=6060 + ns=2;i=6061 + ns=2;i=6062 + ns=2;i=6063 + ns=2;i=6064 + ns=2;i=6070 + ns=2;i=6082 - + AssetIdentifier - Resolver identifier / relative path of this asset, matching the stage RootLayerIdentifier or a ComponentAssetReference asset path; used for @...@ resolution and cache placement. + Resolver identifier / relative path of this asset, matching the stage RootLayerIdentifier or a ComponentAssetReference asset path; used for @...@ resolution and cache placement. It is the artifact's source identity: the inherited ResourceId is the symbolic identifier constructed from it, and the inherited Name is this string verbatim. i=78 i=68 - ns=1;i=1006 + ns=2;i=1006 - + AssetKind Role of this asset within the stage's served layer closure. i=78 i=68 - ns=1;i=1006 + ns=2;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 + ns=2;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 + ns=2;i=1006 - + DigestAlgorithm Digest algorithm for Digest (default SHA-256). i=80 i=68 - ns=1;i=1006 + ns=2;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. + Optional VIEW onto this stage's served layer closure: a Folder that Organizes the OpenUsdAssetType artifacts of the server's Artifacts registry which this stage needs (exactly one RootLayer). It does NOT own the artifacts - an artifact shared by several stages exists once in the registry, and an artifact the registry federates rather than hosts (ResourceUrl / ExternalReference) is organized here the same way, so a stage's closure may span registries. Present only when the server delivers its geometry; a connector that finds it fetches and composes the stage locally, else it resolves RootLayerIdentifier externally. i=80 i=61 - ns=1;i=1002 + ns=2;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 + ns=2;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 + ns=2;i=1004 + + OpenUsdArtifactRegistryType + The server's OpenUSD artifact registry - an xRegistry RegistryType (a FolderType) holding every USD artifact the server serves: layers, packages, textures, MaterialX documents, volumes, schema plugins and manifests. Exposed as the Artifacts component of OpenUsdRootType, it is the single backbone all stages resolve against, replacing per-stage duplication. Each artifact's ResourceId is the symbolic identifier of its USD asset identifier, so the registry is directly addressable as an ArResolver backend and xRegistry federation (ResourceUrl / ExternalReference) is the resolver fallback chain. + OU-ArtifactRegistry + OU-ArtifactFederation + + ns=1;i=63000 + ns=2;i=6072 + ns=2;i=6073 + + + + OpenUsdAssetGroupType + An xRegistry GroupType collecting the artifacts of ONE asset container - a named, versioned USD asset in the AOUSD sense (its root layer plus the sublayers, references, payloads, textures, MaterialX documents and volumes it needs). The group key is the asset container identifier; member artifacts are addressed by their AssetIdentifier relative to it. + OU-ArtifactRegistry + + ns=1;i=63001 + ns=2;i=6068 + ns=2;i=6074 + + + + AssetContainerId + Identifier of the asset container this group represents. This is the group key: its value is identical to the inherited GroupId and to the group's BrowseName, e.g. 'pumps'. It is NOT an asset-identifier prefix - member AssetIdentifiers are already normalized relative to the container. Together with an artifact's AssetIdentifier it locates the artifact within the registry (see spec 5.15.3). + + i=78 + i=68 + ns=2;i=1013 + + + + OpenUsdSchemaPluginGroupType + An xRegistry GroupType collecting the files of ONE USD schema plugin: its plugInfo.json manifest and its generatedSchema.usda. A codeless USD schema needs exactly those two files, so serving them lets a USD client register a vendor schema through PlugRegistry and then interpret the vendor prim types a Part 2 server materializes (Part 2 8.1/8.3), closing the loop between the OPC UA and USD type systems. + OU-SchemaPluginDelivery + + ns=1;i=63001 + ns=2;i=6069 + ns=2;i=6075 + + + + PluginName + The USD plugin name as it appears in plugInfo.json (Plugins[].Name). + + i=78 + i=68 + ns=2;i=1014 + + + + DependsOn + Ordered asset identifiers this artifact directly references (sublayers, references, payloads, textures, MaterialX documents), as authored - not Xids, so a resolver matches them against @...@ references directly. Makes the 5.15 dependency closure explicit and queryable instead of only procedural, so a connector can verify completeness before composing. + + i=80 + i=68 + ns=2;i=1006 + + + + Artifacts + Optional OpenUsdArtifactRegistryType holding every USD artifact the server serves. Present when the server delivers content; a stage's Assets folder then Organizes the artifacts of its closure from here rather than owning them. + + i=80 + ns=2;i=1012 + ns=2;i=1001 + + + + <AssetContainer> + Constrains the inherited <Group> placeholder: an asset container group held by this registry, keyed by AssetContainerId. + + i=11508 + ns=2;i=1013 + ns=2;i=1012 + + + + <SchemaPlugin> + Constrains the inherited <Group> placeholder: a USD schema plugin group held by this registry, keyed by PluginName. + + i=11508 + ns=2;i=1014 + ns=2;i=1012 + + + + <Asset> + Constrains the inherited <Resource> placeholder: an artifact of this asset container, addressed by the symbolic identifier of its AssetIdentifier. + + i=11508 + ns=2;i=1006 + ns=2;i=1013 + + + + <Asset> + Constrains the inherited <Resource> placeholder: a file of this schema plugin - its plugInfo.json manifest or its generatedSchema.usda. + + i=11508 + ns=2;i=1006 + ns=2;i=1014 + + + + http://opcfoundation.org/UA/OpenUSD/ + Metadata for this namespace, as OPC 10000-5 requires a Server to publish it. + + i=11616 + i=11715 + ns=2;i=6077 + ns=2;i=6078 + ns=2;i=6079 + ns=2;i=6080 + + + + NamespaceUri + + i=68 + ns=2;i=6076 + + http://opcfoundation.org/UA/OpenUSD/ + + + NamespaceVersion + + i=68 + ns=2;i=6076 + + 0.9.0 + + + NamespacePublicationDate + + i=68 + ns=2;i=6076 + + 2026-09-05T00:00:00Z + + + IsNamespaceSubset + + i=68 + ns=2;i=6076 + + false + + + OpenUsdAssetVersionsType + The typed Versions folder of a logical OpenUSD artifact Resource. + OU-ArtifactRegistry + + ns=1;i=63025 + ns=2;i=6081 + + + + <Version> + One exact OpenUSD artifact Version. + + i=11508 + ns=2;i=1006 + ns=2;i=1015 + + + + Versions + Overrides the inherited Versions component so every exact Version is an OpenUsdAssetType. + + i=80 + ns=2;i=1015 + ns=2;i=1006 + + diff --git a/src/Opc.Ua.OpenUsd/NugetREADME.md b/src/Opc.Ua.OpenUsd/NugetREADME.md index 8cec3ae943..e83b8493be 100644 --- a/src/Opc.Ua.OpenUsd/NugetREADME.md +++ b/src/Opc.Ua.OpenUsd/NugetREADME.md @@ -11,7 +11,13 @@ binds OPC UA values to it: - `OpenUsdRootType`, `OpenUsdStageType`, and `OpenUsdRepresentationType`; - live, alarm, history, and command binding types; - component composition and aggregation; -- content integrity and Part 5 asset delivery metadata. +- content integrity and Part 5 asset delivery metadata; +- xRegistry-based artifact registries, asset-container groups, and typed exact Versions. + +The binding model consumes the xRegistry companion model from `Opc.Ua.XRegistry`. +Servers must load that model before the OpenUSD binding model. Stage `Assets` folders +are non-owning views of logical Resources in `Server/OpenUSD/Artifacts`, not per-stage +file containers. The generic, dependency-injectable connector lives in **Opc.Ua.OpenUsd.Client**. diff --git a/src/Opc.Ua.OpenUsd/Opc.Ua.OpenUsd.csproj b/src/Opc.Ua.OpenUsd/Opc.Ua.OpenUsd.csproj index 04751a1ed9..3976d20c42 100644 --- a/src/Opc.Ua.OpenUsd/Opc.Ua.OpenUsd.csproj +++ b/src/Opc.Ua.OpenUsd/Opc.Ua.OpenUsd.csproj @@ -26,6 +26,7 @@ + @@ -40,6 +41,12 @@ + + Opc.Ua.XRegistry + XRegistry + http://opcfoundation.org/UA/xRegistry/ + true + Opc.Ua.OpenUsd diff --git a/tests/Opc.Ua.Di.Tests/PumpOpenUsdE2eTests.cs b/tests/Opc.Ua.Di.Tests/PumpOpenUsdE2eTests.cs index 6907785222..867345166d 100644 --- a/tests/Opc.Ua.Di.Tests/PumpOpenUsdE2eTests.cs +++ b/tests/Opc.Ua.Di.Tests/PumpOpenUsdE2eTests.cs @@ -342,6 +342,93 @@ namespace Opc.Ua.Di.Tests } Assert.That(hasRepresentations, Is.True, "Representations registry is not reachable from Server/OpenUSD."); + + ReferenceDescription stages = rootRefs.ToList().Single(reference => reference.BrowseName.Name == "Stages"); + var browseStages = new BrowseDescription + { + NodeId = ExpandedNodeId.ToNodeId(stages.NodeId, m_session.NamespaceUris), + BrowseDirection = BrowseDirection.Forward, + ReferenceTypeId = Opc.Ua.ReferenceTypeIds.Organizes, + IncludeSubtypes = true, + NodeClassMask = (uint)NodeClass.Object, + ResultMask = (uint)BrowseResultMask.All + }; + BrowseResponse stageChildren = await m_session.BrowseAsync( + null!, null!, 0, new BrowseDescription[] { browseStages }, CancellationToken.None) + .ConfigureAwait(false); + Assert.That(StatusCode.IsGood(stageChildren.Results[0].StatusCode), Is.True); + Assert.That(stageChildren.Results[0].References, Has.Count.EqualTo(1), + "PlantStage is not reachable through Server/OpenUSD/Stages/Organizes."); + var connector = new OpenUsdConnector(m_session, new MockUsdSink()); + OpenUsdConnector.RepresentationInfo? representation = await PumpRepAsync(connector).ConfigureAwait(false); + Assert.That(representation, Is.Not.Null); + NodeId stageId = ExpandedNodeId.ToNodeId( + stageChildren.Results[0].References[0].NodeId, m_session.NamespaceUris); + Assert.That(stageId, Is.EqualTo(representation!.StageNodeId), + "The registered stage must match the representation's Stage value."); + + ReferenceDescription artifacts = rootRefs.ToList().Single( + reference => reference.BrowseName.Name == "Artifacts"); + Assert.That( + ExpandedNodeId.ToNodeId(artifacts.TypeDefinition, m_session.NamespaceUris), + Is.EqualTo(ExpandedNodeId.ToNodeId( + Opc.Ua.OpenUsd.ObjectTypeIds.OpenUsdArtifactRegistryType, m_session.NamespaceUris))); + ArrayOf groups = await BrowseObjectsAsync( + ExpandedNodeId.ToNodeId(artifacts.NodeId, m_session.NamespaceUris), + Opc.Ua.ReferenceTypeIds.Organizes).ConfigureAwait(false); + Assert.That(groups, Has.Count.EqualTo(1)); + Assert.That(groups[0].BrowseName.Name, Is.EqualTo("pumps")); + NodeId groupId = ExpandedNodeId.ToNodeId(groups[0].NodeId, m_session.NamespaceUris); + ArrayOf resources = await BrowseObjectsAsync( + groupId, Opc.Ua.ReferenceTypeIds.Organizes).ConfigureAwait(false); + Assert.That(resources, Has.Count.EqualTo(2)); + + ArrayOf stageObjects = await BrowseObjectsAsync( + stageId, Opc.Ua.ReferenceTypeIds.HasComponent).ConfigureAwait(false); + ReferenceDescription view = stageObjects.ToList().Single( + reference => reference.BrowseName.Name == "Assets"); + ArrayOf viewResources = await BrowseObjectsAsync( + ExpandedNodeId.ToNodeId(view.NodeId, m_session.NamespaceUris), + Opc.Ua.ReferenceTypeIds.Organizes).ConfigureAwait(false); + Assert.That(viewResources.ToList().Select(reference => reference.NodeId), + Is.EquivalentTo(resources.ToList().Select(reference => reference.NodeId)), + "Stage Assets must reference the same logical resources as the artifact registry."); + + foreach (ReferenceDescription resource in resources.ToList()) + { + ArrayOf components = await BrowseObjectsAsync( + ExpandedNodeId.ToNodeId(resource.NodeId, m_session.NamespaceUris), + Opc.Ua.ReferenceTypeIds.HasComponent).ConfigureAwait(false); + ReferenceDescription versions = components.ToList().Single( + reference => reference.BrowseName.Name == "Versions"); + ArrayOf exact = await BrowseObjectsAsync( + ExpandedNodeId.ToNodeId(versions.NodeId, m_session.NamespaceUris), + Opc.Ua.ReferenceTypeIds.Organizes).ConfigureAwait(false); + Assert.That(exact, Has.Count.EqualTo(1)); + Assert.That(exact[0].NodeId, Is.Not.EqualTo(resource.NodeId)); + Assert.That(exact[0].BrowseName.Name, Is.EqualTo("1")); + } + + async Task> BrowseObjectsAsync(NodeId nodeId, NodeId referenceType) + { + BrowseResponse response = await m_session!.BrowseAsync( + null!, null!, 0, + new BrowseDescription[] + { + new() + { + NodeId = nodeId, + BrowseDirection = BrowseDirection.Forward, + ReferenceTypeId = referenceType, + IncludeSubtypes = false, + NodeClassMask = (uint)NodeClass.Object, + ResultMask = (uint)BrowseResultMask.All + } + }, + CancellationToken.None).ConfigureAwait(false); + Assert.That(StatusCode.IsGood(response.Results[0].StatusCode), Is.True); + return response.Results[0].References; + } } [Test] @@ -353,7 +440,7 @@ namespace Opc.Ua.Di.Tests Assert.That(rep, Is.Not.Null, "OpenUsdRepresentation not discovered on Pump_1."); Assert.That(rep!.PrimPath, Is.EqualTo("/Plant/Pumps/Pump_1")); Assert.That(rep.StageNodeId.IsNull, Is.False); - Assert.That(rep.RootLayerIdentifier, Is.EqualTo("asset-repo/Plant.usd")); + Assert.That(rep.RootLayerIdentifier, Is.EqualTo("Plant.usda")); // Layout (1) + telemetry (10) + supervision alarms (3) + command (1). Assert.That(rep.Bindings, Has.Count.EqualTo(15)); } diff --git a/tests/Opc.Ua.Di.Tests/RobotOpenUsdE2eTests.cs b/tests/Opc.Ua.Di.Tests/RobotOpenUsdE2eTests.cs index e06bbd4d19..0998f3cdea 100644 --- a/tests/Opc.Ua.Di.Tests/RobotOpenUsdE2eTests.cs +++ b/tests/Opc.Ua.Di.Tests/RobotOpenUsdE2eTests.cs @@ -548,7 +548,7 @@ namespace Opc.Ua.Di.Tests // 1 system + 2 robots + 12 axes = 15 representations in the registry. Assert.That(reps, Has.Count.GreaterThanOrEqualTo(15), "Expected >= 15 representations."); Assert.That(cell, Is.Not.Null, "System (RobotCell) representation not discovered."); - Assert.That(cell!.RootLayerIdentifier, Is.EqualTo("asset-repo/Cell.usd")); + Assert.That(cell!.RootLayerIdentifier, Is.EqualTo("Cell.usda")); Assert.That(r1, Is.Not.Null, "Robot R1 representation not discovered."); Assert.That(axisReps, Is.EqualTo(12), "Expected 12 axis representations (6 per robot)."); // The system carries the RobotsAggregation (Many) component binding. diff --git a/tests/Opc.Ua.OpenUsd.Tests/OpenUsdAuthoringHarness.cs b/tests/Opc.Ua.OpenUsd.Tests/OpenUsdAuthoringHarness.cs index 17ad58ffd6..ae430361cd 100644 --- a/tests/Opc.Ua.OpenUsd.Tests/OpenUsdAuthoringHarness.cs +++ b/tests/Opc.Ua.OpenUsd.Tests/OpenUsdAuthoringHarness.cs @@ -72,6 +72,7 @@ namespace Opc.Ua.OpenUsd.Server.Tests public static (SystemContext Context, ushort Namespace) NewContext() { var namespaces = new NamespaceTable(); + namespaces.GetIndexOrAppend(Opc.Ua.XRegistry.Namespaces.xRegistry); var ns = (ushort)namespaces.GetIndexOrAppend(Namespaces.OpenUSD); var instanceNs = (ushort)namespaces.GetIndexOrAppend(InstanceNamespaceUri); @@ -107,7 +108,7 @@ namespace Opc.Ua.OpenUsd.Server.Tests FolderState stages = root.Stages!; OpenUsdStageState stage = context.CreateInstanceOfOpenUsdStageType( stages, new QualifiedName(name, ns)); - stage.ReferenceTypeId = ReferenceTypeIds.HasComponent; + stage.ReferenceTypeId = ReferenceTypeIds.Organizes; stages.AddChild(stage); stage.NodeId = context.NodeIdFactory.New(context, stage); return stage; diff --git a/tests/Opc.Ua.OpenUsd.Tests/UsdAssetDeliveryTests.cs b/tests/Opc.Ua.OpenUsd.Tests/UsdAssetDeliveryTests.cs index b150f40cb1..59b383ba35 100644 --- a/tests/Opc.Ua.OpenUsd.Tests/UsdAssetDeliveryTests.cs +++ b/tests/Opc.Ua.OpenUsd.Tests/UsdAssetDeliveryTests.cs @@ -27,7 +27,12 @@ * http://opcfoundation.org/License/MIT/1.00/ * ======================================================================*/ +using System; +using System.Collections.Generic; +using System.Linq; using System.Text; +using System.Text.Json; +using System.Threading.Tasks; using NUnit.Framework; using Opc.Ua.OpenUsd; @@ -48,7 +53,7 @@ namespace Opc.Ua.OpenUsd.Server.Tests private const byte WriteMode = 0x02; [Test] - public void AttachStageAssetsCreatesTheAssetsFolderAndOneAssetPerLayer() + public async Task AttachStageAssetsCreatesTheAssetsFolderAndOneAssetPerLayerAsync() { (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); OpenUsdRootState root = OpenUsdAuthoringHarness.NewFacility(context, ns); @@ -63,12 +68,43 @@ namespace Opc.Ua.OpenUsd.Server.Tests }); Assert.That(created.Count, Is.EqualTo(2)); + Assert.That( + root.FindChild(context, new QualifiedName("Artifacts", ns)), + Is.Not.Null, + "Served layers must be owned by the server-wide artifact registry."); Assert.That(stage.Assets, Is.Not.Null); Assert.That(created[0].AssetIdentifier!.Value, Is.EqualTo("Cell.usda")); Assert.That(created[0].AssetKind!.Value, Is.EqualTo(OpenUsdAssetKindEnum.RootLayer)); Assert.That(created[0].MediaType!.Value, Is.EqualTo("model/vnd.usda")); Assert.That(created[1].AssetIdentifier!.Value, Is.EqualTo("robot.usda")); Assert.That(created[1].AssetKind!.Value, Is.EqualTo(OpenUsdAssetKindEnum.Reference)); + + context.AssignInstanceChildNodeIds(root); + var typeTable = new TypeTable(context.NamespaceUris); + typeTable.AddReferenceSubtype( + ReferenceTypeIds.HierarchicalReferences, + NodeId.Null, + QualifiedName.From(Opc.Ua.BrowseNames.HierarchicalReferences)); + typeTable.AddReferenceSubtype( + ReferenceTypeIds.Organizes, + ReferenceTypeIds.HierarchicalReferences, + QualifiedName.From(Opc.Ua.BrowseNames.Organizes)); + context.TypeTable = typeTable; + var references = new List(); + using INodeBrowser browser = stage.Assets!.CreateBrowser( + context, null, ReferenceTypeIds.Organizes, false, BrowseDirection.Forward, default, null, false); + for (IReference? reference = await browser.NextAsync().ConfigureAwait(false); + reference != null; + reference = await browser.NextAsync().ConfigureAwait(false)) + { + references.Add(reference); + } + + Assert.That(references, Has.Count.EqualTo(created.Count)); + foreach (OpenUsdAssetState asset in created) + { + Assert.That(references.Exists(reference => reference.TargetId == asset.NodeId), Is.True); + } } [Test] @@ -88,6 +124,239 @@ namespace Opc.Ua.OpenUsd.Server.Tests Assert.That(ReferenceEquals(stage.Assets, first), Is.True); } + [Test] + public void StageViewsShareRegistryResourcesWithoutOwningThem() + { + (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); + OpenUsdRootState root = OpenUsdAuthoringHarness.NewFacility(context, ns); + OpenUsdStageState first = OpenUsdAuthoringHarness.NewStage(context, root, ns, "First"); + OpenUsdStageState second = OpenUsdAuthoringHarness.NewStage(context, root, ns, "Second"); + + OpenUsdAssetState firstAsset = UsdAssetDelivery.AttachStageAssets( + context, first, ns, new ServedAsset[] { Asset("./shared.usda") }, "pumps")[0]; + OpenUsdAssetState secondAsset = UsdAssetDelivery.AttachStageAssets( + context, second, ns, new ServedAsset[] { Asset("shared.usda") }, "pumps")[0]; + context.AssignInstanceChildNodeIds(root); + + Assert.That(secondAsset, Is.SameAs(firstAsset)); + Assert.That(root.Artifacts, Is.Not.Null); + Assert.That(((OpenUsdAssetGroupState)firstAsset.Parent!).Parent, Is.SameAs(root.Artifacts)); + var owned = new List(); + first.Assets!.GetChildren(context, owned); + Assert.That(owned, Is.Empty, "A stage view must not own or duplicate registry resources."); + + first.Assets.RemoveReference(ReferenceTypeIds.Organizes, false, firstAsset.NodeId); + Assert.That( + firstAsset.Parent!.FindChild(context, firstAsset.BrowseName), + Is.SameAs(firstAsset), + "Removing view membership must leave the registry resource intact."); + var references = new List(); + second.Assets!.GetReferences(context, references); + Assert.That(references.Exists(reference => + reference.ReferenceTypeId == ReferenceTypeIds.Organizes && + !reference.IsInverse && reference.TargetId == firstAsset.NodeId), Is.True); + } + + [Test] + public void RegistryResourcesHaveDistinctExactVersionsWithTheSameReadOnlyContent() + { + byte[] payload = Bytes("immutable layer"); + byte[] input = (byte[])payload.Clone(); + OpenUsdAssetState asset = SingleAsset(input, out SystemContext context); + input[0] = (byte)'X'; + var children = new List(); + asset.Versions!.GetChildren(context, children); + OpenUsdAssetState version = children.OfType().Single(); + var group = (OpenUsdAssetGroupState)asset.Parent!; + + Assert.That( + asset.FindChild(context, new QualifiedName( + Opc.Ua.XRegistry.BrowseNames.Versions, + (ushort)context.NamespaceUris.GetIndex(Opc.Ua.XRegistry.Namespaces.xRegistry))), + Is.SameAs(asset.Versions), + "Generic xRegistry clients must discover the narrowed Versions component."); + Assert.That(group.AssetContainerId!.Value, Is.EqualTo(group.GroupId!.Value)); + Assert.That(group.BrowseName.Name, Is.EqualTo(group.GroupId.Value)); + Assert.That(asset.ResourceId!.Value, Is.EqualTo("Cell.usda")); + Assert.That(asset.Name!.Value, Is.EqualTo(asset.AssetIdentifier!.Value)); + Assert.That(asset.Xid!.Value, Is.EqualTo("/usdassetgroups/Cell/usdassets/Cell.usda")); + Assert.That(version.Xid!.Value, Is.EqualTo(asset.Xid.Value + "/versions/1")); + Assert.That(version.NodeId, Is.Not.EqualTo(asset.NodeId)); + Assert.That(version.BrowseName.Name, Is.EqualTo(version.VersionId!.Value)); + Assert.That(version.ResourceId!.Value, Is.EqualTo(asset.ResourceId.Value)); + Assert.That(version.AssetIdentifier!.Value, Is.EqualTo(asset.AssetIdentifier.Value)); + Assert.That(version.Name!.Value, Is.EqualTo(asset.Name.Value)); + Assert.That(version.ContentType!.Value, Is.EqualTo(asset.MediaType!.Value)); + Assert.That(version.Epoch!.Value, Is.EqualTo(1)); + Assert.That(version.Versions, Is.Null, "An exact Version must not have its own Versions folder."); + Assert.That(version.Writable!.Value, Is.False); + Assert.That(version.UserWritable!.Value, Is.False); + Assert.That(version.Digest!.Value, Is.EqualTo(asset.Digest!.Value)); + + foreach (OpenUsdAssetState file in new[] { asset, version }) + { + uint handle = OpenHandle(file, context); + ByteString data = default; + Assert.That(file.Read!.OnCall!( + context, file.Read, file.NodeId, handle, payload.Length, ref data), + Is.EqualTo(ServiceResult.Good)); + Assert.That(data.Span.SequenceEqual(payload), Is.True); + } + } + + [Test] + public void RegistryResourcesDisambiguateCollidingSymbolicIdentifiers() + { + (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); + OpenUsdRootState root = OpenUsdAuthoringHarness.NewFacility(context, ns); + OpenUsdStageState stage = OpenUsdAuthoringHarness.NewStage(context, root, ns, "Cell"); + ArrayOf assets = UsdAssetDelivery.AttachStageAssets( + context, stage, ns, new ServedAsset[] { Asset("a b.usda"), Asset("a-b.usda") }); + + Assert.That(assets[0].ResourceId!.Value, Is.Not.EqualTo(assets[1].ResourceId!.Value)); + Assert.That(assets[0].NodeId, Is.Not.EqualTo(assets[1].NodeId)); + OpenUsdAssetState repeated = UsdAssetDelivery.AttachStageAssets( + context, stage, ns, new ServedAsset[] { Asset("a-b.usda") })[0]; + Assert.That(repeated, Is.SameAs(assets[1])); + } + + [TestCase("Cell A", "Cell-A")] + [TestCase("Cell", "cell")] + [TestCase("Cell", "Cell")] + public void DefaultContainersDisambiguateStagesAndReuseTheirAllocations(string firstName, string secondName) + { + (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); + OpenUsdRootState root = OpenUsdAuthoringHarness.NewFacility(context, ns); + OpenUsdStageState first = OpenUsdAuthoringHarness.NewStage(context, root, ns, firstName); + OpenUsdStageState second = OpenUsdAuthoringHarness.NewStage(context, root, ns, secondName); + var firstDefinition = new ServedAsset("Cell.usda", OpenUsdAssetKindEnum.RootLayer, Bytes("first layer")); + var secondDefinition = new ServedAsset("Cell.usda", OpenUsdAssetKindEnum.RootLayer, Bytes("second layer")); + + OpenUsdAssetState firstAsset = UsdAssetDelivery.AttachStageAssets( + context, first, ns, new[] { firstDefinition })[0]; + OpenUsdAssetState secondAsset = UsdAssetDelivery.AttachStageAssets( + context, second, ns, new[] { secondDefinition })[0]; + + Assert.That(secondAsset.Parent, Is.Not.SameAs(firstAsset.Parent)); + Assert.That( + ((OpenUsdAssetGroupState)secondAsset.Parent!).GroupId!.Value, + Is.Not.EqualTo(((OpenUsdAssetGroupState)firstAsset.Parent!).GroupId!.Value).IgnoreCase); + Assert.That( + UsdAssetDelivery.AttachStageAssets(context, first, ns, new[] { firstDefinition })[0], + Is.SameAs(firstAsset)); + Assert.That( + UsdAssetDelivery.AttachStageAssets(context, second, ns, new[] { secondDefinition })[0], + Is.SameAs(secondAsset)); + } + + [Test] + public void DefaultContainersDoNotImplicitlyShareExplicitContainers() + { + (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); + OpenUsdRootState root = OpenUsdAuthoringHarness.NewFacility(context, ns); + OpenUsdStageState first = OpenUsdAuthoringHarness.NewStage(context, root, ns, "Explicit"); + OpenUsdStageState second = OpenUsdAuthoringHarness.NewStage(context, root, ns, "Cell"); + OpenUsdAssetState explicitAsset = UsdAssetDelivery.AttachStageAssets( + context, first, ns, new ServedAsset[] { Asset("Cell.usda") }, "Cell")[0]; + OpenUsdAssetState defaultAsset = UsdAssetDelivery.AttachStageAssets( + context, second, ns, new ServedAsset[] { Asset("Cell.usda") })[0]; + + Assert.That(defaultAsset.Parent, Is.Not.SameAs(explicitAsset.Parent)); + string defaultContainer = ((OpenUsdAssetGroupState)defaultAsset.Parent!).GroupId!.Value; + Assert.That( + UsdAssetDelivery.AttachStageAssets( + context, first, ns, new ServedAsset[] { Asset("Cell.usda") }, defaultContainer)[0], + Is.SameAs(defaultAsset), + "Explicit container identifiers must still allow intentional sharing."); + } + + [Test] + public void NamespaceMetadataUsesStandardQualifiedBrowseNames() + { + (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); + var nodes = new NodeStateCollection(); + nodes.AddOpcUaOpenUsd(context); + NodeState metadata = nodes.Single(node => node.NodeId == new NodeId(6076u, ns)); + + foreach (string name in new[] + { + Opc.Ua.BrowseNames.NamespaceUri, + Opc.Ua.BrowseNames.NamespaceVersion, + Opc.Ua.BrowseNames.NamespacePublicationDate, + Opc.Ua.BrowseNames.IsNamespaceSubset + }) + { + BaseInstanceState? property = metadata.FindChild(context, new QualifiedName(name, 0)); + Assert.That(property, Is.Not.Null, $"The standard qualified browse path for {name} must resolve."); + Assert.That(property!.NodeId.NamespaceIndex, Is.EqualTo(ns)); + } + } + + [Test] + public void ExactVersionPlaceholderUsesXRegistryQualifiedBrowseName() + { + (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); + var nodes = new NodeStateCollection(); + nodes.AddOpcUaOpenUsd(context); + NodeState versionsType = nodes.Single(node => node.NodeId == new NodeId(1015u, ns)); + BaseInstanceState? placeholder = versionsType.FindChild( + context, new QualifiedName( + "", (ushort)context.NamespaceUris.GetIndex(Opc.Ua.XRegistry.Namespaces.xRegistry))); + + Assert.That(placeholder, Is.Not.Null); + Assert.That(placeholder!.NodeId.NamespaceIndex, Is.EqualTo(ns)); + Assert.That(placeholder.TypeDefinitionId, Is.EqualTo(new NodeId(1006u, ns))); + } + + [Test] + public void RegistryBootstrapCapabilitiesAreReadableAsAReadOnlyFile() + { + (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); + OpenUsdRootState root = OpenUsdAuthoringHarness.NewFacility(context, ns); + OpenUsdStageState stage = OpenUsdAuthoringHarness.NewStage(context, root, ns, "Cell"); + UsdAssetDelivery.AttachStageAssets(context, stage, ns, new ServedAsset[] { Asset("Cell.usda") }); + Assert.That(root.Artifacts!.SpecVersion!.Value, Is.EqualTo("0.1.0")); + + FileState capabilities = root.Artifacts.Capabilities!; + uint metadataHandle = 0; + Assert.That(capabilities.Open!.OnCall!( + context, capabilities.Open, capabilities.NodeId, ReadMode, ref metadataHandle), + Is.EqualTo(ServiceResult.Good)); + ByteString json = default; + Assert.That(capabilities.Read!.OnCall!( + context, capabilities.Read, capabilities.NodeId, metadataHandle, 1024, ref json), + Is.EqualTo(ServiceResult.Good)); + using JsonDocument document = JsonDocument.Parse(json.Memory); + JsonElement declaration = document.RootElement; + Assert.That(declaration.GetProperty("available").GetProperty("entities").GetProperty("mutable").GetBoolean(), + Is.False); + Assert.That(declaration.GetProperty("mutable").GetArrayLength(), Is.Zero); + Assert.That(declaration.GetProperty("specversions").EnumerateArray().Select(value => value.GetString()), + Is.EquivalentTo(new[] { root.Artifacts.SpecVersion.Value })); + Assert.That(capabilities.Writable!.Value, Is.False); + Assert.That(capabilities.UserWritable!.Value, Is.False); + } + + [TestCase("different", OpenUsdAssetKindEnum.Reference, "model/vnd.usda")] + [TestCase("#usda 1.0", OpenUsdAssetKindEnum.Payload, "model/vnd.usda")] + [TestCase("#usda 1.0", OpenUsdAssetKindEnum.Reference, "model/vnd.usdc")] + public void RegistryResourcesRejectConflictingBootstrapDefinitions( + string content, OpenUsdAssetKindEnum kind, string mediaType) + { + (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); + OpenUsdRootState root = OpenUsdAuthoringHarness.NewFacility(context, ns); + OpenUsdStageState stage = OpenUsdAuthoringHarness.NewStage(context, root, ns, "Cell"); + OpenUsdAssetState original = UsdAssetDelivery.AttachStageAssets( + context, stage, ns, new ServedAsset[] { Asset("shared.usda") })[0]; + + Assert.Throws(() => UsdAssetDelivery.AttachStageAssets( + context, stage, ns, + new ServedAsset[] { new("shared.usda", kind, Bytes(content), mediaType) })); + Assert.That( + original.Parent!.FindChild(context, original.BrowseName), + Is.SameAs(original)); + } + [Test] public void AttachStageAssetsPublishesTheContentDigest() { @@ -146,10 +415,10 @@ namespace Opc.Ua.OpenUsd.Server.Tests Assert.That(created[0].MediaType!.Value, Is.EqualTo("model/vnd.usdc")); } - [TestCase("robot.usda", "robot_usda")] - [TestCase("assets/sub dir/tool.usda", "assets_sub_dir_tool_usda")] + [TestCase("robot.usda", "robot.usda")] + [TestCase("assets/sub dir/tool.usda", "assets.sub-dir.tool.usda")] [TestCase("_private", "_private")] - [TestCase("3rdParty", "_3rdParty")] + [TestCase("3rdParty", "3rdParty")] public void AttachStageAssetsSanitisesTheIdentifierIntoABrowseName( string identifier, string expected) { @@ -165,16 +434,14 @@ namespace Opc.Ua.OpenUsd.Server.Tests } [Test] - public void AttachStageAssetsFallsBackToAConstantBrowseNameForAnEmptyIdentifier() + public void AttachStageAssetsRejectsAnEmptyIdentifier() { (SystemContext context, ushort ns) = OpenUsdAuthoringHarness.NewContext(); OpenUsdRootState root = OpenUsdAuthoringHarness.NewFacility(context, ns); OpenUsdStageState stage = OpenUsdAuthoringHarness.NewStage(context, root, ns, "Cell"); - ArrayOf created = UsdAssetDelivery.AttachStageAssets( - context, stage, ns, new ServedAsset[] { Asset(string.Empty) }); - - Assert.That(created[0].BrowseName.Name, Is.EqualTo("Asset")); + Assert.Throws(() => UsdAssetDelivery.AttachStageAssets( + context, stage, ns, new ServedAsset[] { Asset(string.Empty) })); } [Test]