510 lines
22 KiB
C#
510 lines
22 KiB
C#
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 轴。");
|
||
}
|
||
}
|
||
}
|
||
} |