/* ========================================================================
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
*
* OPC Foundation MIT License 1.00
*
* Permission is hereby granted, free of charge, to any person
* obtaining a copy of this software and associated documentation
* files (the "Software"), to deal in the Software without
* restriction, including without limitation the rights to use,
* copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following
* conditions:
*
* The above copyright notice and this permission notice shall be
* included in all copies or substantial portions of the Software.
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*
* The complete license agreement can be found here:
* http://opcfoundation.org/License/MIT/1.00/
* ======================================================================*/
using System.Collections.Generic;
using System.Threading.Tasks;
using NUnit.Framework;
using Opc.Ua.Vision;
using Opc.Ua.Vision.Client;
namespace Opc.Ua.Vision.Tests
{
///
/// Tests for through the harness.
/// Every test drives the client via the public front door
/// (Client.Sensor(id)) so the underlying
/// VisionSensorTypeClient proxy code is exercised as well.
///
[TestFixture]
[Category("Vision")]
public sealed class VisionSensorClientTests
{
[Test]
public async Task ReadIdentityReturnsAllPopulatedMembers()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.SensorId,
new(2500u, 3), "cam-1");
harness.AddValueChild(harness.SensorNodeId, BrowseNames.RealityKind,
new(2501u, 3), (int)VisionRealityKindEnum.Physical);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.Modality,
new(2502u, 3), (int)VisionSensorModalityEnum.Area2D);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.Manufacturer,
new(2503u, 3), new LocalizedText("ACME"));
harness.AddValueChild(harness.SensorNodeId, BrowseNames.Model,
new(2504u, 3), new LocalizedText("Model X"));
harness.AddValueChild(harness.SensorNodeId, BrowseNames.SerialNumber,
new(2505u, 3), "SN-1");
harness.AddValueChild(harness.SensorNodeId, BrowseNames.DeviceUri,
new(2506u, 3), "urn:acme:camera:1");
harness.AddValueChild(harness.SensorNodeId, BrowseNames.FrameId,
new(2507u, 3), "cam-1");
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionSensorIdentity identity = await sensor.ReadIdentityAsync()
.ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(identity.SensorId, Is.EqualTo("cam-1"));
Assert.That(identity.RealityKind, Is.EqualTo(VisionRealityKindEnum.Physical));
Assert.That(identity.Modality, Is.EqualTo(VisionSensorModalityEnum.Area2D));
Assert.That(identity.Manufacturer.Text, Is.EqualTo("ACME"));
Assert.That(identity.Model.Text, Is.EqualTo("Model X"));
Assert.That(identity.SerialNumber, Is.EqualTo("SN-1"));
Assert.That(identity.DeviceUri, Is.EqualTo("urn:acme:camera:1"));
Assert.That(identity.FrameId, Is.EqualTo("cam-1"));
Assert.That(identity.NodeId, Is.EqualTo(harness.SensorNodeId));
});
}
[Test]
public async Task ReadImageMembersReturnsNullWhenCoreMembersAbsent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionImageSensorSnapshot? snapshot = await sensor.ReadImageMembersAsync()
.ConfigureAwait(false);
Assert.That(snapshot, Is.Null,
"Sensor with no Width, Height or PixelFormat is not an image sensor.");
}
[Test]
public async Task ReadImageMembersReturnsPopulatedSnapshotWhenPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.Width,
new(2510u, 3), 1920u);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.Height,
new(2511u, 3), 1080u);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.PixelFormat,
new(2512u, 3), "Mono8");
harness.AddValueChild(harness.SensorNodeId, BrowseNames.ExposureTime,
new(2513u, 3), 0.001);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.Gain,
new(2514u, 3), 1.5);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.AcquisitionFrameRate,
new(2515u, 3), 30.0);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionImageSensorSnapshot? snapshot = await sensor.ReadImageMembersAsync()
.ConfigureAwait(false);
Assert.That(snapshot, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(snapshot!.Width, Is.EqualTo(1920u));
Assert.That(snapshot.Height, Is.EqualTo(1080u));
Assert.That(snapshot.PixelFormat, Is.EqualTo("Mono8"));
Assert.That(snapshot.ExposureTime, Is.EqualTo(0.001));
Assert.That(snapshot.Gain, Is.EqualTo(1.5));
Assert.That(snapshot.AcquisitionFrameRate, Is.EqualTo(30.0));
});
}
[Test]
public async Task ReadDepthMembersReturnsNullWhenNoDepthMembersPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.Depth3DSensorType);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionDepth3DSensorSnapshot? snapshot = await sensor.ReadDepthMembersAsync()
.ConfigureAwait(false);
Assert.That(snapshot, Is.Null);
}
[Test]
public async Task ReadDepthMembersReturnsPopulatedSnapshotWhenPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.Depth3DSensorType);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.MinDepth,
new(2520u, 3), 0.1);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.MaxDepth,
new(2521u, 3), 5.0);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.DepthScale,
new(2522u, 3), 0.001);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.Baseline,
new(2523u, 3), 0.05);
harness.AddValueChild(harness.SensorNodeId, BrowseNames.PointsPerFrame,
new(2524u, 3), 640u * 480u);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionDepth3DSensorSnapshot? snapshot = await sensor.ReadDepthMembersAsync()
.ConfigureAwait(false);
Assert.That(snapshot, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(snapshot!.MinDepth, Is.EqualTo(0.1));
Assert.That(snapshot.MaxDepth, Is.EqualTo(5.0));
Assert.That(snapshot.DepthScale, Is.EqualTo(0.001));
Assert.That(snapshot.Baseline, Is.EqualTo(0.05));
Assert.That(snapshot.PointsPerFrame, Is.EqualTo(640u * 480u));
});
}
[Test]
public async Task ReadOpticsReturnsNullWhenNotPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionOpticsSnapshot? optics = await sensor.ReadOpticsAsync()
.ConfigureAwait(false);
Assert.That(optics, Is.Null);
}
[Test]
public async Task ReadOpticsReadsMembersWhenPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
harness.AddChild(harness.SensorNodeId, BrowseNames.Optics, harness.OpticsNodeId);
harness.AddValueChild(harness.OpticsNodeId, BrowseNames.FocalLength,
new(2530u, 3), 8.0);
harness.AddValueChild(harness.OpticsNodeId, BrowseNames.Aperture,
new(2531u, 3), 2.8);
harness.AddValueChild(harness.OpticsNodeId, BrowseNames.MinimumWorkingDistance,
new(2532u, 3), 0.3);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionOpticsSnapshot? optics = await sensor.ReadOpticsAsync()
.ConfigureAwait(false);
Assert.That(optics, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(optics!.NodeId, Is.EqualTo(harness.OpticsNodeId));
Assert.That(optics.FocalLength, Is.EqualTo(8.0));
Assert.That(optics.Aperture, Is.EqualTo(2.8));
Assert.That(optics.WorkingDistance, Is.EqualTo(0.3));
});
}
[Test]
public async Task ReadIlluminationReturnsNullWhenNotPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionIlluminationSnapshot? illumination = await sensor.ReadIlluminationAsync()
.ConfigureAwait(false);
Assert.That(illumination, Is.Null);
}
[Test]
public async Task ReadIlluminationReadsMembersWhenPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
harness.AddChild(harness.SensorNodeId, BrowseNames.Illumination,
harness.IlluminationNodeId);
harness.AddValueChild(harness.IlluminationNodeId, BrowseNames.Wavelength,
new(2540u, 3), 850.0);
harness.AddValueChild(harness.IlluminationNodeId, BrowseNames.RelativeIntensity,
new(2541u, 3), 0.9);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionIlluminationSnapshot? illumination = await sensor.ReadIlluminationAsync()
.ConfigureAwait(false);
Assert.That(illumination, Is.Not.Null);
Assert.Multiple(() =>
{
Assert.That(illumination!.NodeId, Is.EqualTo(harness.IlluminationNodeId));
Assert.That(illumination.Wavelength, Is.EqualTo(850.0));
Assert.That(illumination.RelativeIntensity, Is.EqualTo(0.9));
});
}
[Test]
public async Task GetMountedFrameIdReturnsNullNodeIdWhenNotMounted()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
NodeId mount = await sensor.GetMountedFrameIdAsync().ConfigureAwait(false);
Assert.That(mount.IsNull, Is.True);
}
[Test]
public async Task GetMountedFrameIdReturnsFrameWhenMountedOnReferenceExists()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
harness.AppendBrowse(harness.SensorNodeId, new ReferenceDescription
{
NodeId = new ExpandedNodeId(harness.FrameNodeId),
BrowseName = new QualifiedName("MyMount", harness.VisionNamespaceIndex),
DisplayName = new LocalizedText("MyMount"),
NodeClass = NodeClass.Object,
TypeDefinition = new ExpandedNodeId(
new NodeId(ObjectTypes.CoordinateFrameType, harness.VisionNamespaceIndex)),
ReferenceTypeId = new NodeId(ReferenceTypes.MountedOn, harness.VisionNamespaceIndex),
IsForward = true
});
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
NodeId mount = await sensor.GetMountedFrameIdAsync().ConfigureAwait(false);
Assert.That(mount, Is.EqualTo(harness.FrameNodeId));
}
[Test]
public async Task EnumerateCalibrationsYieldsCalibrationsFromNestedFolder()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
harness.AddChild(harness.SensorNodeId, BrowseNames.Calibrations,
harness.CalibrationsFolderId);
harness.AddBrowse(harness.CalibrationsFolderId,
[harness.Ref(harness.IntrinsicCalibrationNodeId, "Intrinsic",
ObjectTypes.IntrinsicCalibrationType)]);
var entries = new List();
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
await foreach (VisionNodeEntry entry in sensor.EnumerateCalibrationsAsync())
{
entries.Add(entry);
}
Assert.That(entries.Count, Is.EqualTo(1));
Assert.That(entries[0].NodeId, Is.EqualTo(harness.IntrinsicCalibrationNodeId));
}
[Test]
public async Task ReadIntrinsicCalibrationThrowsForNullNodeId()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
Assert.ThrowsAsync(async () =>
await sensor.ReadIntrinsicCalibrationAsync(NodeId.Null)
.ConfigureAwait(false));
}
[Test]
public async Task ReadIntrinsicCalibrationReturnsPopulatedSnapshot()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
harness.AddValueChild(harness.IntrinsicCalibrationNodeId, BrowseNames.CalibrationId,
new(2600u, 3), "cal-1");
harness.AddValueChild(harness.IntrinsicCalibrationNodeId, BrowseNames.PerformedAt,
new(2601u, 3), new DateTimeUtc(new System.DateTime(2024, 1, 1)));
harness.AddValueChild(harness.IntrinsicCalibrationNodeId, BrowseNames.Valid,
new(2602u, 3), true);
harness.AddValueChild(harness.IntrinsicCalibrationNodeId, BrowseNames.ResidualError,
new(2603u, 3), 0.15);
harness.AddValueChild(harness.IntrinsicCalibrationNodeId, BrowseNames.Method,
new(2604u, 3), "Zhang");
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionIntrinsicCalibrationSnapshot snapshot = await sensor
.ReadIntrinsicCalibrationAsync(harness.IntrinsicCalibrationNodeId)
.ConfigureAwait(false);
Assert.Multiple(() =>
{
Assert.That(snapshot.CalibrationId, Is.EqualTo("cal-1"));
Assert.That(snapshot.Valid, Is.True);
Assert.That(snapshot.ResidualError, Is.EqualTo(0.15));
Assert.That(snapshot.Method, Is.EqualTo("Zhang"));
Assert.That(snapshot.NodeId, Is.EqualTo(harness.IntrinsicCalibrationNodeId));
});
}
[Test]
public async Task ReadExtrinsicCalibrationThrowsForNullNodeId()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
Assert.ThrowsAsync(async () =>
await sensor.ReadExtrinsicCalibrationAsync(NodeId.Null)
.ConfigureAwait(false));
}
[Test]
public async Task OpenMediaReturnsNullWhenNoMediaObjectPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionMediaClient? media = await sensor.OpenMediaAsync()
.ConfigureAwait(false);
Assert.That(media, Is.Null);
}
[Test]
public async Task OpenMediaReturnsClientWhenMediaObjectPresent()
{
var harness = new VisionSessionHarness();
harness.AddSensor(ObjectTypes.ImageSensorType);
harness.AddChild(harness.SensorNodeId, BrowseNames.Media, harness.MediaNodeId);
VisionSensorClient sensor = harness.Client.Sensor(harness.SensorNodeId);
VisionMediaClient? media = await sensor.OpenMediaAsync()
.ConfigureAwait(false);
Assert.That(media, Is.Not.Null);
Assert.That(media!.MediaNodeId, Is.EqualTo(harness.MediaNodeId));
}
[Test]
public void ConstructorRejectsNullSensorNodeId()
{
var harness = new VisionSessionHarness();
Assert.Throws(() => harness.Client.Sensor(NodeId.Null));
}
}
}