mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-26 04:19:53 +08:00
**Stack 4/5** — base #4125. Part of the #4096 split; see #4096 for the overall picture. > Review after #4123, #4124 and #4125. ## What this is The generic OPC UA to OpenUSD binding, scene materialization, and the connector that drives a live stage from a running server. ## Contents **Binding (Part 1)** — `Opc.Ua.OpenUsd` and `Opc.Ua.OpenUsd.Server`. A server describes how its Variables drive USD prims, and serves the USD layers it refers to as Part 5 files with a content digest. **Scene materialization (Part 2)** — `Opc.Ua.OpenUsdScene` and `Opc.Ua.OpenUsdScene.Server`, including the USDA writer. - **Escaping fix**: string values, composition-arc references, variant set names and selections, `doc` and prim names were all emitted unescaped, so a value carrying a quote or a newline could author its own layer syntax. Every emit site now goes through the escapers. The tests assert the right property — not that the payload is absent (it legitimately survives inside the escaped literal) but that no payload becomes its own line of layer syntax. **Client** — `Opc.Ua.OpenUsd.Client` streams live values into an `IUsdSink`. `CompositeUsdSink` fans out to several sinks; the file sink interpolates between server samples so motion is continuous rather than stepped at the publishing interval. **Connector** — `tools/Opc.Ua.OpenUsd.Connector` composes a stage from a live server. The optional in-process viewport lives in a separate assembly probed at run time, so the connector still runs — and says why — when it is absent. - **Fix**: the asset cache and PKI root moved off the shared temp directory to `LocalApplicationData`. **Sample** — `PumpDeviceIntegrationServer` gains an OpenUSD representation of the pump. ## Notes for the reviewer - The viewport assembly is net10.0-only. It is what exercises the `Directory.Build.targets` change from 1/5: without it, the `net8.0` leg of the per-TFM CI build fails with `CS0433` because the `System.Threading.Lock` polyfill collides with the real type. - `UsdAssetDelivery` hashes through `SHA256.Create` rather than the static `HashData`, which does not exist on the net472/net48 targets this test project also builds for. ## Validation `dotnet build UA.slnx` — 0 errors, 0 warnings. | Suite | Result | | --- | --- | | Opc.Ua.OpenUsd.Tests | 621 / 0 | | Opc.Ua.Di.Tests | 321 / 0 | --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8cbb8cd0-f0cb-4ab0-bea2-6202fbf69485
253 lines
8.9 KiB
C#
253 lines
8.9 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2025 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using NUnit.Framework;
|
|
|
|
namespace Opc.Ua.OpenUsd.Client.Tests
|
|
{
|
|
/// <summary>
|
|
/// Fail-closed edges of the §5.8 conversion profile and of the §5.2 stage-digest
|
|
/// guards that do not require a session.
|
|
/// </summary>
|
|
[TestFixture]
|
|
[Category("OpenUsd")]
|
|
public sealed class OpenUsdConnectorConversionEdgeTests
|
|
{
|
|
private static EUInformation Unit(string code)
|
|
{
|
|
int id = 0;
|
|
foreach (char c in code)
|
|
{
|
|
id = (id << 8) | (byte)c;
|
|
}
|
|
return new EUInformation { UnitId = id, DisplayName = new LocalizedText(code) };
|
|
}
|
|
|
|
private static OpenUsdConnector.BindingInfo Binding(
|
|
OpenUsdRenderTargetKind kind, string? source, string? target)
|
|
{
|
|
return new OpenUsdConnector.BindingInfo
|
|
{
|
|
Kind = kind,
|
|
PrimPath = "/World/Robot",
|
|
PropertyName = "value",
|
|
SourceEngineeringUnits = source == null ? null : Unit(source),
|
|
TargetEngineeringUnits = target == null ? null : Unit(target)
|
|
};
|
|
}
|
|
|
|
[Test]
|
|
public void ConvertLeavesDisplayColorUnresolvedForANonScalarSource()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.DisplayColor, null, null);
|
|
|
|
Variant result = OpenUsdConnector.Convert(b, new Variant("warm"));
|
|
|
|
Assert.That(result.IsNull, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void ConvertLeavesEmissiveColorUnresolvedForANonScalarSource()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.EmissiveColor, null, null);
|
|
|
|
Variant result = OpenUsdConnector.Convert(b, new Variant("bright"));
|
|
|
|
Assert.That(result.IsNull, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void ConvertLeavesTranslationUnresolvedForAnUnhonourableLengthUnit()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Translation, "MTR", "FOT");
|
|
|
|
Variant result = OpenUsdConnector.Convert(b, new Variant(1.0));
|
|
|
|
Assert.That(result.IsNull, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void ConvertLeavesGeoreferenceHeightUnresolvedForAnUnhonourableLengthUnit()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Georeference, "MTR", "FOT");
|
|
b.PropertyName = "cesium:anchor:height";
|
|
|
|
Variant result = OpenUsdConnector.Convert(b, new Variant(12.0));
|
|
|
|
Assert.That(result.IsNull, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void ConvertLeavesGeoreferenceUnresolvedWhenTheTargetAttributeIsUnnamed()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Georeference, null, null);
|
|
b.PropertyName = string.Empty;
|
|
|
|
Variant result = OpenUsdConnector.Convert(b, new Variant(12.0));
|
|
|
|
Assert.That(result.IsNull, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void ConvertScalesRadiansToDegreesForAScalarTarget()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Custom, "C81", "DD");
|
|
|
|
Variant result = OpenUsdConnector.Convert(b, new Variant(Math.PI));
|
|
|
|
Assert.That(result.TryGetValue(out double actual), Is.True);
|
|
Assert.That(actual, Is.EqualTo(180.0).Within(1e-9));
|
|
}
|
|
|
|
[Test]
|
|
public void ConvertScalesDegreesToRadiansForAScalarTarget()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Custom, "DD", "C81");
|
|
|
|
Variant result = OpenUsdConnector.Convert(b, new Variant(180.0));
|
|
|
|
Assert.That(result.TryGetValue(out double actual), Is.True);
|
|
Assert.That(actual, Is.EqualTo(Math.PI).Within(1e-9));
|
|
}
|
|
|
|
[Test]
|
|
public void ConvertLeavesAScalarTargetUnresolvedForAnUnknownUnitPair()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Custom, "MTR", "FOT");
|
|
|
|
Variant result = OpenUsdConnector.Convert(b, new Variant(1.0));
|
|
|
|
Assert.That(result.IsNull, Is.True);
|
|
}
|
|
|
|
[Test]
|
|
public void UnitCodeFallsBackToTheDisplayNameWhenNoUnitIdIsSet()
|
|
{
|
|
var units = new EUInformation { UnitId = 0, DisplayName = new LocalizedText("rad") };
|
|
|
|
string code = OpenUsdConnector.UnitCode(units);
|
|
|
|
Assert.That(code, Is.EqualTo("rad"));
|
|
}
|
|
|
|
[Test]
|
|
public void UnitCodeIsEmptyForAnUndeclaredUnit()
|
|
{
|
|
Assert.That(OpenUsdConnector.UnitCode(null), Is.Empty);
|
|
}
|
|
|
|
[Test]
|
|
public void VerifyStageDigestRejectsARepresentationWithoutADigest()
|
|
{
|
|
var rep = new OpenUsdConnector.RepresentationInfo();
|
|
|
|
bool verified = OpenUsdConnector.VerifyStageDigest(rep, [1, 2, 3]);
|
|
|
|
Assert.That(verified, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void VerifyStageDigestRejectsARepresentationWithoutADigestAlgorithm()
|
|
{
|
|
var rep = new OpenUsdConnector.RepresentationInfo
|
|
{
|
|
RootLayerDigest = new ByteString(new byte[] { 1, 2, 3 }),
|
|
DigestAlgorithm = OpenUsdDigestAlgorithm.None
|
|
};
|
|
|
|
bool verified = OpenUsdConnector.VerifyStageDigest(rep, [1, 2, 3]);
|
|
|
|
Assert.That(verified, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void VerifyStageDigestRejectsNullContent()
|
|
{
|
|
var rep = new OpenUsdConnector.RepresentationInfo
|
|
{
|
|
RootLayerDigest = new ByteString(new byte[] { 1, 2, 3 }),
|
|
DigestAlgorithm = OpenUsdDigestAlgorithm.Sha256
|
|
};
|
|
|
|
bool verified = OpenUsdConnector.VerifyStageDigest(rep, null!);
|
|
|
|
Assert.That(verified, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void TryInvertConversionRejectsAZeroScale()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Custom, null, null);
|
|
b.Scale = 0.0;
|
|
|
|
bool inverted = OpenUsdConnector.TryInvertConversion(b, 1.0, out double uaValue);
|
|
|
|
Assert.That(inverted, Is.False);
|
|
Assert.That(uaValue, Is.Zero);
|
|
}
|
|
|
|
[Test]
|
|
public void TryInvertConversionRejectsAnUnhonourableUnitPair()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Custom, "MTR", "FOT");
|
|
|
|
bool inverted = OpenUsdConnector.TryInvertConversion(b, 1.0, out _);
|
|
|
|
Assert.That(inverted, Is.False);
|
|
}
|
|
|
|
[Test]
|
|
public void TryInvertConversionUndoesTheAngleFactorForARotationTarget()
|
|
{
|
|
OpenUsdConnector.BindingInfo b = Binding(OpenUsdRenderTargetKind.Rotation, "C81", "DD");
|
|
|
|
bool inverted = OpenUsdConnector.TryInvertConversion(b, 180.0, out double uaValue);
|
|
|
|
Assert.That(inverted, Is.True);
|
|
Assert.That(uaValue, Is.EqualTo(Math.PI).Within(1e-9));
|
|
}
|
|
|
|
[Test]
|
|
public void SelectCommandBindingIgnoresABindingWithoutATargetOrMethod()
|
|
{
|
|
var rep = new OpenUsdConnector.RepresentationInfo();
|
|
rep.Bindings.Add(new OpenUsdConnector.BindingInfo
|
|
{
|
|
Intent = OpenUsdIntentProfile.UsdToUaCommand,
|
|
SignalRole = OpenUsdSignalRole.Controllable
|
|
});
|
|
|
|
OpenUsdConnector.BindingInfo? selected = OpenUsdConnector.SelectCommandBinding([rep]);
|
|
|
|
Assert.That(selected, Is.Null);
|
|
}
|
|
}
|
|
}
|