添加 MX-PD-盘古 项目文件
将 MX-PD-盘古 - new 目录下的所有文件添加到主仓库
This commit is contained in:
@@ -0,0 +1,241 @@
|
||||
using MainShell.Common;
|
||||
using MainShell.Hardware;
|
||||
using MainShell.Log;
|
||||
using MainShell.ParaSetting.Model;
|
||||
using MainShell.Vision;
|
||||
using MaxwellFramework.Core.Interfaces;
|
||||
using MwFramework.Device;
|
||||
using MwFramework.ManagerService;
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Windows;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class DefaultWaferMachineAdapter : IWaferMachineAdapter
|
||||
{
|
||||
private readonly HardwareManager _hardwareManager;
|
||||
private readonly IParamList _parameterList;
|
||||
|
||||
public DefaultWaferMachineAdapter(HardwareManager hardwareManager, IParameterManager parameterManager)
|
||||
{
|
||||
_hardwareManager = hardwareManager ?? throw new ArgumentNullException(nameof(hardwareManager));
|
||||
_parameterList = parameterManager as IParamList;
|
||||
}
|
||||
|
||||
public CameraType GetCameraSource(WaferDiePositionContext context)
|
||||
{
|
||||
return CameraType.TopPositionCamera;
|
||||
}
|
||||
|
||||
public MwCamera GetCamera(WaferDiePositionContext context)
|
||||
{
|
||||
return ResolveCamera(GetCameraSource(context));
|
||||
}
|
||||
|
||||
public Point GetCameraFieldOfView(WaferDiePositionContext context)
|
||||
{
|
||||
DeviceFoundationSetting deviceFoundationSetting = GetDeviceFoundationSetting();
|
||||
CameraFovSettingItem cameraFovSettingItem = GetCameraFovSettingItem(deviceFoundationSetting, GetCameraSource(context));
|
||||
if (cameraFovSettingItem == null || cameraFovSettingItem.FovX <= 0d || cameraFovSettingItem.FovY <= 0d)
|
||||
{
|
||||
LogManager.LogProcessInfo("晶圆定位:DefaultWaferMachineAdapter 正在使用相机视野默认回退值。");
|
||||
return new Point(1.0d, 1.0d);
|
||||
}
|
||||
|
||||
return new Point(cameraFovSettingItem.FovX, cameraFovSettingItem.FovY);
|
||||
}
|
||||
|
||||
public double GetExposureTimeMilliseconds(WaferDiePositionContext context)
|
||||
{
|
||||
MwCamera camera = GetCamera(context);
|
||||
if (camera == null)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
double exposureTime = 0d;
|
||||
DriverLibResult result = camera.GetExposureTime(ref exposureTime);
|
||||
if (result != DriverLibResult.DriverLibNoError || double.IsNaN(exposureTime) || double.IsInfinity(exposureTime) || exposureTime < 0d)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
|
||||
return exposureTime;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
}
|
||||
|
||||
public CameraCaptureOptions CreateSoftTriggerCaptureOptions(WaferDiePositionContext context)
|
||||
{
|
||||
CameraCaptureOptions options = CameraCaptureOptions.CreateSoftTrigger(5000);
|
||||
options.TriggerDelay = 20d;
|
||||
return options;
|
||||
}
|
||||
|
||||
public Point AdjustPointWithinSoftLimit(Point candidatePoint, WaferDiePositionContext context, double offsetMm, out bool adjusted, out string reason)
|
||||
{
|
||||
IAxis axisX = _hardwareManager.Axis_PHS_X1;
|
||||
IAxis axisY = _hardwareManager.Axis_Stage_Y3;
|
||||
double adjustedX = candidatePoint.X;
|
||||
double adjustedY = candidatePoint.Y;
|
||||
bool adjustedXFlag = TryClampByAxisSoftLimit(axisX, adjustedX, offsetMm, out adjustedX, out string adjustedXReason);
|
||||
bool adjustedYFlag = TryClampByAxisSoftLimit(axisY, adjustedY, offsetMm, out adjustedY, out string adjustedYReason);
|
||||
|
||||
adjusted = adjustedXFlag || adjustedYFlag;
|
||||
if (!adjusted)
|
||||
{
|
||||
reason = string.Empty;
|
||||
return candidatePoint;
|
||||
}
|
||||
|
||||
reason = BuildAdjustmentReason(adjustedXReason, adjustedYReason);
|
||||
return new Point(adjustedX, adjustedY);
|
||||
}
|
||||
|
||||
private DeviceFoundationSetting GetDeviceFoundationSetting()
|
||||
{
|
||||
return _parameterList == null ? null : _parameterList.GetParameter<DeviceFoundationSetting>();
|
||||
}
|
||||
|
||||
private MwCamera ResolveCamera(CameraType cameraType)
|
||||
{
|
||||
MwCameraExtend cameraExtend = GetCameraExtend(cameraType);
|
||||
if (cameraExtend == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return cameraExtend.Camera;
|
||||
}
|
||||
|
||||
private MwCameraExtend GetCameraExtend(CameraType cameraType)
|
||||
{
|
||||
return _hardwareManager.GetCamera(cameraType);
|
||||
}
|
||||
|
||||
private static CameraFovSettingItem GetCameraFovSettingItem(DeviceFoundationSetting deviceFoundationSetting, CameraType cameraType)
|
||||
{
|
||||
if (deviceFoundationSetting == null || deviceFoundationSetting.CameraSettingItem == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
deviceFoundationSetting.CameraSettingItem.EnsureDefaultCameras();
|
||||
switch (cameraType)
|
||||
{
|
||||
case CameraType.TopWideCamera:
|
||||
return deviceFoundationSetting.CameraSettingItem.DownCamera;
|
||||
|
||||
case CameraType.MapCamera:
|
||||
return deviceFoundationSetting.CameraSettingItem.MapCamera;
|
||||
|
||||
case CameraType.TopPositionCamera:
|
||||
default:
|
||||
return deviceFoundationSetting.CameraSettingItem.UpCamera;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryClampByAxisSoftLimit(IAxis axis, double candidateValue, double offsetMm, out double adjustedValue, out string reason)
|
||||
{
|
||||
adjustedValue = candidateValue;
|
||||
reason = string.Empty;
|
||||
|
||||
if (axis == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
bool hasNegativeLimit = TryGetAxisLimit(axis, true, out double negativeLimit);
|
||||
bool hasPositiveLimit = TryGetAxisLimit(axis, false, out double positiveLimit);
|
||||
if (!hasNegativeLimit && !hasPositiveLimit)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
double minLimit = hasNegativeLimit && hasPositiveLimit ? Math.Min(negativeLimit, positiveLimit) : negativeLimit;
|
||||
double maxLimit = hasNegativeLimit && hasPositiveLimit ? Math.Max(negativeLimit, positiveLimit) : positiveLimit;
|
||||
double minAllowed = hasNegativeLimit ? minLimit + Math.Max(0d, offsetMm) : double.NegativeInfinity;
|
||||
double maxAllowed = hasPositiveLimit ? maxLimit - Math.Max(0d, offsetMm) : double.PositiveInfinity;
|
||||
|
||||
if (candidateValue < minAllowed)
|
||||
{
|
||||
adjustedValue = minAllowed;
|
||||
reason = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}: clamp to negative soft limit {1:F3} with offset {2:F3}.",
|
||||
axis.Name,
|
||||
adjustedValue,
|
||||
offsetMm);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (candidateValue > maxAllowed)
|
||||
{
|
||||
adjustedValue = maxAllowed;
|
||||
reason = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"{0}: clamp to positive soft limit {1:F3} with offset {2:F3}.",
|
||||
axis.Name,
|
||||
adjustedValue,
|
||||
offsetMm);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryGetAxisLimit(IAxis axis, bool isNegativeLimit, out double limitValue)
|
||||
{
|
||||
limitValue = 0d;
|
||||
if (axis == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
double axisLimit = 0d;
|
||||
MotionErrorCode errorCode = isNegativeLimit ? axis.GetSoftMel(ref axisLimit) : axis.GetSoftPel(ref axisLimit);
|
||||
if (errorCode != MotionErrorCode.NoError)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
limitValue = axisLimit;
|
||||
if (double.IsNaN(limitValue) || double.IsInfinity(limitValue))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string BuildAdjustmentReason(string adjustedXReason, string adjustedYReason)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(adjustedXReason))
|
||||
{
|
||||
return adjustedYReason ?? string.Empty;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(adjustedYReason))
|
||||
{
|
||||
return adjustedXReason;
|
||||
}
|
||||
|
||||
return adjustedXReason + " | " + adjustedYReason;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
using MW.WorkFlow;
|
||||
using MainShell.Common;
|
||||
using MainShell.Log;
|
||||
using MainShell.ProcessResult;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class DiePositionActivity : ActivityAbstractBase
|
||||
{
|
||||
private readonly DiePositionService _diePositionService;
|
||||
|
||||
public DiePositionActivity(string name, DiePositionService diePositionService)
|
||||
: base(name)
|
||||
{
|
||||
_diePositionService = diePositionService ?? throw new ArgumentNullException(nameof(diePositionService));
|
||||
}
|
||||
|
||||
protected override async Task<ActivityResult> OnExecuteAsync(WorkflowContext context, ActivityControl activityControl)
|
||||
{
|
||||
await _diePositionService.ExecuteAsync(context, activityControl).ConfigureAwait(false);
|
||||
|
||||
DiePositionProcessResult result = context.GetData<DiePositionProcessResult>(WorkflowContextKeys.DiePositionResult);
|
||||
if (result == null || !result.IsSuccess)
|
||||
{
|
||||
MessageKey failureMessageKey = result != null && result.ErrorMessageKey != MessageKey.None
|
||||
? result.ErrorMessageKey
|
||||
: MessageKey.ProcessDiePositionFailedWithReason;
|
||||
|
||||
object[] failureMessageArguments = result != null
|
||||
? ConvertToObjectArray(result.ErrorMessageArguments)
|
||||
: new object[] { LanguageResourceHelper.GetString(MessageKey.CommonUnknownError) };
|
||||
|
||||
string errorMessage = result != null && !string.IsNullOrWhiteSpace(result.ErrorMessage)
|
||||
? result.ErrorMessage
|
||||
: LanguageResourceHelper.Format(failureMessageKey, failureMessageArguments);
|
||||
|
||||
LogManager.LogProcessError(string.Format("晶圆定位活动执行失败:{0}", errorMessage));
|
||||
return Fail(context, failureMessageKey, failureMessageArguments);
|
||||
}
|
||||
|
||||
return ActivityResult.Success;
|
||||
}
|
||||
|
||||
private static object[] ConvertToObjectArray(string[] arguments)
|
||||
{
|
||||
return arguments == null ? Array.Empty<object>() : arguments.Cast<object>().ToArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,983 @@
|
||||
using MainShell.Common;
|
||||
using MainShell.Filewritable;
|
||||
using MainShell.Log;
|
||||
using MainShell.Models;
|
||||
using MainShell.Models.Wafer;
|
||||
using MainShell.Parameter;
|
||||
using MainShell.ProcessResult;
|
||||
using MainShell.ParaSetting.Model;
|
||||
using MainShell.Recipe.Models;
|
||||
using MainShell.Vision;
|
||||
using MaxwellFramework.Core.Attributes;
|
||||
using MXJM.FileWritable;
|
||||
using MW.WorkFlow;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.ExceptionServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
|
||||
using Jm1RecheckDiePoint = JM1.JM1Params.RecheckDiePoint;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class DiePositionService
|
||||
{
|
||||
private sealed class PipelineFailureCoordinator
|
||||
{
|
||||
private readonly object _syncRoot = new object();
|
||||
private readonly CancellationTokenSource _pipelineCancellationTokenSource;
|
||||
private readonly ChannelWriter<WaferCaptureFrame> _frameWriter;
|
||||
private ExceptionDispatchInfo _failureDispatchInfo;
|
||||
|
||||
public PipelineFailureCoordinator(CancellationTokenSource pipelineCancellationTokenSource, ChannelWriter<WaferCaptureFrame> frameWriter)
|
||||
{
|
||||
_pipelineCancellationTokenSource = pipelineCancellationTokenSource ?? throw new ArgumentNullException(nameof(pipelineCancellationTokenSource));
|
||||
_frameWriter = frameWriter ?? throw new ArgumentNullException(nameof(frameWriter));
|
||||
}
|
||||
|
||||
public bool HasFailure
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _failureDispatchInfo != null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void TrySet(Exception exception)
|
||||
{
|
||||
if (exception == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(exception));
|
||||
}
|
||||
|
||||
bool shouldCancelPipeline = false;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_failureDispatchInfo == null)
|
||||
{
|
||||
_failureDispatchInfo = ExceptionDispatchInfo.Capture(exception);
|
||||
shouldCancelPipeline = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!shouldCancelPipeline)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_frameWriter.TryComplete(exception);
|
||||
if (!_pipelineCancellationTokenSource.IsCancellationRequested)
|
||||
{
|
||||
_pipelineCancellationTokenSource.Cancel();
|
||||
}
|
||||
}
|
||||
|
||||
public void ThrowIfFailed()
|
||||
{
|
||||
ExceptionDispatchInfo failureDispatchInfo = null;
|
||||
lock (_syncRoot)
|
||||
{
|
||||
failureDispatchInfo = _failureDispatchInfo;
|
||||
}
|
||||
|
||||
if (failureDispatchInfo != null)
|
||||
{
|
||||
failureDispatchInfo.Throw();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const int DefaultRecognitionConsumerCount = 2;
|
||||
private const int DefaultFrameChannelCapacity = 8;
|
||||
private const double DefaultMapAngleThreshold = 3.0d;
|
||||
private const double DefaultMapRotateAngle = 0.0d;
|
||||
private const double DefaultMapDistanceThreshold = 0.01d;
|
||||
|
||||
private readonly IWaferScanPlanner _waferScanPlanner;
|
||||
private readonly IWaferMachineAdapter _waferMachineAdapter;
|
||||
private readonly IWaferCaptureExecutor _waferCaptureExecutor;
|
||||
private readonly IWaferDieRecognizer _waferDieRecognizer;
|
||||
private readonly ICommonVisionAlgorithmService _commonVisionAlgorithmService;
|
||||
private readonly IWaferRecognitionResultCollector _waferRecognitionResultCollector;
|
||||
private readonly IWaferDiePositionResultService _waferDiePositionResultService;
|
||||
private readonly GlobalParameterContext _globalParameterContext;
|
||||
private readonly FileWriteQueue _fileWriteQueue;
|
||||
|
||||
public DiePositionService(
|
||||
IWaferScanPlanner waferScanPlanner,
|
||||
IWaferMachineAdapter waferMachineAdapter,
|
||||
IWaferCaptureExecutor waferCaptureExecutor,
|
||||
IWaferDieRecognizer waferDieRecognizer,
|
||||
ICommonVisionAlgorithmService commonVisionAlgorithmService,
|
||||
IWaferRecognitionResultCollector waferRecognitionResultCollector,
|
||||
IWaferDiePositionResultService waferDiePositionResultService,
|
||||
GlobalParameterContext globalParameterContext,
|
||||
FileWriteQueue fileWriteQueue)
|
||||
{
|
||||
_waferScanPlanner = waferScanPlanner ?? throw new ArgumentNullException(nameof(waferScanPlanner));
|
||||
_waferMachineAdapter = waferMachineAdapter ?? throw new ArgumentNullException(nameof(waferMachineAdapter));
|
||||
_waferCaptureExecutor = waferCaptureExecutor ?? throw new ArgumentNullException(nameof(waferCaptureExecutor));
|
||||
_waferDieRecognizer = waferDieRecognizer ?? throw new ArgumentNullException(nameof(waferDieRecognizer));
|
||||
_commonVisionAlgorithmService = commonVisionAlgorithmService ?? throw new ArgumentNullException(nameof(commonVisionAlgorithmService));
|
||||
_waferRecognitionResultCollector = waferRecognitionResultCollector ?? throw new ArgumentNullException(nameof(waferRecognitionResultCollector));
|
||||
_waferDiePositionResultService = waferDiePositionResultService ?? throw new ArgumentNullException(nameof(waferDiePositionResultService));
|
||||
_globalParameterContext = globalParameterContext ?? throw new ArgumentNullException(nameof(globalParameterContext));
|
||||
_fileWriteQueue = fileWriteQueue ?? throw new ArgumentNullException(nameof(fileWriteQueue));
|
||||
}
|
||||
|
||||
public async Task ExecuteAsync(WorkflowContext context, ActivityControl activityControl)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (activityControl == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(activityControl));
|
||||
}
|
||||
|
||||
RecipeManager recipeManager = context.GetData<RecipeManager>(WorkflowContextKeys.RecipeManager);
|
||||
ProcessResultManager processResultManager = context.GetData<ProcessResultManager>(WorkflowContextKeys.ProcessResultManager);
|
||||
DiePositionProcessResult processResult = InitializeProcessResult(processResultManager);
|
||||
|
||||
try
|
||||
{
|
||||
await CheckCancellationAndPauseAsync(activityControl).ConfigureAwait(false);
|
||||
_waferRecognitionResultCollector.Reset();
|
||||
|
||||
WaferDiePositionContext runtimeContext = BuildRuntimeContext(recipeManager, context, activityControl);
|
||||
runtimeContext.MapAngleThreshold = ResolveMapAngleThreshold(_globalParameterContext);
|
||||
runtimeContext.MapRotateAngle = ResolveMapRotateAngle(processResultManager);
|
||||
runtimeContext.MapDistanceThreshold = ResolveDistanceThreshold();
|
||||
WaferScanPlan scanPlan = CreateScanPlan(runtimeContext);
|
||||
runtimeContext.ScanPlan = scanPlan;
|
||||
runtimeContext.ExecutionSummary = await ExecuteScanWithRecognitionPipelineAsync(runtimeContext).ConfigureAwait(false);
|
||||
WaferRecognitionSnapshot recognitionSnapshot = CollectSnapshot();
|
||||
_waferDiePositionResultService.ApplySnapshot(runtimeContext, recognitionSnapshot);
|
||||
await GenerateWaferMapAsync(runtimeContext, recognitionSnapshot).ConfigureAwait(false);
|
||||
|
||||
PopulateProcessResult(processResult, runtimeContext, scanPlan);
|
||||
_waferDiePositionResultService.WriteProcessResult(runtimeContext, processResult);
|
||||
processResult.IsSuccess = true;
|
||||
processResult.ErrorMessage = null;
|
||||
processResult.ErrorMessageKey = MessageKey.None;
|
||||
processResult.ErrorMessageArguments = Array.Empty<string>();
|
||||
|
||||
context.SetData(WorkflowContextKeys.DiePositionContext, runtimeContext);
|
||||
context.SetData(WorkflowContextKeys.DiePositionScanPlan, scanPlan);
|
||||
|
||||
LogManager.LogProcessInfo(
|
||||
string.Format(
|
||||
"晶圆定位:扫描执行完成。配方={0},行数={1},列数={2},扫描点数={3},采图帧数={4},识别成功数={5},识别失败数={6}。",
|
||||
processResult.RecipeName,
|
||||
processResult.RowCount,
|
||||
processResult.ColumnCount,
|
||||
processResult.ScanPointCount,
|
||||
runtimeContext.ExecutionSummary == null ? 0 : runtimeContext.ExecutionSummary.ProducedFrameCount,
|
||||
runtimeContext.ExecutionSummary == null ? 0 : runtimeContext.ExecutionSummary.RecognitionSuccessCount,
|
||||
runtimeContext.ExecutionSummary == null ? 0 : runtimeContext.ExecutionSummary.RecognitionFailureCount));
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
processResult.IsSuccess = false;
|
||||
processResult.ErrorMessage = null;
|
||||
processResult.ErrorMessageKey = MessageKey.None;
|
||||
processResult.ErrorMessageArguments = Array.Empty<string>();
|
||||
LogManager.LogProcessInfo("晶圆定位:流程已取消。");
|
||||
throw;
|
||||
}
|
||||
catch (LocalizedProcessException ex)
|
||||
{
|
||||
processResult.IsSuccess = false;
|
||||
processResult.ErrorMessageKey = ex.FailureMessageKey;
|
||||
processResult.ErrorMessageArguments = ex.FailureMessageArguments;
|
||||
processResult.ErrorMessage = ex.Message;
|
||||
LogManager.LogProcessError(string.Format("晶圆定位:{0}", ex.Message));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
processResult.IsSuccess = false;
|
||||
processResult.ErrorMessageKey = MessageKey.ProcessDiePositionFailedWithReason;
|
||||
processResult.ErrorMessageArguments = new string[] { ex.Message ?? string.Empty };
|
||||
processResult.ErrorMessage = LanguageResourceHelper.Format(
|
||||
processResult.ErrorMessageKey,
|
||||
LocalizedProcessException.ConvertArguments(processResult.ErrorMessageArguments));
|
||||
LogManager.LogProcessError(string.Format("晶圆定位:{0}", processResult.ErrorMessage));
|
||||
}
|
||||
finally
|
||||
{
|
||||
WriteResultToContext(context, processResultManager, processResult);
|
||||
}
|
||||
}
|
||||
|
||||
private static WaferDiePositionContext BuildRuntimeContext(RecipeManager recipeManager, WorkflowContext context, ActivityControl activityControl)
|
||||
{
|
||||
WaferRecipe waferRecipe = GetCurrentWaferRecipe(recipeManager);
|
||||
WaferInfo waferInfo = GetWaferInfo(waferRecipe);
|
||||
|
||||
WaferDiePositionContext runtimeContext = new WaferDiePositionContext();
|
||||
runtimeContext.WorkflowName = context.GetData<string>(WorkflowContextKeys.WorkflowName);
|
||||
runtimeContext.RecipeName = waferRecipe.RecipeName;
|
||||
runtimeContext.WaferRecipe = waferRecipe;
|
||||
runtimeContext.ScanSettings = waferRecipe.ScanSettings ?? new WaferScanSettings();
|
||||
runtimeContext.RowCount = waferInfo.WaferRowNum;
|
||||
runtimeContext.ColumnCount = waferInfo.WaferColNum;
|
||||
runtimeContext.PitchX = waferInfo.DiePitchX;
|
||||
runtimeContext.PitchY = waferInfo.DiePitchY;
|
||||
runtimeContext.CancellationToken = activityControl.CancellationToken;
|
||||
return runtimeContext;
|
||||
}
|
||||
|
||||
private WaferScanPlan CreateScanPlan(WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
return _waferScanPlanner.CreatePlan(runtimeContext);
|
||||
}
|
||||
|
||||
private async Task<WaferScanExecutionSummary> ExecuteScanWithRecognitionPipelineAsync(WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
if (runtimeContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(runtimeContext));
|
||||
}
|
||||
|
||||
CancellationToken originalCancellationToken = runtimeContext.CancellationToken;
|
||||
CancellationTokenSource pipelineCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(originalCancellationToken);
|
||||
try
|
||||
{
|
||||
runtimeContext.CancellationToken = pipelineCancellationTokenSource.Token;
|
||||
EnsureFrameResults(runtimeContext);
|
||||
|
||||
int consumerCount = ResolveRecognitionConsumerCount(runtimeContext);
|
||||
Channel<WaferCaptureFrame> frameChannel = CreateFrameChannel(runtimeContext, consumerCount);
|
||||
PipelineFailureCoordinator failureCoordinator = new PipelineFailureCoordinator(pipelineCancellationTokenSource, frameChannel.Writer);
|
||||
List<WaferRecognitionFrameResult> frameResults = new List<WaferRecognitionFrameResult>();
|
||||
object frameResultsSyncRoot = new object();
|
||||
List<Task> consumerTasks = StartRecognitionConsumers(frameChannel.Reader, runtimeContext, frameResults, frameResultsSyncRoot, consumerCount, failureCoordinator);
|
||||
|
||||
LogManager.LogProcessInfo($"晶圆定位:启动流式识别处理链。消费者数量={consumerCount}。");
|
||||
|
||||
WaferScanExecutionSummary summary = null;
|
||||
try
|
||||
{
|
||||
summary = await _waferCaptureExecutor.ExecuteAsync(runtimeContext, frameChannel.Writer, runtimeContext.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (failureCoordinator.HasFailure)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failureCoordinator.TrySet(ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
frameChannel.Writer.TryComplete();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
await Task.WhenAll(consumerTasks).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) when (failureCoordinator.HasFailure)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failureCoordinator.TrySet(ex);
|
||||
}
|
||||
|
||||
failureCoordinator.ThrowIfFailed();
|
||||
runtimeContext.CancellationToken.ThrowIfCancellationRequested();
|
||||
ApplyFrameResults(runtimeContext, frameResults, summary);
|
||||
return summary;
|
||||
}
|
||||
finally
|
||||
{
|
||||
runtimeContext.CancellationToken = originalCancellationToken;
|
||||
pipelineCancellationTokenSource.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static Channel<WaferCaptureFrame> CreateFrameChannel(WaferDiePositionContext runtimeContext, int consumerCount)
|
||||
{
|
||||
if (runtimeContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(runtimeContext));
|
||||
}
|
||||
|
||||
int capacity = ResolveFrameChannelCapacity(runtimeContext, consumerCount);
|
||||
BoundedChannelOptions options = new BoundedChannelOptions(capacity);
|
||||
options.SingleWriter = true;
|
||||
options.SingleReader = false;
|
||||
options.AllowSynchronousContinuations = false;
|
||||
options.FullMode = BoundedChannelFullMode.Wait;
|
||||
return Channel.CreateBounded<WaferCaptureFrame>(options);
|
||||
}
|
||||
|
||||
private List<Task> StartRecognitionConsumers(
|
||||
ChannelReader<WaferCaptureFrame> frameReader,
|
||||
WaferDiePositionContext runtimeContext,
|
||||
List<WaferRecognitionFrameResult> frameResults,
|
||||
object frameResultsSyncRoot,
|
||||
int consumerCount,
|
||||
PipelineFailureCoordinator failureCoordinator)
|
||||
{
|
||||
if (frameReader == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(frameReader));
|
||||
}
|
||||
|
||||
if (runtimeContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(runtimeContext));
|
||||
}
|
||||
|
||||
List<Task> consumerTasks = new List<Task>();
|
||||
for (int consumerIndex = 0; consumerIndex < consumerCount; consumerIndex++)
|
||||
{
|
||||
consumerTasks.Add(ProcessQueuedFramesAsync(frameReader, runtimeContext, frameResults, frameResultsSyncRoot, consumerIndex + 1, failureCoordinator));
|
||||
}
|
||||
|
||||
return consumerTasks;
|
||||
}
|
||||
|
||||
private void ApplyFrameResults(
|
||||
WaferDiePositionContext runtimeContext,
|
||||
List<WaferRecognitionFrameResult> frameResults,
|
||||
WaferScanExecutionSummary summary)
|
||||
{
|
||||
if (runtimeContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(runtimeContext));
|
||||
}
|
||||
|
||||
if (frameResults == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(frameResults));
|
||||
}
|
||||
|
||||
List<WaferRecognitionFrameResult> orderedResults = frameResults
|
||||
.OrderBy(result => result == null ? long.MaxValue : result.SequenceId)
|
||||
.ToList();
|
||||
|
||||
runtimeContext.FrameResults.Clear();
|
||||
foreach (WaferRecognitionFrameResult frameResult in orderedResults)
|
||||
{
|
||||
runtimeContext.FrameResults.Add(frameResult);
|
||||
}
|
||||
|
||||
if (summary != null)
|
||||
{
|
||||
summary.ConsumedFrameCount = orderedResults.Count;
|
||||
summary.RecognitionSuccessCount = orderedResults.Count(result => result != null && result.IsSuccess);
|
||||
summary.RecognitionFailureCount = orderedResults.Count(result => result == null || !result.IsSuccess);
|
||||
}
|
||||
|
||||
LogManager.LogProcessInfo($"晶圆定位:识别处理链完成。结果数={orderedResults.Count},成功数={(summary == null ? 0 : summary.RecognitionSuccessCount)},失败数={(summary == null ? 0 : summary.RecognitionFailureCount)}。");
|
||||
}
|
||||
|
||||
private async Task ProcessQueuedFramesAsync(
|
||||
ChannelReader<WaferCaptureFrame> frameReader,
|
||||
WaferDiePositionContext runtimeContext,
|
||||
List<WaferRecognitionFrameResult> frameResults,
|
||||
object frameResultsSyncRoot,
|
||||
int consumerId,
|
||||
PipelineFailureCoordinator failureCoordinator)
|
||||
{
|
||||
if (frameReader == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(frameReader));
|
||||
}
|
||||
|
||||
if (runtimeContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(runtimeContext));
|
||||
}
|
||||
|
||||
if (frameResults == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(frameResults));
|
||||
}
|
||||
|
||||
if (frameResultsSyncRoot == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(frameResultsSyncRoot));
|
||||
}
|
||||
|
||||
if (failureCoordinator == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(failureCoordinator));
|
||||
}
|
||||
|
||||
LogManager.LogProcessInfo($"晶圆定位:识别消费者 {consumerId} 已启动。");
|
||||
|
||||
try
|
||||
{
|
||||
while (await frameReader.WaitToReadAsync(runtimeContext.CancellationToken).ConfigureAwait(false))
|
||||
{
|
||||
WaferCaptureFrame captureFrame = null;
|
||||
while (frameReader.TryRead(out captureFrame))
|
||||
{
|
||||
runtimeContext.CancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
WaferRecognitionFrameResult frameResult = await RecognizeFrameAsync(captureFrame, runtimeContext).ConfigureAwait(false);
|
||||
_waferRecognitionResultCollector.Add(frameResult);
|
||||
|
||||
lock (frameResultsSyncRoot)
|
||||
{
|
||||
frameResults.Add(frameResult);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (failureCoordinator.HasFailure)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failureCoordinator.TrySet(ex);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
LogManager.LogProcessInfo($"晶圆定位:识别消费者 {consumerId} 已停止。");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<WaferRecognitionFrameResult> RecognizeFrameAsync(WaferCaptureFrame captureFrame, WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
if (captureFrame == null)
|
||||
{
|
||||
throw new InvalidOperationException("DiePosition: capture frame is null.");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return await _waferDieRecognizer.RecognizeAsync(captureFrame, runtimeContext, runtimeContext.CancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
LogManager.LogProcessError($"晶圆定位:帧识别失败。序号={captureFrame.SequenceId},详情={ex.Message}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
private WaferRecognitionSnapshot CollectSnapshot()
|
||||
{
|
||||
return _waferRecognitionResultCollector.CreateSnapshot();
|
||||
}
|
||||
|
||||
private async Task GenerateWaferMapAsync(WaferDiePositionContext runtimeContext, WaferRecognitionSnapshot recognitionSnapshot)
|
||||
{
|
||||
if (runtimeContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(runtimeContext));
|
||||
}
|
||||
|
||||
if (recognitionSnapshot == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(recognitionSnapshot));
|
||||
}
|
||||
|
||||
InitializeMapResults(runtimeContext);
|
||||
QueueWaferMapFile(recognitionSnapshot);
|
||||
|
||||
await ClearWaferMapAsync(runtimeContext.CancellationToken).ConfigureAwait(false);
|
||||
|
||||
VisionProcessResult generateMapResult = await _commonVisionAlgorithmService.GenWaferMapXAsync(
|
||||
ResolveWaferMapDirectory(),
|
||||
recognitionSnapshot.OverlapRows,
|
||||
recognitionSnapshot.OverlapColumns,
|
||||
recognitionSnapshot.OverlapAngles,
|
||||
recognitionSnapshot.RowsReal,
|
||||
recognitionSnapshot.ColumnsReal,
|
||||
recognitionSnapshot.Angles,
|
||||
DefaultMapAngleThreshold,
|
||||
ResolveRowDistance(runtimeContext),
|
||||
ResolveColumnDistance(runtimeContext),
|
||||
runtimeContext.RowCount,
|
||||
runtimeContext.ColumnCount,
|
||||
0d,
|
||||
0d,
|
||||
runtimeContext.MapDistanceThreshold,
|
||||
runtimeContext.MapAngleThreshold,
|
||||
runtimeContext.MapRotateAngle,
|
||||
runtimeContext.CancellationToken).ConfigureAwait(false);
|
||||
EnsureVisionSucceeded(generateMapResult, "生成晶圆图");
|
||||
|
||||
VisionProcessResult<(RecheckDieMap UpMap, List<Jm1RecheckDiePoint> PatchPos, List<Jm1RecheckDiePoint> ExceptPos)> getMapResult = await _commonVisionAlgorithmService.GetMapAsync(runtimeContext.CancellationToken).ConfigureAwait(false);
|
||||
EnsureVisionSucceeded(getMapResult, "读取晶圆图");
|
||||
|
||||
runtimeContext.MapDies = ConvertMapToDies(getMapResult.Data.UpMap, runtimeContext.RowCount, runtimeContext.ColumnCount);
|
||||
runtimeContext.PatchDies = ConvertPatchDies(getMapResult.Data.PatchPos, runtimeContext.RowCount, runtimeContext.ColumnCount);
|
||||
runtimeContext.ExceptDies = ConvertExceptDies(getMapResult.Data.ExceptPos, runtimeContext.RowCount, runtimeContext.ColumnCount);
|
||||
|
||||
LogManager.LogProcessInfo($"晶圆定位:晶圆图生成完成。补点数={runtimeContext.PatchDies.Count},异常点数={runtimeContext.ExceptDies.Count}。");
|
||||
}
|
||||
|
||||
private static void InitializeMapResults(WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
runtimeContext.MapDies = new Die[Math.Max(0, runtimeContext.RowCount), Math.Max(0, runtimeContext.ColumnCount)];
|
||||
runtimeContext.PatchDies = new List<Die>();
|
||||
runtimeContext.ExceptDies = new List<Die>();
|
||||
}
|
||||
|
||||
private void QueueWaferMapFile(WaferRecognitionSnapshot recognitionSnapshot)
|
||||
{
|
||||
if (recognitionSnapshot == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(recognitionSnapshot));
|
||||
}
|
||||
|
||||
WaferMapFile waferMapFile = CreateWaferMapFile(recognitionSnapshot);
|
||||
bool enqueueSucceeded = _fileWriteQueue.Enqueue(waferMapFile);
|
||||
if (!enqueueSucceeded)
|
||||
{
|
||||
throw new InvalidOperationException("晶圆定位:识别点文件入队保存失败。");
|
||||
}
|
||||
}
|
||||
|
||||
private async Task ClearWaferMapAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
int[] clearMapFlags = new int[] { 1, 2, 3 };
|
||||
for (int index = 0; index < clearMapFlags.Length; index++)
|
||||
{
|
||||
int clearMapFlag = clearMapFlags[index];
|
||||
VisionProcessResult clearMapResult = await _commonVisionAlgorithmService.ClearMapAsync(clearMapFlag, cancellationToken).ConfigureAwait(false);
|
||||
EnsureVisionSucceeded(clearMapResult, $"清空晶圆图,标志={clearMapFlag}");
|
||||
}
|
||||
}
|
||||
|
||||
private static WaferMapFile CreateWaferMapFile(WaferRecognitionSnapshot recognitionSnapshot)
|
||||
{
|
||||
WaferMapFile waferMapFile = new WaferMapFile();
|
||||
waferMapFile.UserData = Tuple.Create(
|
||||
recognitionSnapshot.RowsReal ?? Array.Empty<double>(),
|
||||
recognitionSnapshot.ColumnsReal ?? Array.Empty<double>(),
|
||||
recognitionSnapshot.Angles ?? Array.Empty<double>(),
|
||||
recognitionSnapshot.OverlapRows ?? Array.Empty<double>(),
|
||||
recognitionSnapshot.OverlapColumns ?? Array.Empty<double>(),
|
||||
recognitionSnapshot.OverlapAngles ?? Array.Empty<double>());
|
||||
return waferMapFile;
|
||||
}
|
||||
|
||||
private static string ResolveWaferMapDirectory()
|
||||
{
|
||||
return Path.GetDirectoryName(Paths.WaferFileNames["Row"]);
|
||||
}
|
||||
|
||||
private static double ResolveRowDistance(WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
if (runtimeContext.WaferRecipe != null &&
|
||||
runtimeContext.WaferRecipe.WaferInfo != null &&
|
||||
runtimeContext.WaferRecipe.WaferInfo.DiePitchY > 0d)
|
||||
{
|
||||
return runtimeContext.WaferRecipe.WaferInfo.DiePitchY;
|
||||
}
|
||||
|
||||
return runtimeContext.PitchY;
|
||||
}
|
||||
|
||||
private static double ResolveColumnDistance(WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
if (runtimeContext.WaferRecipe != null &&
|
||||
runtimeContext.WaferRecipe.WaferInfo != null &&
|
||||
runtimeContext.WaferRecipe.WaferInfo.DiePitchX > 0d)
|
||||
{
|
||||
return runtimeContext.WaferRecipe.WaferInfo.DiePitchX;
|
||||
}
|
||||
|
||||
return runtimeContext.PitchX;
|
||||
}
|
||||
|
||||
private static double ResolveDistanceThreshold()
|
||||
{
|
||||
return DefaultMapDistanceThreshold;
|
||||
}
|
||||
|
||||
private static double ResolveMapAngleThreshold(GlobalParameterContext globalParameterContext)
|
||||
{
|
||||
if (globalParameterContext != null &&
|
||||
globalParameterContext.RunSetting != null &&
|
||||
globalParameterContext.RunSetting.DieRecognizeProcessParameter != null)
|
||||
{
|
||||
return globalParameterContext.RunSetting.DieRecognizeProcessParameter.DieAngleThreshold;
|
||||
}
|
||||
|
||||
return DefaultMapAngleThreshold;
|
||||
}
|
||||
|
||||
private static double ResolveMapRotateAngle(ProcessResultManager processResultManager)
|
||||
{
|
||||
if (processResultManager == null || processResultManager.SubstratePositionResult == null)
|
||||
{
|
||||
return DefaultMapRotateAngle;
|
||||
}
|
||||
|
||||
return processResultManager.SubstratePositionResult.SubstrateAngle * -1d;
|
||||
}
|
||||
|
||||
private static void EnsureVisionSucceeded(VisionProcessResult result, string operationName)
|
||||
{
|
||||
if (result != null && result.Succeeded)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
string message = result == null || string.IsNullOrWhiteSpace(result.Message)
|
||||
? $"晶圆定位:{operationName}失败。"
|
||||
: result.Message;
|
||||
throw new InvalidOperationException(message, result == null ? null : result.Exception);
|
||||
}
|
||||
|
||||
private static Die[,] ConvertMapToDies(RecheckDieMap map, int rowCount, int columnCount)
|
||||
{
|
||||
Die[,] mapDies = new Die[Math.Max(0, rowCount), Math.Max(0, columnCount)];
|
||||
if (map == null)
|
||||
{
|
||||
return mapDies;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<int, Dictionary<int, Jm1RecheckDiePoint>> rowPair in map)
|
||||
{
|
||||
if (rowPair.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (KeyValuePair<int, Jm1RecheckDiePoint> columnPair in rowPair.Value)
|
||||
{
|
||||
int resolvedRowIndex;
|
||||
int resolvedColumnIndex;
|
||||
if (!TryResolveArrayIndex(rowPair.Key, rowCount, out resolvedRowIndex) ||
|
||||
!TryResolveArrayIndex(columnPair.Key, columnCount, out resolvedColumnIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
mapDies[resolvedRowIndex, resolvedColumnIndex] = CreateMapDie(columnPair.Value, resolvedRowIndex + 1, resolvedColumnIndex + 1);
|
||||
}
|
||||
}
|
||||
|
||||
return mapDies;
|
||||
}
|
||||
|
||||
private static List<Die> ConvertPatchDies(List<Jm1RecheckDiePoint> patchPoints, int rowCount, int columnCount)
|
||||
{
|
||||
return ConvertPointListToDies(patchPoints, rowCount, columnCount, DieStatus.Normal);
|
||||
}
|
||||
|
||||
private static List<Die> ConvertExceptDies(List<Jm1RecheckDiePoint> exceptPoints, int rowCount, int columnCount)
|
||||
{
|
||||
return ConvertPointListToDies(exceptPoints, rowCount, columnCount, DieStatus.Ng);
|
||||
}
|
||||
|
||||
private static List<Die> ConvertPointListToDies(List<Jm1RecheckDiePoint> points, int rowCount, int columnCount, DieStatus defaultStatus)
|
||||
{
|
||||
List<Die> dies = new List<Die>();
|
||||
if (points == null)
|
||||
{
|
||||
return dies;
|
||||
}
|
||||
|
||||
foreach (Jm1RecheckDiePoint point in points)
|
||||
{
|
||||
if (point == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
int resolvedRowIndex;
|
||||
int resolvedColumnIndex;
|
||||
if (!TryResolveArrayIndex(point.rowIndex, rowCount, out resolvedRowIndex) ||
|
||||
!TryResolveArrayIndex(point.colIndex, columnCount, out resolvedColumnIndex))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
dies.Add(CreateListDie(point, resolvedRowIndex + 1, resolvedColumnIndex + 1, defaultStatus));
|
||||
}
|
||||
|
||||
return dies;
|
||||
}
|
||||
|
||||
private static Die CreateMapDie(Jm1RecheckDiePoint point, int row, int column)
|
||||
{
|
||||
Die die = new Die();
|
||||
die.Row = row;
|
||||
die.Column = column;
|
||||
if (point != null)
|
||||
{
|
||||
die.X = point.X;
|
||||
die.Y = point.Y;
|
||||
die.Angle = point.Angle;
|
||||
die.Status = point.State ? DieStatus.Normal : DieStatus.Ng;
|
||||
}
|
||||
return die;
|
||||
}
|
||||
|
||||
private static Die CreateListDie(Jm1RecheckDiePoint point, int row, int column, DieStatus defaultStatus)
|
||||
{
|
||||
Die die = new Die();
|
||||
die.Row = row;
|
||||
die.Column = column;
|
||||
die.X = point.X;
|
||||
die.Y = point.Y;
|
||||
die.Angle = point.Angle;
|
||||
die.Status = defaultStatus;
|
||||
return die;
|
||||
}
|
||||
|
||||
private static bool TryResolveArrayIndex(int rawIndex, int count, out int resolvedIndex)
|
||||
{
|
||||
resolvedIndex = -1;
|
||||
if (count <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (rawIndex >= 1 && rawIndex <= count)
|
||||
{
|
||||
resolvedIndex = rawIndex - 1;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (rawIndex >= 0 && rawIndex < count)
|
||||
{
|
||||
resolvedIndex = rawIndex;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static int ResolveRecognitionConsumerCount(WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
if (runtimeContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(runtimeContext));
|
||||
}
|
||||
|
||||
int configuredConsumerCount = runtimeContext.ScanSettings == null
|
||||
? DefaultRecognitionConsumerCount
|
||||
: runtimeContext.ScanSettings.ConsumerCount;
|
||||
int safeConsumerCount = Math.Max(1, configuredConsumerCount);
|
||||
int plannedFrameCount = runtimeContext.ScanPlan == null ? 0 : runtimeContext.ScanPlan.PathPointCount;
|
||||
if (plannedFrameCount <= 0)
|
||||
{
|
||||
return safeConsumerCount;
|
||||
}
|
||||
|
||||
return Math.Min(plannedFrameCount, safeConsumerCount);
|
||||
}
|
||||
|
||||
private static int ResolveFrameChannelCapacity(WaferDiePositionContext runtimeContext, int consumerCount)
|
||||
{
|
||||
if (runtimeContext == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(runtimeContext));
|
||||
}
|
||||
|
||||
int configuredCapacity = runtimeContext.ScanSettings == null
|
||||
? DefaultFrameChannelCapacity
|
||||
: runtimeContext.ScanSettings.FrameChannelCapacity;
|
||||
int safeCapacity = configuredCapacity <= 0 ? DefaultFrameChannelCapacity : configuredCapacity;
|
||||
return Math.Max(1, Math.Max(consumerCount, safeCapacity));
|
||||
}
|
||||
|
||||
private static void EnsureFrameResults(WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
if (runtimeContext.FrameResults == null)
|
||||
{
|
||||
runtimeContext.FrameResults = new List<WaferRecognitionFrameResult>();
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeContext.FrameResults.Clear();
|
||||
}
|
||||
|
||||
private static WaferRecipe GetCurrentWaferRecipe(RecipeManager recipeManager)
|
||||
{
|
||||
if (recipeManager == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(recipeManager));
|
||||
}
|
||||
|
||||
if (recipeManager.CurrentWaferRecipe == null)
|
||||
{
|
||||
throw new LocalizedProcessException(
|
||||
MessageKey.ProcessDiePositionRecipeNotLoaded,
|
||||
Array.Empty<string>());
|
||||
}
|
||||
|
||||
return recipeManager.CurrentWaferRecipe;
|
||||
}
|
||||
|
||||
private static WaferInfo GetWaferInfo(WaferRecipe waferRecipe)
|
||||
{
|
||||
if (waferRecipe == null || waferRecipe.WaferInfo == null)
|
||||
{
|
||||
throw new LocalizedProcessException(
|
||||
MessageKey.ProcessDiePositionRecipeNotLoaded,
|
||||
Array.Empty<string>());
|
||||
}
|
||||
|
||||
WaferInfo waferInfo = waferRecipe.WaferInfo;
|
||||
if (waferInfo.WaferRowNum <= 0 ||
|
||||
waferInfo.WaferColNum <= 0 ||
|
||||
waferInfo.DiePitchX == 0d ||
|
||||
waferInfo.DiePitchY == 0d)
|
||||
{
|
||||
throw new LocalizedProcessException(
|
||||
MessageKey.ProcessDiePositionWaferInfoMissing,
|
||||
Array.Empty<string>());
|
||||
}
|
||||
|
||||
return waferInfo;
|
||||
}
|
||||
|
||||
private static DiePositionProcessResult InitializeProcessResult(ProcessResultManager processResultManager)
|
||||
{
|
||||
if (processResultManager == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(processResultManager));
|
||||
}
|
||||
|
||||
DiePositionProcessResult processResult = processResultManager.DiePositionResult;
|
||||
processResult.IsSuccess = false;
|
||||
processResult.RecipeName = null;
|
||||
processResult.ErrorMessage = null;
|
||||
processResult.ErrorMessageKey = MessageKey.None;
|
||||
processResult.ErrorMessageArguments = Array.Empty<string>();
|
||||
processResult.RowCount = 0;
|
||||
processResult.ColumnCount = 0;
|
||||
processResult.TheoryDieCount = 0;
|
||||
processResult.ScanPointCount = 0;
|
||||
processResult.RecognizedDieCount = 0;
|
||||
processResult.NgDieCount = 0;
|
||||
processResult.AverageSpacingX = 0;
|
||||
processResult.AverageSpacingY = 0;
|
||||
processResult.MapDies = new Die[0, 0];
|
||||
processResult.PatchDies.Clear();
|
||||
processResult.ExceptDies.Clear();
|
||||
processResult.ScanPoints.Clear();
|
||||
processResult.RecognizedDies.Clear();
|
||||
return processResult;
|
||||
}
|
||||
|
||||
private static void PopulateProcessResult(
|
||||
DiePositionProcessResult processResult,
|
||||
WaferDiePositionContext runtimeContext,
|
||||
WaferScanPlan scanPlan)
|
||||
{
|
||||
List<Die> placeholderDies = CreatePlaceholderDies(runtimeContext);
|
||||
|
||||
processResult.RecipeName = runtimeContext.RecipeName;
|
||||
processResult.RowCount = runtimeContext.RowCount;
|
||||
processResult.ColumnCount = runtimeContext.ColumnCount;
|
||||
processResult.TheoryDieCount = runtimeContext.RowCount * runtimeContext.ColumnCount;
|
||||
processResult.ScanPointCount = scanPlan == null ? 0 : scanPlan.PathPointCount;
|
||||
processResult.RecognizedDieCount = runtimeContext.RecognitionSnapshot == null
|
||||
? placeholderDies.Count
|
||||
: runtimeContext.RecognitionSnapshot.RowsReal.Length;
|
||||
processResult.NgDieCount = runtimeContext.ExceptDies == null ? 0 : runtimeContext.ExceptDies.Count;
|
||||
processResult.AverageSpacingX = runtimeContext.PitchX;
|
||||
processResult.AverageSpacingY = runtimeContext.PitchY;
|
||||
processResult.MapDies = runtimeContext.MapDies ?? new Die[0, 0];
|
||||
processResult.PatchDies.Clear();
|
||||
if (runtimeContext.PatchDies != null)
|
||||
{
|
||||
foreach (Die patchDie in runtimeContext.PatchDies)
|
||||
{
|
||||
processResult.PatchDies.Add(patchDie);
|
||||
}
|
||||
}
|
||||
|
||||
processResult.ExceptDies.Clear();
|
||||
if (runtimeContext.ExceptDies != null)
|
||||
{
|
||||
foreach (Die exceptDie in runtimeContext.ExceptDies)
|
||||
{
|
||||
processResult.ExceptDies.Add(exceptDie);
|
||||
}
|
||||
}
|
||||
|
||||
if (scanPlan != null && scanPlan.PathPoints != null)
|
||||
{
|
||||
foreach (Point scanPoint in scanPlan.PathPoints)
|
||||
{
|
||||
processResult.ScanPoints.Add(scanPoint);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (Die die in placeholderDies)
|
||||
{
|
||||
processResult.RecognizedDies.Add(die);
|
||||
}
|
||||
|
||||
if (runtimeContext.MapDies != null && runtimeContext.MapDies.Length > 0)
|
||||
{
|
||||
processResult.RecognizedDies.Clear();
|
||||
foreach (Die mapDie in runtimeContext.MapDies)
|
||||
{
|
||||
if (mapDie != null)
|
||||
{
|
||||
processResult.RecognizedDies.Add(mapDie);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Die> CreatePlaceholderDies(WaferDiePositionContext runtimeContext)
|
||||
{
|
||||
List<Die> dies = new List<Die>();
|
||||
if (runtimeContext == null || runtimeContext.RowCount <= 0 || runtimeContext.ColumnCount <= 0)
|
||||
{
|
||||
return dies;
|
||||
}
|
||||
|
||||
for (int rowIndex = 0; rowIndex < runtimeContext.RowCount; rowIndex++)
|
||||
{
|
||||
for (int columnIndex = 0; columnIndex < runtimeContext.ColumnCount; columnIndex++)
|
||||
{
|
||||
Die die = new Die();
|
||||
die.Row = rowIndex + 1;
|
||||
die.Column = columnIndex + 1;
|
||||
die.X = columnIndex * runtimeContext.PitchX;
|
||||
die.Y = rowIndex * runtimeContext.PitchY;
|
||||
die.Status = DieStatus.Normal;
|
||||
dies.Add(die);
|
||||
}
|
||||
}
|
||||
|
||||
return dies;
|
||||
}
|
||||
|
||||
private static async Task CheckCancellationAndPauseAsync(ActivityControl activityControl)
|
||||
{
|
||||
activityControl.ThrowIfCancellationRequested();
|
||||
await activityControl.CheckPauseAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private static void WriteResultToContext(
|
||||
WorkflowContext context,
|
||||
ProcessResultManager processResultManager,
|
||||
DiePositionProcessResult processResult)
|
||||
{
|
||||
context.SetData(WorkflowContextKeys.DiePositionResult, processResult);
|
||||
processResultManager.SaveDiePositionResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public interface IWaferCaptureExecutor
|
||||
{
|
||||
Task<WaferScanExecutionSummary> ExecuteAsync(WaferDiePositionContext context, ChannelWriter<WaferCaptureFrame> frameWriter, CancellationToken cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
using MainShell.ProcessResult;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public interface IWaferDiePositionResultService
|
||||
{
|
||||
void ApplySnapshot(WaferDiePositionContext context, WaferRecognitionSnapshot snapshot);
|
||||
|
||||
void WriteProcessResult(WaferDiePositionContext context, DiePositionProcessResult processResult);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
using MainShell.Vision;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public interface IWaferDieRecognizer
|
||||
{
|
||||
Task<WaferRecognitionFrameResult> RecognizeAsync(WaferCaptureFrame frame, WaferDiePositionContext context, CancellationToken cancellationToken);
|
||||
|
||||
FindDieImageRequest BuildRequest(WaferCaptureFrame frame, WaferDiePositionContext context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
using MainShell.Common;
|
||||
using MainShell.Vision;
|
||||
using MwFramework.Device;
|
||||
using System.Windows;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public interface IWaferMachineAdapter
|
||||
{
|
||||
CameraType GetCameraSource(WaferDiePositionContext context);
|
||||
|
||||
MwCamera GetCamera(WaferDiePositionContext context);
|
||||
|
||||
Point GetCameraFieldOfView(WaferDiePositionContext context);
|
||||
|
||||
double GetExposureTimeMilliseconds(WaferDiePositionContext context);
|
||||
|
||||
CameraCaptureOptions CreateSoftTriggerCaptureOptions(WaferDiePositionContext context);
|
||||
|
||||
Point AdjustPointWithinSoftLimit(Point candidatePoint, WaferDiePositionContext context, double offsetMm, out bool adjusted, out string reason);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public interface IWaferRecognitionResultCollector
|
||||
{
|
||||
void Add(WaferRecognitionFrameResult frameResult);
|
||||
|
||||
WaferRecognitionSnapshot CreateSnapshot();
|
||||
|
||||
void Reset();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public interface IWaferScanPlanner
|
||||
{
|
||||
WaferScanPlan CreatePlan(WaferDiePositionContext context);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
using MainShell.Common;
|
||||
using MainShell.Hardware;
|
||||
using MainShell.Log;
|
||||
using MainShell.Motion;
|
||||
using MainShell.Vision;
|
||||
using MwFramework.Device;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Threading;
|
||||
using System.Threading.Channels;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using CameraBufferReceivedEventArgs = MwFramework.Device.Model.CameraBufferReceivedEventArgs;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class PlaceholderWaferCaptureExecutor : IWaferCaptureExecutor
|
||||
{
|
||||
private sealed class PendingCaptureRequest
|
||||
{
|
||||
public long SequenceId { get; set; }
|
||||
|
||||
public Point TriggerPoint { get; set; }
|
||||
|
||||
public DateTime TriggerTime { get; set; }
|
||||
}
|
||||
|
||||
private sealed class EventDrivenCaptureSession
|
||||
{
|
||||
private readonly object _syncRoot = new object();
|
||||
private readonly Queue<PendingCaptureRequest> _pendingRequests = new Queue<PendingCaptureRequest>();
|
||||
private readonly List<WaferCaptureFrame> _frames = new List<WaferCaptureFrame>();
|
||||
private readonly TaskCompletionSource<bool> _completionSource = new TaskCompletionSource<bool>();
|
||||
private int _expectedFrameCount;
|
||||
private int _receivedFrameCount;
|
||||
|
||||
public void Enqueue(PendingCaptureRequest pendingRequest)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_pendingRequests.Enqueue(pendingRequest);
|
||||
_expectedFrameCount++;
|
||||
}
|
||||
}
|
||||
|
||||
public void AddReceivedFrame(WaferCaptureFrame frame)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_frames.Add(frame);
|
||||
_receivedFrameCount++;
|
||||
if (_receivedFrameCount >= _expectedFrameCount && _pendingRequests.Count == 0)
|
||||
{
|
||||
_completionSource.TrySetResult(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public PendingCaptureRequest Dequeue()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
if (_pendingRequests.Count == 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return _pendingRequests.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
public Task WaitForCompletionAsync(int timeoutMilliseconds, CancellationToken cancellationToken)
|
||||
{
|
||||
return WaitForCompletionCoreAsync(timeoutMilliseconds, cancellationToken);
|
||||
}
|
||||
|
||||
public List<WaferCaptureFrame> GetFramesOrdered()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
List<WaferCaptureFrame> frames = new List<WaferCaptureFrame>(_frames);
|
||||
frames.Sort((left, right) => left.SequenceId.CompareTo(right.SequenceId));
|
||||
return frames;
|
||||
}
|
||||
}
|
||||
|
||||
public int ExpectedFrameCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _expectedFrameCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int ReceivedFrameCount
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
return _receivedFrameCount;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public List<WaferCaptureFrame> DrainPendingAsLost(string errorMessage)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
List<WaferCaptureFrame> lostFrames = new List<WaferCaptureFrame>();
|
||||
while (_pendingRequests.Count > 0)
|
||||
{
|
||||
PendingCaptureRequest pendingRequest = _pendingRequests.Dequeue();
|
||||
WaferCaptureFrame lostFrame = new WaferCaptureFrame();
|
||||
lostFrame.SequenceId = pendingRequest.SequenceId;
|
||||
lostFrame.TriggerPoint = pendingRequest.TriggerPoint;
|
||||
lostFrame.FrameId = Guid.NewGuid().ToString("N");
|
||||
lostFrame.TriggerTime = pendingRequest.TriggerTime;
|
||||
lostFrame.ReceiveTime = DateTime.Now;
|
||||
lostFrame.IsLostFrame = true;
|
||||
lostFrame.CaptureErrorMessage = errorMessage;
|
||||
_frames.Add(lostFrame);
|
||||
lostFrames.Add(lostFrame);
|
||||
}
|
||||
|
||||
if (_receivedFrameCount >= _expectedFrameCount && _pendingRequests.Count == 0)
|
||||
{
|
||||
_completionSource.TrySetResult(true);
|
||||
}
|
||||
|
||||
return lostFrames;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task WaitForCompletionCoreAsync(int timeoutMilliseconds, CancellationToken cancellationToken)
|
||||
{
|
||||
using (CancellationTokenRegistration registration = cancellationToken.Register(() => _completionSource.TrySetCanceled()))
|
||||
{
|
||||
Task completedTask = await Task.WhenAny(_completionSource.Task, Task.Delay(timeoutMilliseconds, cancellationToken)).ConfigureAwait(false);
|
||||
if (completedTask != _completionSource.Task)
|
||||
{
|
||||
throw new TimeoutException(string.Format(CultureInfo.InvariantCulture, "晶圆定位:等待相机图像超时,超时时间 {0} ms。", timeoutMilliseconds));
|
||||
}
|
||||
|
||||
await _completionSource.Task.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const int DefaultMotionSettlingMilliseconds = 20;
|
||||
private readonly IWaferMachineAdapter _waferMachineAdapter;
|
||||
private readonly SafeAxisMotion _safeAxisMotion;
|
||||
private readonly HardwareManager _hardwareManager;
|
||||
|
||||
public PlaceholderWaferCaptureExecutor(
|
||||
IWaferMachineAdapter waferMachineAdapter,
|
||||
SafeAxisMotion safeAxisMotion,
|
||||
HardwareManager hardwareManager)
|
||||
{
|
||||
_waferMachineAdapter = waferMachineAdapter ?? throw new ArgumentNullException(nameof(waferMachineAdapter));
|
||||
_safeAxisMotion = safeAxisMotion ?? throw new ArgumentNullException(nameof(safeAxisMotion));
|
||||
_hardwareManager = hardwareManager ?? throw new ArgumentNullException(nameof(hardwareManager));
|
||||
}
|
||||
|
||||
public async Task<WaferScanExecutionSummary> ExecuteAsync(WaferDiePositionContext context, ChannelWriter<WaferCaptureFrame> frameWriter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
if (frameWriter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(frameWriter));
|
||||
}
|
||||
|
||||
List<Point> pathPoints = ResolvePathPoints(context);
|
||||
WaferScanExecutionSummary summary = new WaferScanExecutionSummary();
|
||||
summary.PlannedPointCount = pathPoints.Count;
|
||||
summary.ProducedFrameCount = 0;
|
||||
summary.ConsumedFrameCount = 0;
|
||||
summary.RecognitionSuccessCount = 0;
|
||||
summary.RecognitionFailureCount = 0;
|
||||
summary.LastErrorMessage = null;
|
||||
|
||||
if (pathPoints.Count == 0)
|
||||
{
|
||||
LogManager.LogProcessInfo("晶圆定位:未生成扫描点,跳过运动与采图。");
|
||||
return summary;
|
||||
}
|
||||
|
||||
EnsureCaptureFrameList(context);
|
||||
|
||||
CameraType cameraSource = _waferMachineAdapter.GetCameraSource(context);
|
||||
CameraCaptureOptions captureOptions = _waferMachineAdapter.CreateSoftTriggerCaptureOptions(context) ?? CameraCaptureOptions.CreateSoftTrigger(5000);
|
||||
MwCamera camera = _waferMachineAdapter.GetCamera(context);
|
||||
IAxis axisX = _hardwareManager.Axis_PHS_X1;
|
||||
IAxis axisY = _hardwareManager.Axis_Stage_Y3;
|
||||
EnsureAxes(axisX, axisY);
|
||||
EnsureCamera(camera, cameraSource);
|
||||
|
||||
int motionSettlingMilliseconds = ResolveMotionSettlingMilliseconds(captureOptions);
|
||||
int exposureDelayMilliseconds = ResolveExposureDelayMilliseconds(_waferMachineAdapter.GetExposureTimeMilliseconds(context));
|
||||
int completionTimeoutMilliseconds = ResolveCompletionTimeoutMilliseconds(captureOptions, exposureDelayMilliseconds, pathPoints.Count);
|
||||
EventDrivenCaptureSession captureSession = new EventDrivenCaptureSession();
|
||||
|
||||
EventHandler<CameraBufferReceivedEventArgs> cameraBufferHandler = (sender, eventArgs) =>
|
||||
{
|
||||
HandleCameraBufferReceived(captureSession, frameWriter, eventArgs, cancellationToken);
|
||||
};
|
||||
|
||||
PrepareCameraForSoftTrigger(camera);
|
||||
camera.CameraBufferReceived += cameraBufferHandler;
|
||||
|
||||
try
|
||||
{
|
||||
for (int pointIndex = 0; pointIndex < pathPoints.Count; pointIndex++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
Point targetPoint = pathPoints[pointIndex];
|
||||
LogManager.LogProcessInfo(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"晶圆定位:执行扫描点 {0}/{1},X={2:F3},Y={3:F3}。",
|
||||
pointIndex + 1,
|
||||
pathPoints.Count,
|
||||
targetPoint.X,
|
||||
targetPoint.Y));
|
||||
|
||||
try
|
||||
{
|
||||
await MoveToPointAsync(axisX, axisY, targetPoint, cancellationToken).ConfigureAwait(false);
|
||||
await Task.Delay(motionSettlingMilliseconds, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
PendingCaptureRequest pendingCaptureRequest = new PendingCaptureRequest();
|
||||
pendingCaptureRequest.SequenceId = pointIndex + 1;
|
||||
pendingCaptureRequest.TriggerPoint = targetPoint;
|
||||
pendingCaptureRequest.TriggerTime = DateTime.Now;
|
||||
captureSession.Enqueue(pendingCaptureRequest);
|
||||
|
||||
TriggerSoftCapture(camera, pendingCaptureRequest, exposureDelayMilliseconds);
|
||||
await Task.Delay(exposureDelayMilliseconds, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
summary.LastErrorMessage = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"晶圆定位:在扫描点 {0}/{1} 处取消流程。",
|
||||
pointIndex + 1,
|
||||
pathPoints.Count);
|
||||
context.ErrorMessage = summary.LastErrorMessage;
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
summary.LastErrorMessage = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"晶圆定位:扫描点 {0}/{1} 执行失败,X={2:F3},Y={3:F3}。{4}",
|
||||
pointIndex + 1,
|
||||
pathPoints.Count,
|
||||
targetPoint.X,
|
||||
targetPoint.Y,
|
||||
ex.Message);
|
||||
context.ErrorMessage = summary.LastErrorMessage;
|
||||
LogManager.LogProcessError(summary.LastErrorMessage);
|
||||
throw new InvalidOperationException(summary.LastErrorMessage, ex);
|
||||
}
|
||||
}
|
||||
|
||||
await captureSession.WaitForCompletionAsync(completionTimeoutMilliseconds, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (TimeoutException ex)
|
||||
{
|
||||
List<WaferCaptureFrame> lostFrames = captureSession.DrainPendingAsLost(ex.Message);
|
||||
summary.LastErrorMessage = ex.Message;
|
||||
context.ErrorMessage = ex.Message;
|
||||
foreach (WaferCaptureFrame lostFrame in lostFrames)
|
||||
{
|
||||
context.CaptureFrames.Add(lostFrame);
|
||||
PublishCapturedFrame(lostFrame, frameWriter, cancellationToken);
|
||||
}
|
||||
|
||||
throw new InvalidOperationException(ex.Message, ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
camera.CameraBufferReceived -= cameraBufferHandler;
|
||||
}
|
||||
|
||||
List<WaferCaptureFrame> receivedFrames = captureSession.GetFramesOrdered();
|
||||
context.CaptureFrames.Clear();
|
||||
foreach (WaferCaptureFrame receivedFrame in receivedFrames)
|
||||
{
|
||||
context.CaptureFrames.Add(receivedFrame);
|
||||
}
|
||||
|
||||
summary.ProducedFrameCount = context.CaptureFrames.Count;
|
||||
summary.ConsumedFrameCount = context.CaptureFrames.Count;
|
||||
if (summary.ProducedFrameCount != summary.PlannedPointCount)
|
||||
{
|
||||
summary.LastErrorMessage = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"晶圆定位:采图帧数量不一致。计划={0},实际接收={1}。",
|
||||
summary.PlannedPointCount,
|
||||
summary.ProducedFrameCount);
|
||||
context.ErrorMessage = summary.LastErrorMessage;
|
||||
throw new InvalidOperationException(summary.LastErrorMessage);
|
||||
}
|
||||
|
||||
LogManager.LogProcessInfo(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"晶圆定位:扫描执行完成。计划帧数={0},采图帧数={1}。",
|
||||
summary.PlannedPointCount,
|
||||
summary.ProducedFrameCount));
|
||||
|
||||
return summary;
|
||||
}
|
||||
|
||||
private async Task MoveToPointAsync(IAxis axisX, IAxis axisY, Point targetPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
MotionMoveRequest requestX = MotionMoveRequest.ForAxis(axisX, targetPoint.X, source: "DiePosition", tags: new string[] { "WaferScan", "TopCamera" });
|
||||
MotionMoveRequest requestY = MotionMoveRequest.ForAxis(axisY, targetPoint.Y, source: "DiePosition", tags: new string[] { "WaferScan", "TopCamera" });
|
||||
MotionBatchResult motionResult = await _safeAxisMotion.SafeMoveAsync(cancellationToken, requestX, requestY).ConfigureAwait(false);
|
||||
motionResult.EnsureSuccess();
|
||||
}
|
||||
|
||||
private void HandleCameraBufferReceived(
|
||||
EventDrivenCaptureSession captureSession,
|
||||
ChannelWriter<WaferCaptureFrame> frameWriter,
|
||||
CameraBufferReceivedEventArgs eventArgs,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
PendingCaptureRequest pendingCaptureRequest = captureSession.Dequeue();
|
||||
if (pendingCaptureRequest == null)
|
||||
{
|
||||
LogManager.LogProcessInfo("晶圆定位:收到异常相机图像,因未找到待处理触发请求,已忽略该图像。");
|
||||
return;
|
||||
}
|
||||
|
||||
WaferCaptureFrame frame = new WaferCaptureFrame();
|
||||
frame.SequenceId = pendingCaptureRequest.SequenceId;
|
||||
frame.TriggerPoint = pendingCaptureRequest.TriggerPoint;
|
||||
frame.FrameId = Guid.NewGuid().ToString("N");
|
||||
frame.TriggerTime = pendingCaptureRequest.TriggerTime;
|
||||
frame.ReceiveTime = DateTime.Now;
|
||||
|
||||
if (eventArgs == null || !eventArgs.IsValid || eventArgs.CameraData == null)
|
||||
{
|
||||
frame.IsLostFrame = true;
|
||||
frame.CaptureErrorMessage = eventArgs == null
|
||||
? "晶圆定位:相机事件参数为空。"
|
||||
: string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"晶圆定位:收到无效相机图像。错误码={0},描述={1}。",
|
||||
eventArgs.GrabErrorCode,
|
||||
eventArgs.GrabErrorDescription ?? string.Empty);
|
||||
captureSession.AddReceivedFrame(frame);
|
||||
PublishCapturedFrame(frame, frameWriter, cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
frame.IsLostFrame = false;
|
||||
frame.Image = new MainShell.Models.MxImage(eventArgs.CameraData);
|
||||
frame.CaptureErrorMessage = null;
|
||||
captureSession.AddReceivedFrame(frame);
|
||||
PublishCapturedFrame(frame, frameWriter, cancellationToken);
|
||||
}
|
||||
|
||||
private static void PublishCapturedFrame(WaferCaptureFrame frame, ChannelWriter<WaferCaptureFrame> frameWriter, CancellationToken cancellationToken)
|
||||
{
|
||||
if (frame == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (frameWriter == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(frameWriter));
|
||||
}
|
||||
|
||||
frameWriter.WriteAsync(frame, cancellationToken).AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
|
||||
private static List<Point> ResolvePathPoints(WaferDiePositionContext context)
|
||||
{
|
||||
if (context == null || context.ScanPlan == null)
|
||||
{
|
||||
return new List<Point>();
|
||||
}
|
||||
|
||||
IReadOnlyList<Point> candidatePoints = context.ScanPlan.FinalPathPoints ?? context.ScanPlan.PathPoints;
|
||||
if (candidatePoints == null || candidatePoints.Count == 0)
|
||||
{
|
||||
return new List<Point>();
|
||||
}
|
||||
|
||||
return new List<Point>(candidatePoints);
|
||||
}
|
||||
|
||||
private static void EnsureCaptureFrameList(WaferDiePositionContext context)
|
||||
{
|
||||
if (context.CaptureFrames == null)
|
||||
{
|
||||
context.CaptureFrames = new List<WaferCaptureFrame>();
|
||||
}
|
||||
else
|
||||
{
|
||||
context.CaptureFrames.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureCamera(MwCamera camera, CameraType cameraSource)
|
||||
{
|
||||
if (camera == null)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "晶圆定位:相机“{0}”不可用。", cameraSource));
|
||||
}
|
||||
|
||||
if (!camera.IsOpen)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "晶圆定位:相机“{0}”未打开。", cameraSource));
|
||||
}
|
||||
}
|
||||
|
||||
private static int ResolveMotionSettlingMilliseconds(CameraCaptureOptions captureOptions)
|
||||
{
|
||||
if (captureOptions != null && captureOptions.TriggerDelay.HasValue && captureOptions.TriggerDelay.Value > 0d)
|
||||
{
|
||||
return Math.Max(1, (int)Math.Ceiling(captureOptions.TriggerDelay.Value));
|
||||
}
|
||||
|
||||
return DefaultMotionSettlingMilliseconds;
|
||||
}
|
||||
|
||||
private static int ResolveExposureDelayMilliseconds(double exposureTimeMilliseconds)
|
||||
{
|
||||
if (double.IsNaN(exposureTimeMilliseconds) || double.IsInfinity(exposureTimeMilliseconds) || exposureTimeMilliseconds <= 0d)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return Math.Max(1, (int)Math.Ceiling(exposureTimeMilliseconds));
|
||||
}
|
||||
|
||||
private static int ResolveCompletionTimeoutMilliseconds(CameraCaptureOptions captureOptions, int exposureDelayMilliseconds, int pointCount)
|
||||
{
|
||||
int baseTimeout = captureOptions == null || captureOptions.TimeoutMilliseconds <= 0 ? 5000 : captureOptions.TimeoutMilliseconds;
|
||||
int pipelineTimeout = exposureDelayMilliseconds * Math.Max(1, pointCount) + 1000;
|
||||
return Math.Max(baseTimeout, pipelineTimeout);
|
||||
}
|
||||
|
||||
private static void TriggerSoftCapture(MwCamera camera, PendingCaptureRequest pendingCaptureRequest, int exposureDelayMilliseconds)
|
||||
{
|
||||
DriverLibResult triggerResult = camera.ExcuteSoftTrigger();
|
||||
if (triggerResult != DriverLibResult.DriverLibNoError)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"晶圆定位:软触发失败。序号={0},X={1:F3},Y={2:F3},曝光等待={3} ms,结果={4}。",
|
||||
pendingCaptureRequest.SequenceId,
|
||||
pendingCaptureRequest.TriggerPoint.X,
|
||||
pendingCaptureRequest.TriggerPoint.Y,
|
||||
exposureDelayMilliseconds,
|
||||
triggerResult));
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrepareCameraForSoftTrigger(MwCamera camera)
|
||||
{
|
||||
CameraTriggerMode triggerMode = (CameraTriggerMode)Enum.Parse(typeof(CameraTriggerMode), "On", true);
|
||||
CameraTriggerSource triggerSource = (CameraTriggerSource)Enum.Parse(typeof(CameraTriggerSource), "Software", true);
|
||||
|
||||
DriverLibResult setModeResult = camera.SetTriggerMode(triggerMode);
|
||||
if (setModeResult != DriverLibResult.DriverLibNoError)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "晶圆定位:设置相机触发模式失败。结果={0}。", setModeResult));
|
||||
}
|
||||
|
||||
DriverLibResult setSourceResult = camera.SetTriggerSource(triggerSource);
|
||||
if (setSourceResult != DriverLibResult.DriverLibNoError)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "晶圆定位:设置相机触发源失败。结果={0}。", setSourceResult));
|
||||
}
|
||||
|
||||
if (!camera.IsGrabbing)
|
||||
{
|
||||
DriverLibResult startGrabbingResult = camera.StartGrabbing();
|
||||
if (startGrabbingResult != DriverLibResult.DriverLibNoError && startGrabbingResult != DriverLibResult.DriverLibDeviceAlreadyGrabbing)
|
||||
{
|
||||
throw new InvalidOperationException(string.Format(CultureInfo.InvariantCulture, "晶圆定位:启动相机采集失败。结果={0}。", startGrabbingResult));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureAxes(IAxis axisX, IAxis axisY)
|
||||
{
|
||||
if (axisX == null || axisY == null)
|
||||
{
|
||||
throw new InvalidOperationException("晶圆定位:HardwareManager 中未找到顶相机对应的 X/Y 轴。");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
using MainShell.Models;
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferCaptureFrame
|
||||
{
|
||||
public long SequenceId { get; set; }
|
||||
|
||||
public Point TriggerPoint { get; set; }
|
||||
|
||||
public string FrameId { get; set; }
|
||||
|
||||
public DateTime TriggerTime { get; set; }
|
||||
|
||||
public DateTime ReceiveTime { get; set; }
|
||||
|
||||
public bool IsLostFrame { get; set; }
|
||||
|
||||
public string ImageFilePath { get; set; }
|
||||
|
||||
public MxImage Image { get; set; }
|
||||
|
||||
public string CaptureErrorMessage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using MainShell.Recipe.Models;
|
||||
using MainShell.Models.Wafer;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Windows;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferDiePositionContext
|
||||
{
|
||||
public WaferDiePositionContext()
|
||||
{
|
||||
CaptureFrames = new List<WaferCaptureFrame>();
|
||||
FrameResults = new List<WaferRecognitionFrameResult>();
|
||||
MapDies = new Die[0, 0];
|
||||
PatchDies = new List<Die>();
|
||||
ExceptDies = new List<Die>();
|
||||
}
|
||||
|
||||
public string WorkflowName { get; set; }
|
||||
|
||||
public string RecipeName { get; set; }
|
||||
|
||||
public WaferRecipe WaferRecipe { get; set; }
|
||||
|
||||
public WaferScanSettings ScanSettings { get; set; }
|
||||
|
||||
public int RowCount { get; set; }
|
||||
|
||||
public int ColumnCount { get; set; }
|
||||
|
||||
public double PitchX { get; set; }
|
||||
|
||||
public double PitchY { get; set; }
|
||||
|
||||
public WaferScanArea ScanArea { get; set; }
|
||||
|
||||
public CancellationToken CancellationToken { get; set; }
|
||||
|
||||
public List<WaferCaptureFrame> CaptureFrames { get; set; }
|
||||
|
||||
public List<WaferRecognitionFrameResult> FrameResults { get; set; }
|
||||
|
||||
public WaferRecognitionSnapshot RecognitionSnapshot { get; set; }
|
||||
|
||||
public WaferScanExecutionSummary ExecutionSummary { get; set; }
|
||||
|
||||
public bool IsManualMode { get; set; }
|
||||
|
||||
public bool IsVisionCheckMode { get; set; }
|
||||
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public string OutputDirectory { get; set; }
|
||||
|
||||
public WaferScanPlan ScanPlan { get; set; }
|
||||
|
||||
public double MapAngleThreshold { get; set; }
|
||||
|
||||
public double MapRotateAngle { get; set; }
|
||||
|
||||
public double MapDistanceThreshold { get; set; }
|
||||
|
||||
public Die[,] MapDies { get; set; }
|
||||
|
||||
public List<Die> PatchDies { get; set; }
|
||||
|
||||
public List<Die> ExceptDies { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using MainShell.ProcessResult;
|
||||
using System;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferDiePositionResultService : IWaferDiePositionResultService
|
||||
{
|
||||
public void ApplySnapshot(WaferDiePositionContext context, WaferRecognitionSnapshot snapshot)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
context.RecognitionSnapshot = snapshot;
|
||||
}
|
||||
|
||||
public void WriteProcessResult(WaferDiePositionContext context, DiePositionProcessResult processResult)
|
||||
{
|
||||
if (processResult == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(processResult));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
using MainShell.Vision;
|
||||
using SemiconductorVisionAlgorithm.SemiParams;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferDieRecognizer : IWaferDieRecognizer
|
||||
{
|
||||
private readonly IFindDieService _findDieService;
|
||||
|
||||
public WaferDieRecognizer(IFindDieService findDieService)
|
||||
{
|
||||
_findDieService = findDieService ?? throw new ArgumentNullException(nameof(findDieService));
|
||||
}
|
||||
|
||||
public FindDieImageRequest BuildRequest(WaferCaptureFrame frame, WaferDiePositionContext context)
|
||||
{
|
||||
FindDieImageRequest request = new FindDieImageRequest();
|
||||
FindDieParameters parameters = new FindDieParameters();
|
||||
parameters.CurrentRuler = frame == null
|
||||
? new Point(0.0d, 0.0d)
|
||||
: new Point(frame.TriggerPoint.X, frame.TriggerPoint.Y);
|
||||
parameters.OverlapX = context != null && context.ScanSettings != null ? context.ScanSettings.OverlapRateX : 0d;
|
||||
parameters.OverlapY = context != null && context.ScanSettings != null ? context.ScanSettings.OverlapRateY : 0d;
|
||||
parameters.DieWidth = context != null && context.WaferRecipe != null && context.WaferRecipe.WaferInfo != null
|
||||
? context.WaferRecipe.WaferInfo.DieSizeX
|
||||
: 0d;
|
||||
parameters.DieHeight = context != null && context.WaferRecipe != null && context.WaferRecipe.WaferInfo != null
|
||||
? context.WaferRecipe.WaferInfo.DieSizeY
|
||||
: 0d;
|
||||
parameters.ScoreThreshold = 0.8d;
|
||||
parameters.TemplatePath = string.Empty;
|
||||
parameters.SerialNumber = string.Empty;
|
||||
request.Parameters = parameters;
|
||||
request.TimeoutMilliseconds = 3000;
|
||||
return request;
|
||||
}
|
||||
|
||||
public async Task<WaferRecognitionFrameResult> RecognizeAsync(WaferCaptureFrame frame, WaferDiePositionContext context, CancellationToken cancellationToken)
|
||||
{
|
||||
if (frame == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(frame));
|
||||
}
|
||||
|
||||
WaferRecognitionFrameResult frameResult = new WaferRecognitionFrameResult();
|
||||
frameResult.SequenceId = frame.SequenceId;
|
||||
|
||||
if (frame.Image == null)
|
||||
{
|
||||
frameResult.IsSuccess = false;
|
||||
frameResult.ErrorMessage = "DiePosition: capture frame image is null.";
|
||||
return frameResult;
|
||||
}
|
||||
|
||||
FindDieImageRequest request = BuildRequest(frame, context);
|
||||
VisionProcessResult<FindDiesResult> result = await _findDieService.ProcessMultipleImageAsync(frame.Image, request, cancellationToken).ConfigureAwait(false);
|
||||
if (result == null || !result.Succeeded || result.Data == null)
|
||||
{
|
||||
frameResult.IsSuccess = false;
|
||||
frameResult.ErrorMessage = result == null ? "DiePosition: recognition returned null result." : result.Message;
|
||||
return frameResult;
|
||||
}
|
||||
|
||||
List<double> rowsReal = new List<double>();
|
||||
List<double> columnsReal = new List<double>();
|
||||
List<double> angles = new List<double>();
|
||||
List<double> overlapRows = new List<double>();
|
||||
List<double> overlapColumns = new List<double>();
|
||||
List<double> overlapAngles = new List<double>();
|
||||
|
||||
foreach (FindDieResult item in result.Data.Items)
|
||||
{
|
||||
if (item.IsOverlap)
|
||||
{
|
||||
overlapRows.Add(item.CenterY);
|
||||
overlapColumns.Add(item.CenterX);
|
||||
overlapAngles.Add(item.Angle);
|
||||
}
|
||||
else
|
||||
{
|
||||
rowsReal.Add(item.CenterY);
|
||||
columnsReal.Add(item.CenterX);
|
||||
angles.Add(item.Angle);
|
||||
}
|
||||
}
|
||||
|
||||
frameResult.IsSuccess = true;
|
||||
frameResult.ErrorMessage = null;
|
||||
frameResult.RowsReal = rowsReal.ToArray();
|
||||
frameResult.ColumnsReal = columnsReal.ToArray();
|
||||
frameResult.Angles = angles.ToArray();
|
||||
frameResult.OverlapRows = overlapRows.ToArray();
|
||||
frameResult.OverlapColumns = overlapColumns.ToArray();
|
||||
frameResult.OverlapAngles = overlapAngles.ToArray();
|
||||
frameResult.UniqueCount = frameResult.RowsReal.Length;
|
||||
frameResult.OverlapCount = frameResult.OverlapRows.Length;
|
||||
return frameResult;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferPlannerPointAdjustment
|
||||
{
|
||||
public Point OriginalPoint { get; set; }
|
||||
|
||||
public Point AdjustedPoint { get; set; }
|
||||
|
||||
public bool WasAdjusted { get; set; }
|
||||
|
||||
public string Reason { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferRecognitionFrameResult
|
||||
{
|
||||
public WaferRecognitionFrameResult()
|
||||
{
|
||||
RowsReal = Array.Empty<double>();
|
||||
ColumnsReal = Array.Empty<double>();
|
||||
Angles = Array.Empty<double>();
|
||||
OverlapRows = Array.Empty<double>();
|
||||
OverlapColumns = Array.Empty<double>();
|
||||
OverlapAngles = Array.Empty<double>();
|
||||
}
|
||||
|
||||
public long SequenceId { get; set; }
|
||||
|
||||
public bool IsSuccess { get; set; }
|
||||
|
||||
public string ErrorMessage { get; set; }
|
||||
|
||||
public double[] RowsReal { get; set; }
|
||||
|
||||
public double[] ColumnsReal { get; set; }
|
||||
|
||||
public double[] Angles { get; set; }
|
||||
|
||||
public double[] OverlapRows { get; set; }
|
||||
|
||||
public double[] OverlapColumns { get; set; }
|
||||
|
||||
public double[] OverlapAngles { get; set; }
|
||||
|
||||
public int UniqueCount { get; set; }
|
||||
|
||||
public int OverlapCount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferRecognitionResultCollector : IWaferRecognitionResultCollector
|
||||
{
|
||||
private readonly object _syncRoot = new object();
|
||||
private readonly List<double> _rowsReal = new List<double>();
|
||||
private readonly List<double> _columnsReal = new List<double>();
|
||||
private readonly List<double> _angles = new List<double>();
|
||||
private readonly List<double> _overlapRows = new List<double>();
|
||||
private readonly List<double> _overlapColumns = new List<double>();
|
||||
private readonly List<double> _overlapAngles = new List<double>();
|
||||
private int _totalFrameCount;
|
||||
private int _successFrameCount;
|
||||
private int _failedFrameCount;
|
||||
private int _lostFrameCount;
|
||||
|
||||
public void Add(WaferRecognitionFrameResult frameResult)
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_totalFrameCount++;
|
||||
|
||||
if (frameResult == null)
|
||||
{
|
||||
_failedFrameCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!frameResult.IsSuccess)
|
||||
{
|
||||
_failedFrameCount++;
|
||||
return;
|
||||
}
|
||||
|
||||
_successFrameCount++;
|
||||
_rowsReal.AddRange(frameResult.RowsReal);
|
||||
_columnsReal.AddRange(frameResult.ColumnsReal);
|
||||
_angles.AddRange(frameResult.Angles);
|
||||
_overlapRows.AddRange(frameResult.OverlapRows);
|
||||
_overlapColumns.AddRange(frameResult.OverlapColumns);
|
||||
_overlapAngles.AddRange(frameResult.OverlapAngles);
|
||||
}
|
||||
}
|
||||
|
||||
public WaferRecognitionSnapshot CreateSnapshot()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
WaferRecognitionSnapshot snapshot = new WaferRecognitionSnapshot();
|
||||
snapshot.RowsReal = _rowsReal.ToArray();
|
||||
snapshot.ColumnsReal = _columnsReal.ToArray();
|
||||
snapshot.Angles = _angles.ToArray();
|
||||
snapshot.OverlapRows = _overlapRows.ToArray();
|
||||
snapshot.OverlapColumns = _overlapColumns.ToArray();
|
||||
snapshot.OverlapAngles = _overlapAngles.ToArray();
|
||||
snapshot.TotalFrameCount = _totalFrameCount;
|
||||
snapshot.SuccessFrameCount = _successFrameCount;
|
||||
snapshot.FailedFrameCount = _failedFrameCount;
|
||||
snapshot.LostFrameCount = _lostFrameCount;
|
||||
return snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_syncRoot)
|
||||
{
|
||||
_rowsReal.Clear();
|
||||
_columnsReal.Clear();
|
||||
_angles.Clear();
|
||||
_overlapRows.Clear();
|
||||
_overlapColumns.Clear();
|
||||
_overlapAngles.Clear();
|
||||
_totalFrameCount = 0;
|
||||
_successFrameCount = 0;
|
||||
_failedFrameCount = 0;
|
||||
_lostFrameCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
using System;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferRecognitionSnapshot
|
||||
{
|
||||
public WaferRecognitionSnapshot()
|
||||
{
|
||||
RowsReal = Array.Empty<double>();
|
||||
ColumnsReal = Array.Empty<double>();
|
||||
Angles = Array.Empty<double>();
|
||||
OverlapRows = Array.Empty<double>();
|
||||
OverlapColumns = Array.Empty<double>();
|
||||
OverlapAngles = Array.Empty<double>();
|
||||
}
|
||||
|
||||
public double[] RowsReal { get; set; }
|
||||
|
||||
public double[] ColumnsReal { get; set; }
|
||||
|
||||
public double[] Angles { get; set; }
|
||||
|
||||
public double[] OverlapRows { get; set; }
|
||||
|
||||
public double[] OverlapColumns { get; set; }
|
||||
|
||||
public double[] OverlapAngles { get; set; }
|
||||
|
||||
public int TotalFrameCount { get; set; }
|
||||
|
||||
public int SuccessFrameCount { get; set; }
|
||||
|
||||
public int FailedFrameCount { get; set; }
|
||||
|
||||
public int LostFrameCount { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
using System.Windows;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferScanArea
|
||||
{
|
||||
public Point StartPoint { get; set; }
|
||||
|
||||
public Point EndPoint { get; set; }
|
||||
|
||||
public double MinX { get; set; }
|
||||
|
||||
public double MaxX { get; set; }
|
||||
|
||||
public double MinY { get; set; }
|
||||
|
||||
public double MaxY { get; set; }
|
||||
|
||||
public double WaferWidth { get; set; }
|
||||
|
||||
public double WaferHeight { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferScanExecutionSummary
|
||||
{
|
||||
public int PlannedPointCount { get; set; }
|
||||
|
||||
public int ProducedFrameCount { get; set; }
|
||||
|
||||
public int ConsumedFrameCount { get; set; }
|
||||
|
||||
public int RecognitionSuccessCount { get; set; }
|
||||
|
||||
public int RecognitionFailureCount { get; set; }
|
||||
|
||||
public string LastErrorMessage { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Windows;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
public class WaferScanPlan
|
||||
{
|
||||
public WaferScanPlan()
|
||||
{
|
||||
PathPoints = Array.Empty<Point>();
|
||||
RawPathPoints = Array.Empty<Point>();
|
||||
FinalPathPoints = Array.Empty<Point>();
|
||||
Adjustments = Array.Empty<WaferPlannerPointAdjustment>();
|
||||
}
|
||||
|
||||
public Point StartPoint { get; set; }
|
||||
|
||||
public Point EndPoint { get; set; }
|
||||
|
||||
public IReadOnlyList<Point> PathPoints { get; set; }
|
||||
|
||||
public IReadOnlyList<Point> RawPathPoints { get; set; }
|
||||
|
||||
public IReadOnlyList<Point> FinalPathPoints { get; set; }
|
||||
|
||||
public WaferScanArea ScanArea { get; set; }
|
||||
|
||||
public double StepX { get; set; }
|
||||
|
||||
public double StepY { get; set; }
|
||||
|
||||
public double OverlapX { get; set; }
|
||||
|
||||
public double OverlapY { get; set; }
|
||||
|
||||
public IReadOnlyList<WaferPlannerPointAdjustment> Adjustments { get; set; }
|
||||
|
||||
public string PathGenerationMessage { get; set; }
|
||||
|
||||
public int ScanRowCount { get; set; }
|
||||
|
||||
public int ScanColumnCount { get; set; }
|
||||
|
||||
public int SoftLimitAdjustedCount { get; set; }
|
||||
|
||||
public int AddedCoveragePointCount { get; set; }
|
||||
|
||||
public int PathPointCount
|
||||
{
|
||||
get
|
||||
{
|
||||
IReadOnlyList<Point> effectivePathPoints = FinalPathPoints ?? PathPoints;
|
||||
return effectivePathPoints == null ? 0 : effectivePathPoints.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
using MaxwellFramework.Core.Attributes;
|
||||
using System;
|
||||
using MainShell.Common;
|
||||
using MainShell.Recipe.Models;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
|
||||
namespace MainShell.Process
|
||||
{
|
||||
[Singleton]
|
||||
public class WaferScanPlanner : IWaferScanPlanner
|
||||
{
|
||||
private readonly IWaferMachineAdapter _waferMachineAdapter;
|
||||
|
||||
public WaferScanPlanner(IWaferMachineAdapter waferMachineAdapter)
|
||||
{
|
||||
_waferMachineAdapter = waferMachineAdapter ?? throw new ArgumentNullException(nameof(waferMachineAdapter));
|
||||
}
|
||||
|
||||
public WaferScanPlan CreatePlan(WaferDiePositionContext context)
|
||||
{
|
||||
if (context == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(context));
|
||||
}
|
||||
|
||||
WaferScanSettings scanSettings = context.ScanSettings ?? throw new InvalidOperationException("DiePosition: scan settings are required for scan planning.");
|
||||
Point cameraFieldOfView = _waferMachineAdapter.GetCameraFieldOfView(context);
|
||||
ValidateCameraFieldOfView(cameraFieldOfView);
|
||||
|
||||
WaferScanArea scanArea = BuildScanArea(context, scanSettings, cameraFieldOfView);
|
||||
double stepX = CalculateStep(cameraFieldOfView.X, scanSettings.OverlapRateX);
|
||||
double stepY = CalculateStep(cameraFieldOfView.Y, scanSettings.OverlapRateY);
|
||||
int scanColumnCount = CalculateScanCount(scanArea.WaferWidth, cameraFieldOfView.X, stepX);
|
||||
int scanRowCount = CalculateScanCount(scanArea.WaferHeight, cameraFieldOfView.Y, stepY);
|
||||
List<Point> rawPathPoints = BuildRawPathPoints(scanArea, scanSettings, stepX, stepY, scanColumnCount, scanRowCount);
|
||||
List<WaferPlannerPointAdjustment> adjustments = new List<WaferPlannerPointAdjustment>();
|
||||
List<Point> finalPathPoints = BuildAdjustedPathPoints(rawPathPoints, context, scanSettings, adjustments);
|
||||
|
||||
Point startPoint = finalPathPoints.Count > 0 ? finalPathPoints[0] : scanArea.StartPoint;
|
||||
Point endPoint = finalPathPoints.Count > 0 ? finalPathPoints[finalPathPoints.Count - 1] : scanArea.EndPoint;
|
||||
|
||||
WaferScanPlan scanPlan = new WaferScanPlan();
|
||||
scanPlan.ScanRowCount = scanRowCount;
|
||||
scanPlan.ScanColumnCount = scanColumnCount;
|
||||
scanPlan.PathPoints = finalPathPoints;
|
||||
scanPlan.RawPathPoints = rawPathPoints;
|
||||
scanPlan.FinalPathPoints = finalPathPoints;
|
||||
scanPlan.ScanArea = scanArea;
|
||||
scanPlan.StartPoint = startPoint;
|
||||
scanPlan.EndPoint = endPoint;
|
||||
scanPlan.StepX = stepX;
|
||||
scanPlan.StepY = stepY;
|
||||
scanPlan.OverlapX = NormalizeOverlap(scanSettings.OverlapRateX);
|
||||
scanPlan.OverlapY = NormalizeOverlap(scanSettings.OverlapRateY);
|
||||
scanPlan.Adjustments = adjustments;
|
||||
scanPlan.PathGenerationMessage = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
"DiePosition: generated serpentine scan plan with {0} points ({1}x{2}), stepX={3:F3}, stepY={4:F3}, adjusted={5}.",
|
||||
scanPlan.PathPointCount,
|
||||
scanColumnCount,
|
||||
scanRowCount,
|
||||
stepX,
|
||||
stepY,
|
||||
adjustments.Count(x => x.WasAdjusted));
|
||||
scanPlan.SoftLimitAdjustedCount = adjustments.Count(x => x.WasAdjusted);
|
||||
scanPlan.AddedCoveragePointCount = 0;
|
||||
context.ScanArea = scanArea;
|
||||
return scanPlan;
|
||||
}
|
||||
|
||||
private static void ValidateCameraFieldOfView(Point cameraFieldOfView)
|
||||
{
|
||||
if (cameraFieldOfView.X <= 0d || cameraFieldOfView.Y <= 0d)
|
||||
{
|
||||
throw new InvalidOperationException("DiePosition: camera field of view must be greater than zero.");
|
||||
}
|
||||
}
|
||||
|
||||
private static WaferScanArea BuildScanArea(WaferDiePositionContext context, WaferScanSettings scanSettings, Point cameraFieldOfView)
|
||||
{
|
||||
double waferWidth = context.WaferRecipe != null && context.WaferRecipe.WaferInfo != null ? context.WaferRecipe.WaferInfo.WaferWidth : 0d;
|
||||
double waferHeight = context.WaferRecipe != null && context.WaferRecipe.WaferInfo != null ? context.WaferRecipe.WaferInfo.WaferHeight : 0d;
|
||||
double effectiveWaferWidth = Math.Max(cameraFieldOfView.X, waferWidth);
|
||||
double effectiveWaferHeight = Math.Max(cameraFieldOfView.Y, waferHeight);
|
||||
double endX = scanSettings.StartPointX + ResolveDirectionSign(scanSettings.ScanDirectionX) * Math.Max(0d, effectiveWaferWidth - cameraFieldOfView.X);
|
||||
double endY = scanSettings.StartPointY + ResolveDirectionSign(scanSettings.ScanDirectionY) * Math.Max(0d, effectiveWaferHeight - cameraFieldOfView.Y);
|
||||
|
||||
WaferScanArea scanArea = new WaferScanArea();
|
||||
scanArea.StartPoint = new Point(scanSettings.StartPointX, scanSettings.StartPointY);
|
||||
scanArea.EndPoint = new Point(endX, endY);
|
||||
scanArea.MinX = Math.Min(scanArea.StartPoint.X, scanArea.EndPoint.X);
|
||||
scanArea.MaxX = Math.Max(scanArea.StartPoint.X, scanArea.EndPoint.X);
|
||||
scanArea.MinY = Math.Min(scanArea.StartPoint.Y, scanArea.EndPoint.Y);
|
||||
scanArea.MaxY = Math.Max(scanArea.StartPoint.Y, scanArea.EndPoint.Y);
|
||||
scanArea.WaferWidth = effectiveWaferWidth;
|
||||
scanArea.WaferHeight = effectiveWaferHeight;
|
||||
return scanArea;
|
||||
}
|
||||
|
||||
private static double CalculateStep(double fov, double overlapRate)
|
||||
{
|
||||
double normalizedOverlap = NormalizeOverlap(overlapRate);
|
||||
double step = fov * (1d - normalizedOverlap);
|
||||
if (step <= 0d)
|
||||
{
|
||||
return fov;
|
||||
}
|
||||
|
||||
return step;
|
||||
}
|
||||
|
||||
private static double NormalizeOverlap(double overlapRate)
|
||||
{
|
||||
if (overlapRate < 0d)
|
||||
{
|
||||
return 0d;
|
||||
}
|
||||
|
||||
if (overlapRate >= 1d)
|
||||
{
|
||||
return 0.95d;
|
||||
}
|
||||
|
||||
return overlapRate;
|
||||
}
|
||||
|
||||
private static int CalculateScanCount(double waferSpan, double fov, double step)
|
||||
{
|
||||
if (waferSpan <= fov)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
return (int)Math.Ceiling((waferSpan - fov) / step) + 1;
|
||||
}
|
||||
|
||||
private static List<Point> BuildRawPathPoints(WaferScanArea scanArea, WaferScanSettings scanSettings, double stepX, double stepY, int scanColumnCount, int scanRowCount)
|
||||
{
|
||||
List<Point> rawPathPoints = new List<Point>();
|
||||
int directionX = ResolveDirectionSign(scanSettings.ScanDirectionX);
|
||||
int directionY = ResolveDirectionSign(scanSettings.ScanDirectionY);
|
||||
|
||||
for (int rowIndex = 0; rowIndex < scanRowCount; rowIndex++)
|
||||
{
|
||||
double y = scanArea.StartPoint.Y + rowIndex * stepY * directionY;
|
||||
bool reverseColumns = rowIndex % 2 == 1;
|
||||
for (int columnIndex = 0; columnIndex < scanColumnCount; columnIndex++)
|
||||
{
|
||||
int serpentineColumnIndex = reverseColumns ? (scanColumnCount - 1 - columnIndex) : columnIndex;
|
||||
double x = scanArea.StartPoint.X + serpentineColumnIndex * stepX * directionX;
|
||||
rawPathPoints.Add(new Point(x, y));
|
||||
}
|
||||
}
|
||||
|
||||
return rawPathPoints;
|
||||
}
|
||||
|
||||
private List<Point> BuildAdjustedPathPoints(List<Point> rawPathPoints, WaferDiePositionContext context, WaferScanSettings scanSettings, List<WaferPlannerPointAdjustment> adjustments)
|
||||
{
|
||||
List<Point> adjustedPathPoints = new List<Point>();
|
||||
foreach (Point rawPathPoint in rawPathPoints)
|
||||
{
|
||||
Point adjustedPoint = _waferMachineAdapter.AdjustPointWithinSoftLimit(
|
||||
rawPathPoint,
|
||||
context,
|
||||
scanSettings.SoftLimitOffsetMm,
|
||||
out bool adjusted,
|
||||
out string reason);
|
||||
|
||||
WaferPlannerPointAdjustment adjustment = new WaferPlannerPointAdjustment();
|
||||
adjustment.OriginalPoint = rawPathPoint;
|
||||
adjustment.AdjustedPoint = adjustedPoint;
|
||||
adjustment.WasAdjusted = adjusted;
|
||||
adjustment.Reason = reason;
|
||||
adjustments.Add(adjustment);
|
||||
adjustedPathPoints.Add(adjustedPoint);
|
||||
}
|
||||
|
||||
return adjustedPathPoints;
|
||||
}
|
||||
|
||||
private static int ResolveDirectionSign(TransPathDirection direction)
|
||||
{
|
||||
return direction == TransPathDirection.Negative ? -1 : 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user