mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-24 02:58:36 +08:00
Fix OpenUSD sample discovery and migrate artifacts to xRegistry (#4880)
# Description
_Describe the changes here to communicate to the maintainers why they
should accept this pull request. By default - this will become the
Commit message after merging and thus define history._
Fix OpenUSD sample discovery and integrate artifact delivery with the
xRegistry companion model.
- Load xRegistry before OpenUSD in the sample node managers, mount
stages with `Organizes`, and align root-layer identifiers with served
`.usda` files.
- Bootstrap immutable logical Resources and exact Versions in
`Artifacts`. Stage `Assets` folders provide non-owning `Organizes`
views, and explicit container identifiers allow sharing.
- Update the binding model to draft 0.9.0 while preserving existing
NodeIds and the `HasAddIn` correction.
- Disambiguate default container identifiers by stage instance, publish
the required read-only capabilities map, and preserve standard and
xRegistry BrowseName namespaces.
- Add registry, file-delivery, sharing, collision, qualified-browse, and
live-discovery regressions. Update the OpenUSD documentation.
Review fixes are in 6ac47a5b82. All four
review threads have been answered and resolved.
**Local validation**
| Command | Result |
| --- | --- |
| `dotnet test tests/Opc.Ua.OpenUsd.Tests/Opc.Ua.OpenUsd.Tests.csproj
--no-restore -f net10.0 -p:CustomTestTarget=net10.0 --verbosity quiet` |
Passed: 1,178 tests. |
| `dotnet test tests/Opc.Ua.OpenUsd.Tests/Opc.Ua.OpenUsd.Tests.csproj -f
net48 -p:CustomTestTarget=net48 --filter
FullyQualifiedName~UsdAssetDeliveryTests --verbosity quiet` | Passed: 36
focused regressions, including build and dependency restore. |
| `dotnet test tests/Opc.Ua.OpenUsd.Tests/Opc.Ua.OpenUsd.Tests.csproj
--no-build --no-restore -f net48 -p:CustomTestTarget=net48 --verbosity
quiet` | Failed: 937 passed; five generator integration tests failed
during fixture setup because Mono does not implement
`ECDsa.Create(ECCurve)`. |
The full `UA.slnx` suite and coverage measurement were not run locally.
## Related Issues
_Reference all GitHub issues this PR addresses. If there is no issue
yet, open one and link it here._
_If this is a relatively large or complex change, a design must have
been discussed in the related tracking issue and signed off (which
becomes the Architectural Decision Record (ADR))._
No linked tracking issue.
## Checklist
_Put an `x` in the boxes that apply. You can complete these step by step
after opening the PR._
- [ ] 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.
- [ ] 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.
This commit is contained in:
+34
-4
@@ -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<ServedAsset>` and the method returns the
|
||||
created `OpenUsdAssetState` nodes as an `ArrayOf<OpenUsdAssetState>`.
|
||||
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<ServedAsset>`; the return value is an `ArrayOf<OpenUsdAssetState>` 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
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ namespace Pumps
|
||||
}
|
||||
}
|
||||
|
||||
private const string PlantRootLayerIdentifier = "asset-repo/Plant.usd";
|
||||
private const string PlantRootLayerIdentifier = "Plant.usda";
|
||||
|
||||
/// <summary>
|
||||
/// 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)
|
||||
|
||||
@@ -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<NodeStateCollection>(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
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -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<NodeStateCollection>(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();
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
/// </remarks>
|
||||
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<SiteCompositionOptions>? 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<SiteNodeManager>();
|
||||
@@ -125,6 +128,7 @@ namespace SiteComposition
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var nodes = new NodeStateCollection();
|
||||
nodes.AddOpcUaXRegistry(context);
|
||||
nodes.AddOpcUaOpenUsd(context);
|
||||
return new ValueTask<NodeStateCollection>(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();
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
<Compile Include="..\IntentEnabledRobot\Simulation\SimulatedCollisionModel.cs" Link="Simulation\SimulatedCollisionModel.cs" />
|
||||
<Compile Include="..\IntentEnabledRobot\Kinematics\ISimulatedArmKinematics.cs" Link="Kinematics\ISimulatedArmKinematics.cs" />
|
||||
<Compile Include="..\IntentEnabledRobot\Kinematics\SimulatedArmKinematics.cs" Link="Kinematics\SimulatedArmKinematics.cs" />
|
||||
<Compile Include="..\IntentEnabledRobot\CompanionModelProviders.cs" Link="CompanionModelProviders.cs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<!-- The cell stage, palletizer and gripper are embedded and extracted to a
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace Vision.BinPickingCell
|
||||
|
||||
m_cellStage = SystemContext.CreateInstanceOfOpenUsdStageType(
|
||||
stages, new QualifiedName("BinPickingCellStage", ns));
|
||||
m_cellStage.ReferenceTypeId = ReferenceTypeIds.Organizes;
|
||||
stages.AddChild(m_cellStage);
|
||||
m_cellStage.CreateOrReplaceRootLayerIdentifier(SystemContext, null).Value = RootLayerIdentifier;
|
||||
|
||||
@@ -81,7 +82,7 @@ namespace Vision.BinPickingCell
|
||||
}
|
||||
|
||||
root.AddReference(ReferenceTypeIds.HasComponent, true, Opc.Ua.ObjectIds.Server);
|
||||
UsdAssetDelivery.AttachStageAssets(SystemContext, m_cellStage, ns, servedAssets);
|
||||
UsdAssetDelivery.AttachStageAssets(SystemContext, m_cellStage, ns, servedAssets, "bin-picking");
|
||||
SystemContext.AssignInstanceChildNodeIds(root);
|
||||
_ = await Manager.AddNodeAsync(
|
||||
SystemContext,
|
||||
|
||||
@@ -92,6 +92,8 @@ builder.Services.AddSingleton(_ => new BinPickingPalletizerKinematics
|
||||
Collisions = BinPickingCellGeometry.CreateCollisionModel()
|
||||
});
|
||||
builder.Services.AddSingleton<BinPickingRobotCell>();
|
||||
builder.Services.TryAddEnumerable(
|
||||
ServiceDescriptor.Singleton<IRobotIntentModelProvider, Robotics.IntentEnabledRobot.OpenUsdIntentModelProvider>());
|
||||
builder.Services.AddSingleton<BinPickingWorldState>();
|
||||
builder.Services.AddSingleton<IBinPickingTargetProvider, BinPickingTargetProvider>();
|
||||
builder.Services.AddSingleton<BinPickingGroundTruthInferenceProvider>();
|
||||
|
||||
@@ -31,6 +31,7 @@ using System;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.OpenUsd;
|
||||
using Opc.Ua.Robotics.Server;
|
||||
using Opc.Ua.XRegistry;
|
||||
|
||||
namespace Robotics.IntentEnabledRobot
|
||||
{
|
||||
@@ -45,7 +46,8 @@ namespace Robotics.IntentEnabledRobot
|
||||
/// <inheritdoc/>
|
||||
public ArrayOf<string> NamespaceUris => new string[]
|
||||
{
|
||||
Opc.Ua.OpenUsd.Namespaces.OpenUSD
|
||||
Opc.Ua.OpenUsd.Namespaces.OpenUSD,
|
||||
Opc.Ua.XRegistry.Namespaces.xRegistry
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -60,6 +62,7 @@ namespace Robotics.IntentEnabledRobot
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
nodes.AddOpcUaXRegistry(context);
|
||||
nodes.AddOpcUaOpenUsd(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ namespace Robotics.IntentEnabledRobot
|
||||
|
||||
m_cellStage = SystemContext.CreateInstanceOfOpenUsdStageType(
|
||||
stages, new QualifiedName("MinimalIntentRobotStage", ns));
|
||||
m_cellStage.ReferenceTypeId = ReferenceTypeIds.Organizes;
|
||||
stages.AddChild(m_cellStage);
|
||||
m_cellStage.CreateOrReplaceRootLayerIdentifier(SystemContext, null!).Value = RootLayerIdentifier;
|
||||
|
||||
@@ -81,7 +82,7 @@ namespace Robotics.IntentEnabledRobot
|
||||
}
|
||||
|
||||
root.AddReference(ReferenceTypeIds.HasComponent, true, Opc.Ua.ObjectIds.Server);
|
||||
UsdAssetDelivery.AttachStageAssets(SystemContext, m_cellStage, ns, servedAssets);
|
||||
UsdAssetDelivery.AttachStageAssets(SystemContext, m_cellStage, ns, servedAssets, "intent-robots");
|
||||
SystemContext.AssignInstanceChildNodeIds(root);
|
||||
await Manager.AddPredefinedNodeAsync(root, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
@@ -280,7 +281,7 @@ namespace Robotics.IntentEnabledRobot
|
||||
SystemContext.AssignInstanceChildNodeIds(node, previousNodeId, referenceRoot);
|
||||
}
|
||||
|
||||
private const string RootLayerIdentifier = "asset-repo/Bench.usd";
|
||||
private const string RootLayerIdentifier = "Bench.usda";
|
||||
private static readonly string[] s_payloadSlotPrimPaths =
|
||||
[
|
||||
"/World/Payloads/FixtureStack/Slot01",
|
||||
|
||||
@@ -33,6 +33,7 @@ using Opc.Ua.Gpos;
|
||||
using Opc.Ua.OpenUsd;
|
||||
using Opc.Ua.Robotics.Server;
|
||||
using Opc.Ua.Rsl;
|
||||
using Opc.Ua.XRegistry;
|
||||
|
||||
namespace Robotics
|
||||
{
|
||||
@@ -47,7 +48,8 @@ namespace Robotics
|
||||
/// <inheritdoc/>
|
||||
public ArrayOf<string> NamespaceUris => new string[]
|
||||
{
|
||||
Opc.Ua.OpenUsd.Namespaces.OpenUSD
|
||||
Opc.Ua.OpenUsd.Namespaces.OpenUSD,
|
||||
Opc.Ua.XRegistry.Namespaces.xRegistry
|
||||
};
|
||||
|
||||
/// <inheritdoc/>
|
||||
@@ -62,6 +64,7 @@ namespace Robotics
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
nodes.AddOpcUaXRegistry(context);
|
||||
nodes.AddOpcUaOpenUsd(context);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +50,7 @@ namespace Robotics
|
||||
private OpenUsdRootState? m_openUsdRoot;
|
||||
private OpenUsdStageState? m_cellStage;
|
||||
|
||||
private const string CellRootLayerIdentifier = "asset-repo/Cell.usd";
|
||||
private const string CellRootLayerIdentifier = "Cell.usda";
|
||||
|
||||
private async ValueTask MaterialiseOpenUsdFacilityAsync(
|
||||
CancellationToken cancellationToken)
|
||||
@@ -70,6 +70,7 @@ namespace Robotics
|
||||
|
||||
m_cellStage = SystemContext.CreateInstanceOfOpenUsdStageType(
|
||||
stages, new QualifiedName("RobotCellStage", ns));
|
||||
m_cellStage.ReferenceTypeId = ReferenceTypeIds.Organizes;
|
||||
stages.AddChild(m_cellStage);
|
||||
m_cellStage.CreateOrReplaceRootLayerIdentifier(SystemContext, null!)
|
||||
.Value = CellRootLayerIdentifier;
|
||||
@@ -106,7 +107,7 @@ namespace Robotics
|
||||
// §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);
|
||||
UsdAssetDelivery.AttachStageAssets(SystemContext, m_cellStage, ns, servedAssets, "robots");
|
||||
|
||||
SystemContext.AssignInstanceChildNodeIds(root);
|
||||
await Manager.AddPredefinedNodeAsync(root, cancellationToken)
|
||||
|
||||
@@ -18,6 +18,14 @@ The `Opc.Ua.OpenUsd.Server` namespace helps a server expose a live external twin
|
||||
|
||||
Pair these APIs with **Opc.Ua.OpenUsd.Client** on the connector side.
|
||||
|
||||
`UsdAssetDelivery.AttachStageAssets(context, stage, openUsdNs, assets, "pumps")`
|
||||
bootstraps shared logical Resources and their first exact Versions in
|
||||
`Server/OpenUSD/Artifacts`. Each stage's `Assets` folder only organizes references
|
||||
to those Resources. Reusing a container and asset identifier reuses its Resource;
|
||||
conflicting immutable definitions are rejected. The four-argument overload uses
|
||||
the stage's symbolic name as the container identifier. Call before final NodeId
|
||||
assignment and address-space registration.
|
||||
|
||||
## Scene materialization
|
||||
|
||||
The `Opc.Ua.OpenUsd.Server.Scene` namespace materializes a composed USD stage inside
|
||||
|
||||
@@ -29,9 +29,12 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Opc.Ua;
|
||||
using Opc.Ua.OpenUsd;
|
||||
using Opc.Ua.XRegistry;
|
||||
|
||||
namespace Opc.Ua.OpenUsd.Server
|
||||
{
|
||||
@@ -56,63 +59,231 @@ namespace Opc.Ua.OpenUsd.Server
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Serves the draft OPC UA — OpenUSD Bindings asset-content-delivery capability
|
||||
/// (spec §5.15): builds a stage's <c>Assets</c> folder of <c>OpenUsdAssetType</c>
|
||||
/// instances, each exposing an artist-authored USD layer as a read-only Part 5
|
||||
/// <see cref="FileState"/> (Open/Read/Close streaming) plus a SHA-256 digest, so a
|
||||
/// generic connector can fetch, verify, cache, compose, and render the twin with no
|
||||
/// external asset resolver.
|
||||
/// Bootstraps read-only USD resources and their first exact Versions in the
|
||||
/// server's artifact registry. Stage Assets folders are non-owning views.
|
||||
/// </summary>
|
||||
public static class UsdAssetDelivery
|
||||
{
|
||||
/// <summary>
|
||||
/// Attaches an <c>Assets</c> folder (if absent) to the stage and materialises one
|
||||
/// <c>OpenUsdAssetType</c> per served layer. Call BEFORE the facility's per-instance
|
||||
/// NodeId assignment + registration; returns the created assets so a caller can link a
|
||||
/// component binding's <c>ComponentAssetNode</c> to one after NodeIds are assigned.
|
||||
/// Allocates a distinct asset container from the stage's name and reuses it for that stage.
|
||||
/// Call before the facility's NodeId assignment and registration.
|
||||
/// </summary>
|
||||
public static ArrayOf<OpenUsdAssetState> AttachStageAssets(
|
||||
ISystemContext context, OpenUsdStageState stage, ushort openUsdNs, ArrayOf<ServedAsset> assets)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
if (stage is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stage));
|
||||
}
|
||||
return AttachStageAssets(
|
||||
context, stage, openUsdNs, assets,
|
||||
s_defaultContainerIds.GetValue(stage, key => AllocateStageContainerId(context, key)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers immutable layers in an asset container and organizes those logical
|
||||
/// Resources through the stage's Assets view. Repeated definitions must have the
|
||||
/// same bytes, kind and content type. Different stages can share one container.
|
||||
/// Call before the facility's NodeId assignment and registration.
|
||||
/// </summary>
|
||||
/// <exception cref="ArgumentException">The container identifier is not symbolic.</exception>
|
||||
/// <exception cref="InvalidOperationException">
|
||||
/// The stage is not under an OpenUSD root, or an existing resource has a conflicting definition.
|
||||
/// </exception>
|
||||
public static ArrayOf<OpenUsdAssetState> AttachStageAssets(
|
||||
ISystemContext context,
|
||||
OpenUsdStageState stage,
|
||||
ushort openUsdNs,
|
||||
ArrayOf<ServedAsset> assets,
|
||||
string assetContainerId)
|
||||
{
|
||||
if (context is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
if (stage is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(stage));
|
||||
}
|
||||
if (string.IsNullOrEmpty(assetContainerId) ||
|
||||
XRegistryIdentifier.FromSourceIdentity(assetContainerId) != assetContainerId)
|
||||
{
|
||||
throw new ArgumentException("The asset container identifier must be symbolic.", nameof(assetContainerId));
|
||||
}
|
||||
if (stage.Parent is not FolderState { Parent: OpenUsdRootState root })
|
||||
{
|
||||
throw new InvalidOperationException("The stage must belong to the OpenUSD facility's Stages folder.");
|
||||
}
|
||||
|
||||
OpenUsdArtifactRegistryState registry = root.Artifacts ?? root.CreateOrReplaceArtifacts(
|
||||
context, context.CreateOpenUsdRootType_Artifacts(root, forInstance: true));
|
||||
registry.RegistryId!.Value = "openusd-artifacts";
|
||||
registry.AddName(context).Name!.Value = "OpenUSD artifacts";
|
||||
registry.DisplayName = new LocalizedText(registry.Name!.Value);
|
||||
registry.AddXid(context).Xid!.Value = "/";
|
||||
registry.AddEpoch(context).Epoch!.Value = 1;
|
||||
if (registry.Capabilities is null)
|
||||
{
|
||||
registry.AddSpecVersion(context).SpecVersion!.Value = "0.1.0";
|
||||
registry.AddCapabilities(context);
|
||||
WireReadOnlyFile(
|
||||
registry.Capabilities!,
|
||||
Encoding.UTF8.GetBytes(
|
||||
"{\"available\":{\"entities\":{\"mutable\":false}},\"specversions\":[\"0.1.0\"],\"mutable\":[]}"),
|
||||
"application/json");
|
||||
}
|
||||
|
||||
OpenUsdAssetGroupState? group = registry.FindChild(
|
||||
context, new QualifiedName(assetContainerId, openUsdNs)) as OpenUsdAssetGroupState;
|
||||
if (group is null)
|
||||
{
|
||||
group = context.CreateInstanceOfOpenUsdAssetGroupType(
|
||||
registry, new QualifiedName(assetContainerId, openUsdNs));
|
||||
group.ReferenceTypeId = ReferenceTypeIds.Organizes;
|
||||
group.GroupId!.Value = assetContainerId;
|
||||
group.AssetContainerId!.Value = assetContainerId;
|
||||
group.Name!.Value = assetContainerId;
|
||||
group.DisplayName = new LocalizedText(assetContainerId);
|
||||
group.AddXid(context).Xid!.Value = "/usdassetgroups/" + assetContainerId;
|
||||
group.AddEpoch(context).Epoch!.Value = 1;
|
||||
registry.AddChild(group);
|
||||
}
|
||||
|
||||
var created = new List<OpenUsdAssetState>();
|
||||
// Assets is an optional member, so it is not auto-created by the factory;
|
||||
// supply a generated node (passing null! leaves it unnamed/untyped).
|
||||
FolderState folder = stage.Assets ?? stage.CreateOrReplaceAssets(
|
||||
context, context.CreateOpenUsdStageType_Assets(stage, forInstance: true));
|
||||
|
||||
foreach (ServedAsset a in assets)
|
||||
{
|
||||
OpenUsdAssetState asset = context.CreateInstanceOfOpenUsdAssetType(
|
||||
folder, new QualifiedName(SafeBrowseName(a.Identifier), openUsdNs));
|
||||
asset.ReferenceTypeId = ReferenceTypeIds.HasComponent;
|
||||
folder.AddChild(asset);
|
||||
|
||||
asset.CreateOrReplaceAssetIdentifier(context, null!).Value = a.Identifier;
|
||||
asset.CreateOrReplaceAssetKind(context, null!).Value = a.Kind;
|
||||
asset.CreateOrReplaceMediaType(
|
||||
context, context.CreateOpenUsdAssetType_MediaType(asset, forInstance: true)).Value = a.MediaType;
|
||||
|
||||
string identifier = a.Identifier;
|
||||
while (identifier.StartsWith("./", StringComparison.Ordinal))
|
||||
{
|
||||
identifier = identifier[2..];
|
||||
}
|
||||
if (string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
throw new ArgumentException("A served asset must have an authored identifier.", nameof(assets));
|
||||
}
|
||||
byte[] bytes = (byte[])a.Bytes.Clone();
|
||||
byte[] digest;
|
||||
#pragma warning disable CA1850 // Prefer static HashData (net48/netstandard2.0 compatibility)
|
||||
using (var sha = System.Security.Cryptography.SHA256.Create())
|
||||
{
|
||||
digest = sha.ComputeHash(a.Bytes);
|
||||
digest = sha.ComputeHash(bytes);
|
||||
}
|
||||
#pragma warning restore CA1850
|
||||
asset.CreateOrReplaceDigest(
|
||||
context, context.CreateOpenUsdAssetType_Digest(asset, forInstance: true)).Value = (ByteString)digest;
|
||||
asset.CreateOrReplaceDigestAlgorithm(
|
||||
context, context.CreateOpenUsdAssetType_DigestAlgorithm(asset, forInstance: true))
|
||||
.Value = OpenUsdDigestAlgorithmEnum.Sha256;
|
||||
|
||||
// The asset node itself IS the Part 5 file (OpenUsdAssetType : FileType);
|
||||
// wire the read-only streaming handlers directly onto it.
|
||||
WireReadOnlyFile(asset, a.Bytes, a.MediaType);
|
||||
var siblings = new List<BaseInstanceState>();
|
||||
group.GetChildren(context, siblings);
|
||||
var resourceIds = new List<string>();
|
||||
OpenUsdAssetState? asset = null;
|
||||
foreach (BaseInstanceState child in siblings)
|
||||
{
|
||||
if (child is OpenUsdAssetState resource)
|
||||
{
|
||||
resourceIds.Add(resource.ResourceId!.Value);
|
||||
if (resource.AssetIdentifier!.Value == identifier)
|
||||
{
|
||||
asset = resource;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (asset is null)
|
||||
{
|
||||
string resourceId = XRegistryIdentifier.FromSourceIdentity(identifier, resourceIds);
|
||||
string xid = group.Xid!.Value + "/usdassets/" + resourceId;
|
||||
asset = CreateAsset(context, group, openUsdNs, resourceId, identifier, xid, a, bytes, digest);
|
||||
OpenUsdAssetVersionsState versions = asset.CreateOrReplaceVersions(
|
||||
context, context.CreateOpenUsdAssetType_Versions(asset, forInstance: true));
|
||||
OpenUsdAssetState version = CreateAsset(
|
||||
context, versions, openUsdNs, "1", identifier, xid + "/versions/1", a, bytes, digest);
|
||||
version.ResourceId!.Value = resourceId;
|
||||
versions.AddChild(version);
|
||||
group.AddChild(asset);
|
||||
}
|
||||
else if (asset.AssetKind!.Value != a.Kind ||
|
||||
asset.ContentType!.Value != a.MediaType ||
|
||||
!asset.Digest!.Value.Span.SequenceEqual(digest))
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The immutable artifact '{assetContainerId}/{identifier}' is already defined differently.");
|
||||
}
|
||||
|
||||
if (!folder.ReferenceExists(ReferenceTypeIds.Organizes, false, asset.NodeId))
|
||||
{
|
||||
folder.AddReference(ReferenceTypeIds.Organizes, false, asset.NodeId);
|
||||
}
|
||||
if (!asset.ReferenceExists(ReferenceTypeIds.Organizes, true, folder.NodeId))
|
||||
{
|
||||
asset.AddReference(ReferenceTypeIds.Organizes, true, folder.NodeId);
|
||||
}
|
||||
created.Add(asset);
|
||||
}
|
||||
return created;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Allocates a symbolic stage container without taking an existing registry group's identifier.
|
||||
/// </summary>
|
||||
private static string AllocateStageContainerId(ISystemContext context, OpenUsdStageState stage)
|
||||
{
|
||||
if (stage.Parent is not FolderState { Parent: OpenUsdRootState root })
|
||||
{
|
||||
throw new InvalidOperationException("The stage must belong to the OpenUSD facility's Stages folder.");
|
||||
}
|
||||
|
||||
var children = new List<BaseInstanceState>();
|
||||
root.Artifacts?.GetChildren(context, children);
|
||||
var containerIds = new List<string>();
|
||||
foreach (BaseInstanceState child in children)
|
||||
{
|
||||
if (child is OpenUsdAssetGroupState group)
|
||||
{
|
||||
containerIds.Add(group.GroupId!.Value);
|
||||
}
|
||||
}
|
||||
return XRegistryIdentifier.FromSourceIdentity(
|
||||
stage.BrowseName.Name ?? throw new ArgumentException("The stage must be named.", nameof(stage)),
|
||||
containerIds);
|
||||
}
|
||||
|
||||
private static OpenUsdAssetState CreateAsset(
|
||||
ISystemContext context,
|
||||
NodeState parent,
|
||||
ushort openUsdNs,
|
||||
string browseName,
|
||||
string identifier,
|
||||
string xid,
|
||||
ServedAsset definition,
|
||||
byte[] bytes,
|
||||
byte[] digest)
|
||||
{
|
||||
OpenUsdAssetState asset = context.CreateInstanceOfOpenUsdAssetType(
|
||||
parent, new QualifiedName(browseName, openUsdNs));
|
||||
asset.ReferenceTypeId = ReferenceTypeIds.Organizes;
|
||||
asset.ResourceId!.Value = browseName;
|
||||
asset.Name!.Value = identifier;
|
||||
asset.DisplayName = new LocalizedText(identifier);
|
||||
asset.AssetIdentifier!.Value = identifier;
|
||||
asset.AssetKind!.Value = definition.Kind;
|
||||
asset.AddVersionId(context).VersionId!.Value = "1";
|
||||
asset.AddXid(context).Xid!.Value = xid;
|
||||
asset.AddEpoch(context).Epoch!.Value = 1;
|
||||
asset.AddContentType(context).ContentType!.Value = definition.MediaType;
|
||||
asset.CreateOrReplaceMediaType(
|
||||
context, context.CreateOpenUsdAssetType_MediaType(asset, forInstance: true)).Value = definition.MediaType;
|
||||
asset.CreateOrReplaceDigest(
|
||||
context, context.CreateOpenUsdAssetType_Digest(asset, forInstance: true)).Value = (ByteString)digest;
|
||||
asset.CreateOrReplaceDigestAlgorithm(
|
||||
context, context.CreateOpenUsdAssetType_DigestAlgorithm(asset, forInstance: true))
|
||||
.Value = OpenUsdDigestAlgorithmEnum.Sha256;
|
||||
WireReadOnlyFile(asset, bytes, definition.MediaType);
|
||||
return asset;
|
||||
}
|
||||
|
||||
// Attaches read-only Open/Read/Close/GetPosition/SetPosition handlers over an
|
||||
// in-memory byte[] to an existing (generator-materialised) FileState.
|
||||
private static void WireReadOnlyFile(FileState file, byte[] bytes, string mediaType)
|
||||
@@ -158,21 +329,10 @@ namespace Opc.Ua.OpenUsd.Server
|
||||
// Read-only: any Write method stays unwired (server rejects the call).
|
||||
}
|
||||
|
||||
// Sanitises an asset identifier into a valid single-segment BrowseName.
|
||||
private static string SafeBrowseName(string identifier)
|
||||
{
|
||||
if (string.IsNullOrEmpty(identifier))
|
||||
{
|
||||
return "Asset";
|
||||
}
|
||||
var sb = new System.Text.StringBuilder(identifier.Length);
|
||||
foreach (char c in identifier)
|
||||
{
|
||||
sb.Append(char.IsLetterOrDigit(c) || c == '_' ? c : '_');
|
||||
}
|
||||
string s = sb.ToString();
|
||||
return char.IsLetter(s[0]) || s[0] == '_' ? s : "_" + s;
|
||||
}
|
||||
/// <summary>
|
||||
/// Remembers default container allocations by stage instance without retaining discarded stages.
|
||||
/// </summary>
|
||||
private static readonly ConditionalWeakTable<OpenUsdStageState, string> s_defaultContainerIds = new();
|
||||
|
||||
/// <summary>
|
||||
/// Read-only in-memory backing for a Part 5 <see cref="FileState"/>: hands out
|
||||
|
||||
@@ -94,3 +94,22 @@ OpenUsdAssetType_DigestAlgorithm,6064,Variable
|
||||
OpenUsdStageType_Assets,6065,Object
|
||||
OpenUsdComponentBindingType_ComponentAssetNode,6066,Variable
|
||||
OpenUsdLiveBindingType_TargetNodeId,6067,Variable
|
||||
OpenUsdArtifactRegistryType,1012,ObjectType
|
||||
OpenUsdAssetGroupType,1013,ObjectType
|
||||
OpenUsdSchemaPluginGroupType,1014,ObjectType
|
||||
OpenUsdAssetVersionsType,1015,ObjectType
|
||||
OpenUsdAssetGroupType_AssetContainerId,6068,Variable
|
||||
OpenUsdSchemaPluginGroupType_PluginName,6069,Variable
|
||||
OpenUsdAssetType_DependsOn,6070,Variable
|
||||
OpenUsdRootType_Artifacts,6071,Object
|
||||
OpenUsdArtifactRegistryType_AssetContainer_Placeholder,6072,Object
|
||||
OpenUsdArtifactRegistryType_SchemaPlugin_Placeholder,6073,Object
|
||||
OpenUsdAssetGroupType_Asset_Placeholder,6074,Object
|
||||
OpenUsdSchemaPluginGroupType_Asset_Placeholder,6075,Object
|
||||
http___opcfoundation_org_UA_OpenUSD_,6076,Object
|
||||
http___opcfoundation_org_UA_OpenUSD__NamespaceUri,6077,Variable
|
||||
http___opcfoundation_org_UA_OpenUSD__NamespaceVersion,6078,Variable
|
||||
http___opcfoundation_org_UA_OpenUSD__NamespacePublicationDate,6079,Variable
|
||||
http___opcfoundation_org_UA_OpenUSD__IsNamespaceSubset,6080,Variable
|
||||
OpenUsdAssetVersionsType_Version_Placeholder,6081,Object
|
||||
OpenUsdAssetType_Versions,6082,Object
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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**.
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Opc.Ua.Server\Opc.Ua.Server.csproj" />
|
||||
<ProjectReference Include="..\Opc.Ua.XRegistry\Opc.Ua.XRegistry.csproj" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\tools\Opc.Ua.SourceGeneration\Opc.Ua.SourceGeneration.csproj">
|
||||
@@ -40,6 +41,12 @@
|
||||
<None Remove="Model\Opc.Ua.OpenUsd.Scene.NodeSet2.csv" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\Opc.Ua.XRegistry\Opc.Ua.XRegistry.NodeSet2.xml">
|
||||
<ModelSourceGeneratorPrefix>Opc.Ua.XRegistry</ModelSourceGeneratorPrefix>
|
||||
<ModelSourceGeneratorName>XRegistry</ModelSourceGeneratorName>
|
||||
<ModelSourceGeneratorModelUri>http://opcfoundation.org/UA/xRegistry/</ModelSourceGeneratorModelUri>
|
||||
<ModelSourceGeneratorIgnore>true</ModelSourceGeneratorIgnore>
|
||||
</AdditionalFiles>
|
||||
<AdditionalFiles Include="Model\Opc.Ua.OpenUsd.NodeSet2.xml">
|
||||
<ModelSourceGeneratorPrefix>Opc.Ua.OpenUsd</ModelSourceGeneratorPrefix>
|
||||
</AdditionalFiles>
|
||||
|
||||
@@ -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<ReferenceDescription> 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<ReferenceDescription> resources = await BrowseObjectsAsync(
|
||||
groupId, Opc.Ua.ReferenceTypeIds.Organizes).ConfigureAwait(false);
|
||||
Assert.That(resources, Has.Count.EqualTo(2));
|
||||
|
||||
ArrayOf<ReferenceDescription> stageObjects = await BrowseObjectsAsync(
|
||||
stageId, Opc.Ua.ReferenceTypeIds.HasComponent).ConfigureAwait(false);
|
||||
ReferenceDescription view = stageObjects.ToList().Single(
|
||||
reference => reference.BrowseName.Name == "Assets");
|
||||
ArrayOf<ReferenceDescription> 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<ReferenceDescription> 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<ReferenceDescription> 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<ArrayOf<ReferenceDescription>> 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));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<IReference>();
|
||||
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<BaseInstanceState>();
|
||||
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<IReference>();
|
||||
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<BaseInstanceState>();
|
||||
asset.Versions!.GetChildren(context, children);
|
||||
OpenUsdAssetState version = children.OfType<OpenUsdAssetState>().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<OpenUsdAssetState> 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(
|
||||
"<Version>", (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<System.InvalidOperationException>(() => 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<OpenUsdAssetState> created = UsdAssetDelivery.AttachStageAssets(
|
||||
context, stage, ns, new ServedAsset[] { Asset(string.Empty) });
|
||||
|
||||
Assert.That(created[0].BrowseName.Name, Is.EqualTo("Asset"));
|
||||
Assert.Throws<System.ArgumentException>(() => UsdAssetDelivery.AttachStageAssets(
|
||||
context, stage, ns, new ServedAsset[] { Asset(string.Empty) }));
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
||||
Reference in New Issue
Block a user