mirror of
https://github.com/OPCFoundation/UA-.NETStandard.git
synced 2026-10-18 22:55:08 +08:00
7e25e1a3d7
# Description The shared AI deployment contracts and instance registration; DI software-upload outcomes, digest handling and simulator; bounded Positioning/Pumps reads; PubSub raw-data/matrix and security-provider handling; Vision read-quality contracts; xRegistry resource writes and instance projection; OpenUSD authoring, embedded viewport and retained-frame capture APIs; standalone sample CLI controls; and the generic Debug/RID package-policy and fork/Dependabot coverage-comment fixes. The Linux native prerequisite step is retained for **Vision tests only**. ## Related Issues - Related tracking issue: [#4461](https://github.com/OPCFoundation/UA-.NETStandard/issues/4461), without automatic closure. - Source work: [#4462](https://github.com/OPCFoundation/UA-.NETStandard/pull/4462), which remains intact for the coordinator's later integration. - Immediate stacked predecessor: [#4804](https://github.com/OPCFoundation/UA-.NETStandard/pull/4804). ## 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. - [ ] 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. - [ ] I have addressed **all** PR feedback received.
291 lines
14 KiB
C#
291 lines
14 KiB
C#
/* ========================================================================
|
|
* Copyright (c) 2005-2026 The OPC Foundation, Inc. All rights reserved.
|
|
*
|
|
* OPC Foundation MIT License 1.00
|
|
*
|
|
* Permission is hereby granted, free of charge, to any person
|
|
* obtaining a copy of this software and associated documentation
|
|
* files (the "Software"), to deal in the Software without
|
|
* restriction, including without limitation the rights to use,
|
|
* copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
* copies of the Software, and to permit persons to whom the
|
|
* Software is furnished to do so, subject to the following
|
|
* conditions:
|
|
*
|
|
* The above copyright notice and this permission notice shall be
|
|
* included in all copies or substantial portions of the Software.
|
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
|
* OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
|
* HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
* WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
|
* OTHER DEALINGS IN THE SOFTWARE.
|
|
*
|
|
* The complete license agreement can be found here:
|
|
* http://opcfoundation.org/License/MIT/1.00/
|
|
* ======================================================================*/
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
using Opc.Ua;
|
|
using Opc.Ua.AI.Server;
|
|
using Opc.Ua.Vision.Perception;
|
|
using Opc.Ua.Vision.Server;
|
|
|
|
namespace Vision.VisualInspectionCell
|
|
{
|
|
internal sealed class VisualInspectionCell
|
|
{
|
|
public VisualInspectionCell(
|
|
VisualInspectionCellOptions options,
|
|
VisualInspectionMediaProvider mediaProvider,
|
|
VisualInspectionInferenceProvider inferenceProvider,
|
|
VisualInspectionFeedbackSink feedbackSink,
|
|
OperatorDialogController operatorDialog,
|
|
ILogger<VisualInspectionCell> logger)
|
|
{
|
|
m_options = options ?? throw new ArgumentNullException(nameof(options));
|
|
m_mediaProvider = mediaProvider ?? throw new ArgumentNullException(nameof(mediaProvider));
|
|
m_inferenceProvider = inferenceProvider ?? throw new ArgumentNullException(nameof(inferenceProvider));
|
|
m_feedbackSink = feedbackSink ?? throw new ArgumentNullException(nameof(feedbackSink));
|
|
m_operatorDialog = operatorDialog ?? throw new ArgumentNullException(nameof(operatorDialog));
|
|
m_logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
|
}
|
|
|
|
public async ValueTask ConfigureAsync(IVisionBuildContext context, CancellationToken cancellationToken)
|
|
{
|
|
if (context == null)
|
|
{
|
|
throw new ArgumentNullException(nameof(context));
|
|
}
|
|
|
|
context.Nodes.AddFrame(
|
|
"FixtureFrame",
|
|
frame => frame
|
|
.WithFrameId(FixtureFrameId)
|
|
.WithRole(VisionFrameRoleEnum.World)
|
|
.WithTransform(
|
|
new VisionPose3DDataType
|
|
{
|
|
FrameId = FixtureFrameId,
|
|
Position = s_zeroPosition.ToArrayOf(),
|
|
Orientation = s_identityOrientation.ToArrayOf(),
|
|
Covariance = []
|
|
}));
|
|
AddSensor(context);
|
|
AddPipeline(context);
|
|
await AddOperatorDialogAsync(context, cancellationToken).ConfigureAwait(false);
|
|
AttachRuntimeTargets(context);
|
|
m_logger.CellConfigured(PipelineBrowseName, m_options.InferenceLocation);
|
|
}
|
|
|
|
private void AddSensor(IVisionBuildContext context)
|
|
{
|
|
context.Nodes.AddImageSensor(
|
|
SensorBrowseName,
|
|
sensor => sensor
|
|
.WithSensorId("fixture-camera-01")
|
|
.WithModality(VisionSensorModalityEnum.Area2D)
|
|
.WithRealityKind(VisionRealityKindEnum.Simulated)
|
|
.WithManufacturer("OPC Foundation")
|
|
.WithModel("Fixture PNG camera")
|
|
.WithSerialNumber("FIXTURE-CAM-0001")
|
|
.WithDeviceUri("file://visual-inspection-cell/fixtures")
|
|
.WithFrameId(FixtureFrameId)
|
|
.WithResolution(VisualInspectionMediaProvider.Width, VisualInspectionMediaProvider.Height)
|
|
.WithPixelFormat(VisualInspectionMediaProvider.PixelFormat)
|
|
.WithIntrinsics(
|
|
new VisionIntrinsicsDataType
|
|
{
|
|
Fx = 10.0,
|
|
Fy = 10.0,
|
|
Cx = 400.0,
|
|
Cy = 300.0,
|
|
Skew = 0.0,
|
|
DistortionModel = VisionDistortionModelEnum.None,
|
|
DistortionCoefficients = [],
|
|
Width = VisualInspectionMediaProvider.Width,
|
|
Height = VisualInspectionMediaProvider.Height
|
|
})
|
|
.AddClipEndpoint(
|
|
ClipEndpointBrowseName,
|
|
endpoint => endpoint
|
|
.WithEndpointId("fixture-pngs")
|
|
.WithEndpointUri("opcua-inline://visual-inspection-cell/fixtures")
|
|
.WithClipFormat(VisionClipFormatEnum.Png)
|
|
.WithQuality(100u)
|
|
.WithResolution(VisualInspectionMediaProvider.Width, VisualInspectionMediaProvider.Height)
|
|
.WithInlineDelivery(enabled: true, maxInlineClipSize: 1_048_576u)
|
|
.WithDefaultProfileName("FixturePng"))
|
|
.UseMediaProvider(m_mediaProvider));
|
|
}
|
|
|
|
private void AddPipeline(IVisionBuildContext context)
|
|
{
|
|
NodeId sensorNodeId = FindSensor(context)?.NodeId ?? NodeId.Null;
|
|
(NodeId deployment, NodeId learningJob) = ResolveAIBindings(context);
|
|
bool offServer = m_options.InferenceLocation == VisualInspectionInferenceLocation.EdgeOffServer;
|
|
context.Nodes.AddPipeline(
|
|
PipelineBrowseName,
|
|
pipeline =>
|
|
{
|
|
pipeline.WithPipelineId(PipelineId)
|
|
.WithSensor(sensorNodeId)
|
|
.WithDeployment(deployment)
|
|
.WithLearningJob(learningJob)
|
|
.UseFeedbackSink(m_feedbackSink);
|
|
if (offServer)
|
|
{
|
|
pipeline.UseInferenceProvider(m_inferenceProvider, onServer: false);
|
|
}
|
|
else
|
|
{
|
|
pipeline.UseInferenceProvider(m_inferenceProvider, onServer: true);
|
|
}
|
|
});
|
|
}
|
|
|
|
private void AttachRuntimeTargets(IVisionBuildContext context)
|
|
{
|
|
InferencePipelineState pipeline = FindPipeline(context) ??
|
|
throw new InvalidOperationException(
|
|
"The visual-inspection pipeline was not registered.");
|
|
ImageSensorState sensor = FindSensor(context) ??
|
|
throw new InvalidOperationException(
|
|
"The visual-inspection sensor was not registered.");
|
|
FolderState results = pipeline.Results ??
|
|
throw new InvalidOperationException(
|
|
"The Vision builder did not materialise the pipeline Results folder.");
|
|
NodeId deployment = pipeline.Deployment?.Value ?? NodeId.Null;
|
|
NodeId learningJob = pipeline.LearningJob?.Value ?? NodeId.Null;
|
|
var target = new VisualInspectionTarget(
|
|
context.Manager,
|
|
context.Context,
|
|
context.InstanceNamespaceIndex,
|
|
pipeline.NodeId,
|
|
sensor.NodeId,
|
|
deployment,
|
|
learningJob,
|
|
results);
|
|
m_inferenceProvider.Attach(target);
|
|
m_feedbackSink.Attach(target);
|
|
ClipEndpointState clip = FindChild<ClipEndpointState>(
|
|
sensor.Media?.ClipEndpoints,
|
|
context,
|
|
ClipEndpointBrowseName) ??
|
|
throw new InvalidOperationException("The fixture clip endpoint was not registered.");
|
|
clip.CreateOrReplaceState(context.Context, null).Value = VisionEndpointStateEnum.Ready;
|
|
pipeline.CreateOrReplaceState(context.Context, null).Value = VisionEndpointStateEnum.Ready;
|
|
pipeline.CreateOrReplaceContinuous(context.Context, null).Value = false;
|
|
}
|
|
|
|
private async ValueTask AddOperatorDialogAsync(
|
|
IVisionBuildContext context,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
var dialog = new DialogConditionState(null);
|
|
dialog.Create(
|
|
context.Context,
|
|
NodeId.Null,
|
|
new QualifiedName(OperatorDialogBrowseName, context.InstanceNamespaceIndex),
|
|
new LocalizedText("Operator disposition dialog"),
|
|
true);
|
|
dialog.ReferenceTypeId = Opc.Ua.ReferenceTypeIds.HasComponent;
|
|
dialog.TypeDefinitionId = Opc.Ua.ObjectTypeIds.DialogConditionType;
|
|
dialog.NodeId = new NodeId(OperatorDialogBrowseName, context.InstanceNamespaceIndex);
|
|
context.Context.AssignInstanceChildNodeIds(dialog, dialog.NodeId);
|
|
dialog.CreateOrReplacePrompt(context.Context, null).Value =
|
|
LocalizedText.From(
|
|
"Visual inspection is not decidable. Choose the operator disposition.");
|
|
dialog.CreateOrReplaceResponseOptionSet(context.Context, null).Value =
|
|
[
|
|
LocalizedText.From("AcceptAsOk"),
|
|
LocalizedText.From("AcceptAsNotOk"),
|
|
LocalizedText.From("Reinspect"),
|
|
LocalizedText.From("Stop")
|
|
];
|
|
dialog.CreateOrReplaceDefaultResponse(context.Context, null).Value = 2;
|
|
dialog.CreateOrReplaceOkResponse(context.Context, null).Value = 0;
|
|
dialog.CreateOrReplaceCancelResponse(context.Context, null).Value = 3;
|
|
dialog.Retain!.Value = false;
|
|
dialog.Message!.Value = LocalizedText.From(
|
|
"Human disposition required for a not-decidable inspection.");
|
|
dialog.Severity!.Value = (ushort)EventSeverity.MediumHigh;
|
|
dialog.EventNotifier = EventNotifiers.SubscribeToEvents;
|
|
context.Root.AddChild(dialog);
|
|
await context.Manager.AddPredefinedNodeAsync(dialog, cancellationToken).ConfigureAwait(false);
|
|
m_operatorDialog.Attach(context.Context, dialog);
|
|
OperatorDialogNodeId = dialog.NodeId;
|
|
}
|
|
|
|
private static (NodeId Deployment, NodeId LearningJob) ResolveAIBindings(IVisionBuildContext context)
|
|
{
|
|
if (context.Manager.Server.NodeManager.AsyncNodeManagers.OfType<AINodeManager>().FirstOrDefault() is { } ai)
|
|
{
|
|
return (ai.PrimaryDeploymentId, ai.LearningJobId);
|
|
}
|
|
return (NodeId.Null, NodeId.Null);
|
|
}
|
|
|
|
private static ImageSensorState? FindSensor(IVisionBuildContext context)
|
|
{
|
|
return FindChild<ImageSensorState>(context.Root.Sensors, context, SensorBrowseName);
|
|
}
|
|
|
|
private static InferencePipelineState? FindPipeline(IVisionBuildContext context)
|
|
{
|
|
return FindChild<InferencePipelineState>(context.Root.Pipelines, context, PipelineBrowseName);
|
|
}
|
|
|
|
private static T? FindChild<T>(FolderState? folder, IVisionBuildContext context, string browseName)
|
|
where T : BaseInstanceState
|
|
{
|
|
if (folder == null)
|
|
{
|
|
return null;
|
|
}
|
|
var children = new List<BaseInstanceState>();
|
|
folder.GetChildren(context.Context, children);
|
|
var qualified = new QualifiedName(browseName, context.InstanceNamespaceIndex);
|
|
return children.OfType<T>().FirstOrDefault(child => child.BrowseName == qualified);
|
|
}
|
|
|
|
public NodeId OperatorDialogNodeId { get; private set; } = NodeId.Null;
|
|
|
|
public const string SensorBrowseName = "BracketFixtureCamera";
|
|
public const string ClipEndpointBrowseName = "FixtureImages";
|
|
public const string PipelineBrowseName = "BracketInspectionPipeline";
|
|
public const string PipelineId = "pipe-bracket-inspection";
|
|
public const string FixtureFrameId = "fixture_table";
|
|
public const string OperatorDialogBrowseName = "OperatorDispositionDialog";
|
|
|
|
private static readonly double[] s_zeroPosition = [0.0, 0.0, 0.0];
|
|
private static readonly double[] s_identityOrientation = [0.0, 0.0, 0.0, 1.0];
|
|
|
|
private readonly VisualInspectionCellOptions m_options;
|
|
private readonly VisualInspectionMediaProvider m_mediaProvider;
|
|
private readonly VisualInspectionInferenceProvider m_inferenceProvider;
|
|
private readonly VisualInspectionFeedbackSink m_feedbackSink;
|
|
private readonly OperatorDialogController m_operatorDialog;
|
|
private readonly ILogger<VisualInspectionCell> m_logger;
|
|
}
|
|
|
|
internal static partial class VisualInspectionCellLog
|
|
{
|
|
[LoggerMessage(
|
|
EventId = VisualInspectionCellEventIds.Configurator + 1,
|
|
Level = LogLevel.Information,
|
|
Message = "Configured visual inspection pipeline {PipelineBrowseName} with {InferenceLocation} perception.")]
|
|
public static partial void CellConfigured(
|
|
this ILogger<VisualInspectionCell> logger,
|
|
string pipelineBrowseName,
|
|
VisualInspectionInferenceLocation inferenceLocation);
|
|
}
|
|
}
|