/* ========================================================================
* 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.Concurrent;
using System.Collections.Generic;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Opc.Ua;
using Opc.Ua.Vision.MachineVision;
using Opc.Ua.Vision.Perception;
using Opc.Ua.Vision.Server;
using ControlIds = Opc.Ua.Vision.MachineVision.VisionAutomaticModeStateMachineTypeIds;
namespace MachineVisionSample
{
///
/// One deterministic acquisition engine behind both the released control model and the draft pipeline.
/// Draft commands enter the same control runtime; no command silently selects or resets its mode.
///
internal sealed class DemonstrationVisionProvider : IMachineVisionOperationsProvider, IVisionInferenceProvider
{
///
/// Creates the engine with an injectable clock.
///
public DemonstrationVisionProvider(TimeProvider clock)
{
m_clock = clock;
}
///
/// Connects the two address-space views to this single engine.
///
public void Attach(
IVisionBuildContext context, MachineVisionSystemHandle system, InferencePipelineState pipeline)
{
m_context = context;
m_system = system;
m_pipeline = pipeline;
}
///
public ValueTask ControlAsync(
MachineVisionControlAction action, int cause, string causeDescription, CancellationToken cancellationToken)
{
return Accept(cancellationToken);
}
///
public ValueTask PrepareRecipeAsync(
MachineVisionDocument recipe, bool prepare, CancellationToken cancellationToken)
{
return Accept(cancellationToken);
}
///
public ValueTask ActivateConfigurationAsync(
MachineVisionDocument configuration, CancellationToken cancellationToken)
{
return Accept(cancellationToken);
}
///
public ValueTask ValidateDocumentAsync(
MachineVisionDocument document, CancellationToken cancellationToken)
{
return Accept(cancellationToken);
}
///
public ValueTask StartJobAsync(
MachineVisionJob job, CancellationToken cancellationToken)
{
return Accept(cancellationToken);
}
///
public ValueTask ExecuteStepAsync(
MachineVisionStepRequest request, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
return new ValueTask(
new MachineVisionStepResult(MachineVisionOperationResult.Good, false));
}
///
public async IAsyncEnumerable ExecuteAsync(
MachineVisionJob job,
[EnumeratorCancellation] CancellationToken cancellationToken)
{
try
{
do
{
cancellationToken.ThrowIfCancellationRequested();
long sequence = Interlocked.Increment(ref m_sequence);
string id = "synthetic-" + sequence.ToString(CultureInfo.InvariantCulture);
DateTimeUtc timestamp = m_clock.GetUtcNow().UtcDateTime;
yield return new MachineVisionAcquisition(true, new ResultDataType
{
ResultId = new ResultIdDataType { Id = id },
ResultState = 1,
CreationTime = timestamp,
ResultContent = [Variant.From(sequence)],
EncodingMask = (uint)ResultDataTypeFields.ResultContent
});
await PublishDraftAsync(id, timestamp, cancellationToken).ConfigureAwait(false);
if (job.MeasurementId.Id is string measurement &&
m_pending.TryGetValue(measurement, out TaskCompletionSource? completion))
{
completion.TrySetResult(id);
}
if (job.Continuous)
{
await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
}
}
while (job.Continuous);
}
finally
{
if (job.MeasurementId.Id is string measurement &&
m_pending.TryGetValue(measurement, out TaskCompletionSource? completion) &&
!completion.Task.IsCompleted)
{
completion.TrySetException(ServiceResultException.Create(
cancellationToken.IsCancellationRequested
? StatusCodes.BadRequestCancelledByClient : StatusCodes.BadInternalError,
"The shared inspection ended before both result views were published."));
}
}
}
///
public async ValueTask RunInferenceAsync(
VisionInferenceRunRequest request, CancellationToken cancellationToken)
{
MachineVisionSystemHandle system = RequireSystem();
string correlation = Guid.NewGuid().ToString("N");
var completed = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously);
m_pending[correlation] = completed;
try
{
ServiceResult outcome = await CallAsync(system.AutomaticState, system.AutomaticState.StartSingleJob!,
[
Variant.FromStructure(new MeasIdDataType { Id = correlation }),
Variant.FromStructure(new PartIdDataType()),
Variant.FromStructure(new RecipeIdExternalDataType()),
Variant.FromStructure(new ProductIdDataType()),
Variant.From(ArrayOf.Empty)
], cancellationToken).ConfigureAwait(false);
if (ServiceResult.IsBad(outcome))
{
return new VisionInferenceRunResult(outcome, string.Empty);
}
string result = await completed.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
return new VisionInferenceRunResult(ServiceResult.Good, result);
}
finally
{
m_pending.TryRemove(correlation, out _);
}
}
///
public ValueTask StartContinuousAsync(NodeId pipeline, CancellationToken cancellationToken)
{
MachineVisionSystemHandle system = RequireSystem();
if (system.AutomaticState.GetStateId(system.AutomaticState.CurrentState!.Id!.Value) ==
ControlIds.StateIds.ContinuousExecution)
{
return new ValueTask(ServiceResult.Good);
}
return CallAsync(system.AutomaticState, system.AutomaticState.StartContinuous!,
[
Variant.FromStructure(new MeasIdDataType()),
Variant.FromStructure(new PartIdDataType()),
Variant.FromStructure(new RecipeIdExternalDataType()),
Variant.FromStructure(new ProductIdDataType()),
Variant.From(ArrayOf.Empty)
], cancellationToken);
}
///
public ValueTask StopAsync(NodeId pipeline, CancellationToken cancellationToken)
{
MachineVisionSystemHandle system = RequireSystem();
return CallAsync(system.AutomaticState, system.AutomaticState.Stop!,
[Variant.From(0), Variant.From("Draft Vision stop request")], cancellationToken);
}
private async ValueTask PublishDraftAsync(string id, DateTimeUtc timestamp, CancellationToken ct)
{
IVisionBuildContext context = m_context ??
throw new InvalidOperationException("The sample provider is not attached.");
InferencePipelineState pipeline = m_pipeline!;
FolderState results = pipeline.Results!;
InspectionResultState result = context.Context.CreateInstanceOfInspectionResultType(
results, new QualifiedName(id, context.InstanceNamespaceIndex));
result.ReferenceTypeId = Opc.Ua.ReferenceTypeIds.Organizes;
result.ResultId!.Value = id;
result.CreationTime!.Value = timestamp;
result.CreateOrReplacePipeline(context.Context, null).Value = pipeline.NodeId;
result.CreateOrReplaceSensor(context.Context, null).Value = pipeline.Sensor!.Value;
result.CreateOrReplaceEvaluation(context.Context, null).Value = VisionResultEvaluationEnum.Ok;
result.CreateOrReplaceCharacteristics(context.Context, null).Value = [];
results.AddChild(result);
await context.Manager.AddPredefinedNodeAsync(result, ct).ConfigureAwait(false);
m_draftResults.Enqueue(result);
while (m_draftResults.Count > 32 && m_draftResults.TryDequeue(out InspectionResultState? oldest))
{
results.RemoveChild(oldest);
await context.Manager.DeleteNodeAsync(context.Manager.SystemContext, oldest.NodeId, ct)
.ConfigureAwait(false);
}
}
private async ValueTask CallAsync(
NodeState owner, MethodState method, ArrayOf arguments, CancellationToken ct)
{
IVisionBuildContext context = m_context ??
throw new InvalidOperationException("The sample provider is not attached.");
var errors = new List();
var output = new List();
ServiceResult status = await method.CallAsync(context.Context, owner.NodeId, arguments, errors, output, ct)
.ConfigureAwait(false);
foreach (ServiceResult error in errors)
{
if (ServiceResult.IsBad(error))
{
return error;
}
}
if (ServiceResult.IsGood(status) &&
output.Count != 0 &&
output[^1].TryGetValue(out int domainError) &&
domainError != 0)
{
return ServiceResult.Create(StatusCodes.BadInvalidState, "Domain error {0}.", domainError);
}
return status;
}
private MachineVisionSystemHandle RequireSystem()
{
return m_system ?? throw new InvalidOperationException("The sample provider is not attached.");
}
private static ValueTask Accept(CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
return new ValueTask(MachineVisionOperationResult.Good);
}
private readonly TimeProvider m_clock;
private readonly ConcurrentDictionary> m_pending = new();
private readonly ConcurrentQueue m_draftResults = new();
private IVisionBuildContext? m_context;
private MachineVisionSystemHandle? m_system;
private InferencePipelineState? m_pipeline;
private long m_sequence;
}
}