From cdd0db95ff478972009c8c6c9a8bbba5e13e341f Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Tue, 26 May 2026 13:18:29 +0800 Subject: [PATCH] =?UTF-8?q?=E8=B0=83=E8=AF=95CNC=E6=89=A7=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Implementations/SimulatedDetector.cs | 2 +- XplorePlane/Doc/CNC执行机制说明.md | 269 ++++++++ .../Services/Cnc/CncExecutionService.cs | 607 +++++------------- 3 files changed, 425 insertions(+), 453 deletions(-) create mode 100644 XplorePlane/Doc/CNC执行机制说明.md diff --git a/XP.Hardware.Detector/Implementations/SimulatedDetector.cs b/XP.Hardware.Detector/Implementations/SimulatedDetector.cs index 5da1122..33c5bc8 100644 --- a/XP.Hardware.Detector/Implementations/SimulatedDetector.cs +++ b/XP.Hardware.Detector/Implementations/SimulatedDetector.cs @@ -163,7 +163,7 @@ namespace XP.Hardware.Detector.Implementations ExposureTime = 0 }; - _logger?.Info("[SimulatedDetector] 发布合成帧 #{N},分辨率 {W}x{H}", n, w, h); + //_logger?.Info("[SimulatedDetector] 发布合成帧 #{N},分辨率 {W}x{H}", n, w, h); PublishImageCaptured(args); } diff --git a/XplorePlane/Doc/CNC执行机制说明.md b/XplorePlane/Doc/CNC执行机制说明.md new file mode 100644 index 0000000..5ce71a8 --- /dev/null +++ b/XplorePlane/Doc/CNC执行机制说明.md @@ -0,0 +1,269 @@ +# CNC 执行机制说明 + +## 概述 + +`CncExecutionService` 是 CNC 程序的核心执行引擎,负责按节点顺序执行整个检测程序。它协调运动控制、图像采集、流水线处理和结果归档等子系统,实现自动化多位置检测流程。 + +--- + +## 类定义 + +| 类 | 文件 | 职责 | +|----|------|------| +| `CncExecutionService` | `Services/Cnc/CncExecutionService.cs` | CNC 程序执行引擎 | +| `ICncExecutionService` | `Services/Cnc/ICncExecutionService.cs` | 执行服务接口 | +| `CncEditorViewModel` | `ViewModels/Cnc/CncEditorViewModel.cs` | 调用执行服务的 ViewModel | + +--- + +## 依赖注入 + +`CncExecutionService` 通过构造函数注入以下服务: + +| 依赖 | 接口 | 用途 | +|------|------|------| +| 检测结果存储 | `IInspectionResultStore` | 归档检测运行记录和节点结果 | +| 日志 | `ILoggerService` | 结构化日志记录 | +| 主视口 | `IMainViewportService` | 获取实时图像、推送结果图像 | +| 应用状态 | `IAppStateService` | 获取探测器帧数据 | +| 流水线执行 | `IPipelineExecutionService` | 执行图像处理流水线 | +| 图像处理 | `IImageProcessingService` | 获取算子定义和参数 | +| 事件聚合器 | `IEventAggregator` | 订阅探测器断连事件 | +| 图像持久化 | `IImagePersistenceService` | 保存采集图像到磁盘 | +| 运动控制 | `IMotionControlService`(可选) | 控制各轴移动到目标位置 | +| 运动系统 | `IMotionSystem`(可选) | 查询轴状态(是否到位) | +| 射线源 | `IRaySourceService`(可选) | 射线源控制 | + +--- + +## 执行流程 + +### 入口方法 + +```csharp +Task ExecuteAsync(CncProgram program, IProgress progress, CancellationToken cancellationToken) +``` + +### 执行阶段 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 1. 初始化阶段 │ +│ - 创建 LinkedCancellationTokenSource(支持探测器断连取消) │ +│ - 获取初始源图像 │ +│ - 调用 _store.BeginRunAsync() 创建检测运行记录 │ +├─────────────────────────────────────────────────────────────┤ +│ 2. 多位置执行循环(SavePositionNode) │ +│ 对每个 SavePositionNode 按顺序执行: │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ Step 0: 运动到目标位置 (MoveToPositionAsync) │ │ +│ │ Step 1: 图像采集(探测器实时帧 或 手动图像文件) │ │ +│ │ Step 2: 图像保存(如果 SaveImage=true) │ │ +│ │ Step 3: 流水线执行(如果下一节点是 InspectionModule) │ │ +│ │ Step 4: 报告节点执行状态 │ │ +│ └─────────────────────────────────────────────────────┘ │ +├─────────────────────────────────────────────────────────────┤ +│ 3. 批次结果汇总 │ +│ - 构建 BatchCaptureResult │ +│ - 调用 _imagePersistenceService.WriteSummaryAsync() │ +├─────────────────────────────────────────────────────────────┤ +│ 4. 非位置节点执行 │ +│ 按 Index 顺序处理剩余节点: │ +│ - ReferencePointNode: 记录参考点参数 │ +│ - SaveNodeNode / SaveNodeWithImageNode: 记录设备状态 │ +│ - WaitDelayNode: 延时等待(带进度报告) │ +│ - PauseDialogNode: 弹出暂停对话框 │ +│ - InspectionModuleNode: 执行图像处理流水线 │ +│ - CompleteProgramNode: 标记程序完成,退出循环 │ +├─────────────────────────────────────────────────────────────┤ +│ 5. 完成阶段 │ +│ - 调用 _store.CompleteRunAsync() 标记运行结束 │ +│ - 清理 CancellationTokenSource │ +│ - 通知 MainViewportService CNC 执行结束 │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## 核心方法说明 + +### `ExecuteAsync` — 主执行入口 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `program` | `CncProgram` | 待执行的 CNC 程序(含所有节点) | +| `progress` | `IProgress` | 进度回调(通知 UI 更新节点状态) | +| `cancellationToken` | `CancellationToken` | 外部取消令牌(用户点击停止) | + +**关键行为:** +- 使用 `LinkedCancellationTokenSource` 将外部取消和探测器断连事件合并 +- 每个节点执行前检查取消状态 +- 失败节点不中断整体执行(容错设计),仅标记为 Failed 并继续 + +--- + +### `MoveToPositionAsync` — 运动控制 + +```csharp +private Task MoveToPositionAsync(MotionState target, CancellationToken ct) +``` + +- 将 `MotionState` 中的微米(μm)坐标转换为毫米(mm)后发送给运动控制服务 +- 依次移动:StageX → StageY → SourceZ → DetectorZ → DetectorSwing → StageRotation → FixtureRotation +- 任一轴移动失败立即返回错误 +- 运动服务不可用时返回成功(优雅降级) + +--- + +### `WaitForAxesSettledAsync` — 等待轴到位 + +```csharp +private async Task WaitForAxesSettledAsync(CancellationToken ct) +``` + +- 每 50ms 轮询所有线性轴和旋转轴的状态 +- 所有轴 `Status == Idle` 时返回 `true` +- 超时 30 秒返回 `false`(不中断执行,继续后续步骤) + +--- + +### `TryGetSourceImage` — 图像获取 + +```csharp +private BitmapSource TryGetSourceImage() +``` + +**图像源优先级:** +1. `MainViewportService.LatestManualImage`(用户手动加载的图像) +2. `MainViewportService.CurrentDisplayImage`(当前显示的实时图像) +3. `AppStateService.LatestDetectorFrame`(原始探测器帧,Gray16 → BitmapSource) + +--- + +### `ExecuteInspectionNodeAsync` — 检测模块执行 + +```csharp +private async Task ExecuteInspectionNodeAsync( + Guid runId, InspectionModuleNode inspectionNode, BitmapSource sourceImage, CancellationToken ct) +``` + +**执行步骤:** +1. 创建 `InspectionNodeResult` 记录 +2. 序列化 Pipeline 定义为 JSON 快照 +3. 保存输入图像为资产 +4. 调用 `BuildPipelineNodeViewModels()` 构建流水线 ViewModel +5. 调用 `_pipelineExecutionService.ExecutePipelineAsync()` 执行流水线 +6. 推送结果图像到主视口 +7. 推送检测叠加层数据(轮廓、标注) +8. 合成背景图 + 叠加层,保存结果截图 +9. 调用 `_store.AppendNodeResultAsync()` 归档节点结果 + +--- + +### `BuildPipelineNodeViewModels` — 构建流水线 ViewModel + +```csharp +private IEnumerable BuildPipelineNodeViewModels(PipelineModel pipeline) +``` + +- 将 `PipelineModel.Nodes`(数据模型)转换为 `PipelineNodeViewModel`(执行模型) +- 加载每个算子的参数定义和保存值 +- 处理 JSON 参数值的类型转换(`JsonElement` → `int/double/bool/string`) + +--- + +### `ExecuteWaitDelayWithProgressAsync` — 延时等待 + +```csharp +private static async Task ExecuteWaitDelayWithProgressAsync( + WaitDelayNode waitNode, IProgress progress, CancellationToken ct) +``` + +- 每 50ms 报告一次进度百分比 +- 支持取消(抛出 `OperationCanceledException`) + +--- + +## 取消机制 + +| 取消源 | 触发方式 | 处理 | +|--------|----------|------| +| 用户点击停止 | `CancellationToken` 从 ViewModel 传入 | 各节点执行前检查 | +| 探测器断连 | `DetectorDisconnectedEvent` → `_executionCts.Cancel()` | 通过 LinkedCTS 传播 | +| 运动失败 | `MoveToPositionAsync` 返回失败 | 标记节点 Failed,继续下一个 | + +--- + +## 进度报告 + +通过 `IProgress` 回调通知 UI: + +```csharp +public record CncNodeExecutionProgress( + Guid NodeId, + NodeExecutionState State, + double? ProgressPercent = null, + BitmapSource ResultImage = null, + int? PositionIndex = null, + int? TotalPositions = null); +``` + +ViewModel 收到回调后更新 `CncNodeViewModel.ExecutionState`,触发树形节点的颜色变化。 + +--- + +## 结果归档 + +| 方法 | 时机 | 存储内容 | +|------|------|----------| +| `_store.BeginRunAsync()` | 执行开始 | 运行记录 + 源图像 | +| `_store.AppendNodeResultAsync()` | 每个检测模块执行后 | 节点结果 + Pipeline 快照 + 输入/输出图像 | +| `_store.CompleteRunAsync()` | 执行结束 | 标记运行完成/失败 | +| `_imagePersistenceService.WriteSummaryAsync()` | 多位置循环结束 | 批次结果 JSON 摘要 | + +--- + +## 容错设计 + +- **图像采集失败**:标记节点 Failed,跳过该位置,继续下一个 +- **图像保存失败**:标记节点 Failed,但仍继续执行流水线 +- **流水线执行异常**:捕获异常,标记 Failed,不中断整体执行 +- **运动服务不可用**:返回成功(优雅降级,适用于仿真模式) +- **探测器断连**:通过事件取消整个执行 + +--- + +## 调用关系图 + +``` +CncEditorViewModel + │ + ├── RunCncCommand → ExecuteRunAsync() + │ │ + │ └── CncExecutionService.ExecuteAsync() + │ │ + │ ├── IInspectionResultStore.BeginRunAsync() + │ │ + │ ├── [循环] SavePositionNode + │ │ ├── MoveToPositionAsync() → IMotionControlService + │ │ ├── WaitForAxesSettledAsync() → IMotionSystem + │ │ ├── TryGetSourceImage() → IMainViewportService / IAppStateService + │ │ ├── IImagePersistenceService.SaveImageAsync() + │ │ └── ExecuteInspectionNodeAsync() + │ │ ├── BuildPipelineNodeViewModels() + │ │ ├── IPipelineExecutionService.ExecutePipelineAsync() + │ │ ├── IMainViewportService.SetCncResultImage() + │ │ ├── IMainViewportService.PushDetectionOverlay() + │ │ └── IInspectionResultStore.AppendNodeResultAsync() + │ │ + │ ├── [循环] 非位置节点 + │ │ ├── WaitDelayNode → ExecuteWaitDelayWithProgressAsync() + │ │ ├── PauseDialogNode → MessageBox + │ │ ├── InspectionModuleNode → ExecuteInspectionNodeAsync() + │ │ └── CompleteProgramNode → 退出 + │ │ + │ ├── IImagePersistenceService.WriteSummaryAsync() + │ └── IInspectionResultStore.CompleteRunAsync() + │ + └── StopCncCommand → CancellationTokenSource.Cancel() +``` diff --git a/XplorePlane/Services/Cnc/CncExecutionService.cs b/XplorePlane/Services/Cnc/CncExecutionService.cs index 66aea22..d8e8235 100644 --- a/XplorePlane/Services/Cnc/CncExecutionService.cs +++ b/XplorePlane/Services/Cnc/CncExecutionService.cs @@ -26,23 +26,27 @@ using XplorePlane.ViewModels; namespace XplorePlane.Services.Cnc { /// + /// CNC 程序执行服务。 + /// 按节点顺序执行整个检测程序,协调运动控制、图像采集、流水线处理和结果归档。 /// Executes a CNC program node-by-node, reporting progress and persisting inspection results. /// public class CncExecutionService : ICncExecutionService { - private readonly IInspectionResultStore _store; - private readonly ILoggerService _logger; - private readonly IMainViewportService _mainViewportService; - private readonly IAppStateService _appStateService; - private readonly IPipelineExecutionService _pipelineExecutionService; - private readonly IImageProcessingService _imageProcessingService; - private readonly IEventAggregator _eventAggregator; - private readonly IImagePersistenceService _imagePersistenceService; - private readonly IMotionControlService _motionControlService; - private readonly IMotionSystem _motionSystem; - private readonly IRaySourceService _raySourceService; + // ── 依赖服务 ────────────────────────────────────────────────────── + private readonly IInspectionResultStore _store; // 检测结果归档存储 + private readonly ILoggerService _logger; // 日志服务 + private readonly IMainViewportService _mainViewportService; // 主视口(获取/推送图像) + private readonly IAppStateService _appStateService; // 应用状态(探测器帧) + private readonly IPipelineExecutionService _pipelineExecutionService; // 图像处理流水线执行 + private readonly IImageProcessingService _imageProcessingService; // 算子注册表 + private readonly IEventAggregator _eventAggregator; // 事件总线 + private readonly IImagePersistenceService _imagePersistenceService; // 图像文件持久化 + private readonly IMotionControlService _motionControlService; // 运动控制(可选) + private readonly IMotionSystem _motionSystem; // 运动系统状态查询(可选) + private readonly IRaySourceService _raySourceService; // 射线源控制(可选) - // Task 4.2: volatile field so reads/writes are not reordered across threads + // 当前执行的取消令牌源(volatile 保证跨线程可见性) + // 探测器断连事件通过此字段取消正在执行的程序 private volatile CancellationTokenSource _executionCts; public CncExecutionService( @@ -75,7 +79,7 @@ namespace XplorePlane.Services.Cnc .Subscribe(OnDetectorDisconnected, ThreadOption.BackgroundThread); } - // Task 4.3: callback – cancel the running execution when the detector disconnects + // 探测器断连回调:取消当前正在执行的 CNC 程序 private void OnDetectorDisconnected() { var cts = _executionCts; @@ -88,9 +92,14 @@ namespace XplorePlane.Services.Cnc catch (ObjectDisposedException) { } } + /// + /// CNC 程序主执行入口。 + /// 按节点顺序执行:先处理所有 SavePositionNode(多位置循环),再处理其余节点。 + /// 支持取消(用户停止 + 探测器断连)、进度报告和容错。 + /// public async Task ExecuteAsync(CncProgram program, IProgress progress, CancellationToken cancellationToken) { - // Task 4.4: create a linked CTS so DetectorDisconnectedEvent can cancel independently + // 创建联合取消令牌:外部取消 + 探测器断连事件均可触发 using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); _executionCts = linkedCts; _mainViewportService?.SetCncRunning(true); @@ -139,311 +148,137 @@ namespace XplorePlane.Services.Cnc bool cancelled = false; bool allSucceeded = true; BitmapSource lastResultImage = null; - - // Task 5.5: Record start time for batch result summary var startTime = DateTime.UtcNow; var positionResults = new List(); - // Task 5.1: Get all SavePositionNodes ordered by Index ascending for multi-position execution - var savePositionNodes = program.Nodes - .OfType() - .OrderBy(n => n.Index) - .ToList(); - int totalPositions = savePositionNodes.Count; + // ══════════════════════════════════════════════════════════════ + // 单循环:按 Index 顺序逐个执行所有节点 + // ══════════════════════════════════════════════════════════════ + var allNodes = program.Nodes.OrderBy(n => n.Index).ToList(); - // Task 5.1: Multi-position execution loop with progress reporting and cancellation - for (int positionIndex = 0; positionIndex < totalPositions; positionIndex++) + foreach (var node in allNodes) { - // Task 5.1: Check CancellationToken at the start of each iteration if (linkedCts.Token.IsCancellationRequested) { cancelled = true; _motionControlService?.StopAll(); - _logger.ForModule().Info( - "Multi-position execution cancelled at position {0}/{1}", - positionIndex, totalPositions); break; } - var sp = savePositionNodes[positionIndex]; - - // Task 5.1: Report progress (current 0-based index, total count) - progress?.Report(new CncNodeExecutionProgress( - sp.Id, - NodeExecutionState.Running, - ProgressPercent: 0, - PositionIndex: positionIndex, - TotalPositions: totalPositions)); - - _logger.ForModule().Info( - "Executing save-position node [{Index}] {Name} (position {Current}/{Total}) | " + - "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + - "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + - "StageRotation={StageRotation} FixtureRotation={FixtureRotation} SaveImage={SaveImage}", - sp.Index, sp.Name, positionIndex + 1, totalPositions, - sp.MotionState.StageX, sp.MotionState.StageY, - sp.MotionState.SourceZ, sp.MotionState.DetectorZ, - sp.MotionState.DetectorSwing, sp.MotionState.FDD, - sp.MotionState.FOD, sp.MotionState.Magnification, - sp.MotionState.StageRotation, sp.MotionState.FixtureRotation, - sp.SaveImage); - + progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running, ProgressPercent: 0)); bool nodeSucceeded = true; - string savedImagePath = null; - string nodeErrorMessage = null; - - // ── Step 0: Move to target position (motion integration) ── - var moveResult = await MoveToPositionAsync(sp.MotionState, linkedCts.Token); - if (!moveResult.Success) - { - _logger.ForModule().Warn( - "Motion move failed for node '{0}' at index {1}: {2}", - sp.Name, positionIndex, moveResult.ErrorMessage); - progress?.Report(new CncNodeExecutionProgress( - sp.Id, NodeExecutionState.Failed, - PositionIndex: positionIndex, TotalPositions: totalPositions)); - allSucceeded = false; - positionResults.Add(new PositionResult - { - NodeName = sp.Name, - NodeIndex = sp.Index, - Status = "Failed", - ErrorMessage = $"Motion failed: {moveResult.ErrorMessage}" - }); - continue; - } - - // Wait for all axes to settle - var settled = await WaitForAxesSettledAsync(linkedCts.Token); - if (!settled) - { - _logger.ForModule().Warn( - "Axes did not settle within timeout for node '{0}' at index {1}", - sp.Name, positionIndex); - // Continue anyway - axes may be close enough - } - - // ── Step 1: Image Acquisition (with error tolerance - Task 5.4) ── - BitmapSource positionImage = null; try { - if (!string.IsNullOrEmpty(sp.ManualImagePath)) + switch (node) { - // Task 5.2: ManualImagePath is set - validate and load from file - var validationResult = ManualImageValidator.Validate(sp.ManualImagePath); - - if (validationResult == ManualImageValidationResult.Valid) + // ── 保存位置节点:运动 → 采集 → 保存图像 ── + case SavePositionNode sp: { - positionImage = LoadImageFromFile(sp.ManualImagePath); - if (positionImage != null) + _logger.ForModule().Info( + "执行位置节点 [{Index}] {Name} | StageX={StageX} StageY={StageY}", + sp.Index, sp.Name, sp.MotionState.StageX, sp.MotionState.StageY); + + var moveResult = await MoveToPositionAsync(sp.MotionState, linkedCts.Token); + if (!moveResult.Success) { - currentSourceImage = positionImage; - _logger.ForModule().Info( - "Loaded manual image from '{0}' for node '{1}'", - sp.ManualImagePath, sp.Name); + nodeSucceeded = false; + positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Failed", ErrorMessage = moveResult.ErrorMessage }); + break; + } + await WaitForAxesSettledAsync(linkedCts.Token); + + BitmapSource positionImage = null; + if (!string.IsNullOrEmpty(sp.ManualImagePath)) + { + var vr = ManualImageValidator.Validate(sp.ManualImagePath); + if (vr == ManualImageValidationResult.Valid) + positionImage = LoadImageFromFile(sp.ManualImagePath); + if (positionImage == null) { nodeSucceeded = false; positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Failed", ErrorMessage = "手动图像加载失败" }); break; } } else { - // Manual image file could not be decoded - mark Failed, continue - _logger.ForModule().Warn( - "Image acquisition failed for node '{0}' at index {1}: manual image file could not be decoded", - sp.Name, positionIndex); - progress?.Report(new CncNodeExecutionProgress( - sp.Id, NodeExecutionState.Failed, - PositionIndex: positionIndex, TotalPositions: totalPositions)); - allSucceeded = false; - // Task 5.5: Track failed position result - positionResults.Add(new PositionResult - { - NodeName = sp.Name, - NodeIndex = sp.Index, - Status = "Failed", - ErrorMessage = "Manual image file could not be decoded" - }); - continue; + positionImage = TryGetSourceImage(); + if (positionImage == null) { nodeSucceeded = false; positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Failed", ErrorMessage = "探测器无有效帧" }); break; } } - } - else - { - // Validation failed - show error dialog, mark Failed, continue - var errorMessage = validationResult switch + currentSourceImage = positionImage; + + if (sp.SaveImage) { - ManualImageValidationResult.PathTooLong => - $"\u56fe\u50cf\u8def\u5f84\u8fc7\u957f\uff08\u8d85\u8fc7260\u5b57\u7b26\uff09\uff1a\n{sp.ManualImagePath}", - ManualImageValidationResult.FileNotFound => - $"\u56fe\u50cf\u6587\u4ef6\u4e0d\u5b58\u5728\uff1a\n{sp.ManualImagePath}", - ManualImageValidationResult.UnsupportedFormat => - $"\u4e0d\u652f\u6301\u7684\u56fe\u50cf\u683c\u5f0f\uff08\u4ec5\u652f\u6301 BMP\u3001PNG\u3001TIFF\uff09\uff1a\n{sp.ManualImagePath}", - _ => $"\u56fe\u50cf\u8def\u5f84\u65e0\u6548\uff1a\n{sp.ManualImagePath}" - }; + try + { + var bytes = EncodeBitmapToBmp(positionImage); + var sr = await _imagePersistenceService.SaveImageAsync(bytes, sp.Name, program.Name, linkedCts.Token); + positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Success", ImagePath = sr.Success ? sr.FilePath : null }); + } + catch { positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Success" }); } + } + else + { + positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Success" }); + } + break; + } - _logger.ForModule().Warn( - "Image acquisition failed for node '{0}' at index {1}: manual image validation failed ({2})", - sp.Name, positionIndex, validationResult); + // ── 检测模块节点:使用当前源图像执行流水线 ── + case InspectionModuleNode inspectionNode: + { + if (currentSourceImage == null) { nodeSucceeded = false; break; } + try + { + var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, currentSourceImage, linkedCts.Token); + if (img != null) lastResultImage = img; + } + catch (Exception ex) + { + _logger.ForModule().Error(ex, "检测模块 '{0}' 执行失败", inspectionNode.Name); + nodeSucceeded = false; + } + break; + } + case ReferencePointNode rp: + _logger.ForModule().Info("执行参考点 [{Index}] {Name}", rp.Index, rp.Name); + break; + + case WaitDelayNode waitNode: + try { await ExecuteWaitDelayWithProgressAsync(waitNode, progress, linkedCts.Token); } + catch (OperationCanceledException) { cancelled = true; } + break; + + case PauseDialogNode pauseNode: await Application.Current.Dispatcher.InvokeAsync(() => - MessageBox.Show(errorMessage, "\u624b\u52a8\u56fe\u50cf\u52a0\u8f7d\u5931\u8d25", MessageBoxButton.OK, MessageBoxImage.Error)); + MessageBox.Show(pauseNode.DialogMessage, pauseNode.DialogTitle)); + if (linkedCts.Token.IsCancellationRequested) cancelled = true; + break; - progress?.Report(new CncNodeExecutionProgress( - sp.Id, NodeExecutionState.Failed, - PositionIndex: positionIndex, TotalPositions: totalPositions)); - allSucceeded = false; - // Task 5.5: Track failed position result - positionResults.Add(new PositionResult - { - NodeName = sp.Name, - NodeIndex = sp.Index, - Status = "Failed", - ErrorMessage = $"Manual image validation failed: {validationResult}" - }); - continue; - } - } - else - { - // ManualImagePath is empty - use detector acquisition (default) - positionImage = TryGetSourceImage(); - if (positionImage == null) - { - // Detector returned null - mark Failed, continue - _logger.ForModule().Warn( - "Image acquisition failed for node '{0}' at index {1}: detector returned no valid image frame", - sp.Name, positionIndex); - progress?.Report(new CncNodeExecutionProgress( - sp.Id, NodeExecutionState.Failed, - PositionIndex: positionIndex, TotalPositions: totalPositions)); - allSucceeded = false; - // Task 5.5: Track failed position result - positionResults.Add(new PositionResult - { - NodeName = sp.Name, - NodeIndex = sp.Index, - Status = "Failed", - ErrorMessage = "Detector returned no valid image frame" - }); - continue; - } - currentSourceImage = positionImage; + case CompleteProgramNode: + progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Succeeded)); + goto endLoop; + + default: + break; } } catch (Exception ex) { - // Unexpected exception during image acquisition - mark Failed, continue - _logger.ForModule().Warn( - "Image acquisition failed for node '{0}' at index {1}: {2}", - sp.Name, positionIndex, ex.Message); - progress?.Report(new CncNodeExecutionProgress( - sp.Id, NodeExecutionState.Failed, - PositionIndex: positionIndex, TotalPositions: totalPositions)); - allSucceeded = false; - // Task 5.5: Track failed position result - positionResults.Add(new PositionResult - { - NodeName = sp.Name, - NodeIndex = sp.Index, - Status = "Failed", - ErrorMessage = ex.Message - }); - continue; + _logger.ForModule().Error(ex, "节点 '{0}' 执行异常", node.Name); + if (linkedCts.Token.IsCancellationRequested) cancelled = true; + else nodeSucceeded = false; } - // ── Step 2: Image Persistence (with error tolerance - Task 5.4) ── - if (sp.SaveImage && positionImage != null) - { - try - { - var imageBytes = EncodeBitmapToBmp(positionImage); - var saveResult = await _imagePersistenceService.SaveImageAsync( - imageBytes, sp.Name, program.Name, linkedCts.Token); + if (cancelled) { progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Failed)); break; } - if (saveResult.Success) - { - _logger.ForModule().Info( - "Image saved for node '{0}': Path={1}, Size={2} bytes", - sp.Name, saveResult.FilePath, saveResult.FileSizeBytes); - // Task 5.5: Track saved image path - savedImagePath = saveResult.FilePath; - } - else - { - // Save returned failure - log error, mark failed, but continue with pipeline - _logger.ForModule().Error( - new InvalidOperationException(saveResult.ErrorMessage), - "Image save failed for node '{0}': {1}", - sp.Name, saveResult.ErrorMessage); - nodeSucceeded = false; - nodeErrorMessage = saveResult.ErrorMessage; - } - } - catch (Exception ex) - { - // Exception during save - log error, mark failed, but continue with pipeline - _logger.ForModule().Error(ex, - "Image save failed for node '{0}': {1}", - sp.Name, ex.Message); - nodeSucceeded = false; - nodeErrorMessage = ex.Message; - } - // Note: image save failure does NOT prevent pipeline execution - } - - // ── Step 3: Pipeline Execution (with error tolerance - Task 5.4) ── - // Check if the next node (by Index order) is an InspectionModuleNode - if (positionImage != null) - { - var allNodesOrdered = program.Nodes.OrderBy(n => n.Index).ToList(); - int currentNodeOrderIndex = allNodesOrdered.FindIndex(n => n.Id == sp.Id); - if (currentNodeOrderIndex >= 0 && currentNodeOrderIndex + 1 < allNodesOrdered.Count) - { - var nextNode = allNodesOrdered[currentNodeOrderIndex + 1]; - if (nextNode is InspectionModuleNode inspectionNode) - { - try - { - _logger.ForModule().Info( - "Passing captured image from node '{0}' to inspection module '{1}'", - sp.Name, inspectionNode.Name); - - var resultImage = await ExecuteInspectionNodeAsync( - runId, inspectionNode, positionImage, linkedCts.Token); - if (resultImage != null) - lastResultImage = resultImage; - } - catch (Exception ex) - { - // Pipeline execution exception - log error, mark failed, continue - _logger.ForModule().Error(ex, - "Pipeline execution failed for node '{0}' at index {1}: {2}", - sp.Name, positionIndex, ex.Message); - nodeSucceeded = false; - nodeErrorMessage = $"Pipeline execution failed: {ex.Message}"; - } - } - } - } - - // ── Step 4: Report final state ── + var nodeResultImage = node is InspectionModuleNode ? lastResultImage : null; var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed; - progress?.Report(new CncNodeExecutionProgress(sp.Id, finalState, - PositionIndex: positionIndex, TotalPositions: totalPositions)); - - // Task 5.5: Track position result - positionResults.Add(new PositionResult - { - NodeName = sp.Name, - NodeIndex = sp.Index, - Status = nodeSucceeded ? "Success" : "Failed", - ErrorMessage = nodeErrorMessage, - ImagePath = savedImagePath - }); - - if (!nodeSucceeded) - allSucceeded = false; + progress?.Report(new CncNodeExecutionProgress(node.Id, finalState, nodeResultImage)); + if (!nodeSucceeded) allSucceeded = false; } - // ── Task 5.5: Build BatchCaptureResult and write summary ── - var wasCancelled = cancelled; + endLoop: + + // ── 写入批次结果摘要 ── + int totalPositions = program.Nodes.OfType().Count(); var batchResult = new BatchCaptureResult { ProgramName = program.Name, @@ -453,175 +288,18 @@ namespace XplorePlane.Services.Cnc SucceededPositions = positionResults.Count(r => r.Status == "Success"), FailedPositions = positionResults.Count(r => r.Status == "Failed"), SavedImageCount = positionResults.Count(r => r.ImagePath != null), - Status = wasCancelled ? "Cancelled" : "Completed", - CompletedBeforeCancel = wasCancelled ? positionResults.Count : null, - NotExecutedAfterCancel = wasCancelled ? totalPositions - positionResults.Count : null, + Status = cancelled ? "Cancelled" : "Completed", + CompletedBeforeCancel = cancelled ? positionResults.Count : null, + NotExecutedAfterCancel = cancelled ? totalPositions - positionResults.Count : null, Positions = positionResults }; - try - { - var summaryWritten = await _imagePersistenceService.WriteSummaryAsync( - batchResult, program.Name, CancellationToken.None); - if (!summaryWritten) - { - _logger.ForModule().Error( - null, - "Failed to write batch capture summary for program '{0}'", - program.Name); - } - } - catch (Exception ex) - { - _logger.ForModule().Error(ex, - "Failed to write batch capture summary for program '{0}': {1}", - program.Name, ex.Message); - } - - // Process remaining non-SavePosition nodes in order - foreach (var node in program.Nodes.OrderBy(n => n.Index)) - { - if (linkedCts.Token.IsCancellationRequested) - { - cancelled = true; - break; - } - - // Skip SavePositionNodes - already processed in multi-position loop above - if (node is SavePositionNode) - continue; - - progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Running, ProgressPercent: 0)); - - bool nodeSucceeded = true; - - try - { - switch (node) - { - case ReferencePointNode rp: - _logger.ForModule().Info( - "Executing reference point node [{Index}] {Name} | " + - "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + - "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + - "StageRotation={StageRotation} FixtureRotation={FixtureRotation} " + - "RayOn={RayOn} Voltage={Voltage}kV Current={Current}uA", - rp.Index, rp.Name, - rp.StageX, rp.StageY, rp.SourceZ, rp.DetectorZ, - rp.DetectorSwing, rp.FDD, rp.FOD, rp.Magnification, - rp.StageRotation, rp.FixtureRotation, - rp.IsRayOn, rp.Voltage, rp.Current); - break; - - case SaveNodeNode sn: - _logger.ForModule().Info( - "Executing save node [{Index}] {Name} | " + - "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + - "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + - "RayOn={RayOn} Voltage={Voltage}kV Power={Power}W", - sn.Index, sn.Name, - sn.MotionState.StageX, sn.MotionState.StageY, - sn.MotionState.SourceZ, sn.MotionState.DetectorZ, - sn.MotionState.DetectorSwing, sn.MotionState.FDD, - sn.MotionState.FOD, sn.MotionState.Magnification, - sn.RaySourceState.IsOn, sn.RaySourceState.Voltage, sn.RaySourceState.Power); - break; - - case SaveNodeWithImageNode sni: - _logger.ForModule().Info( - "Executing save-with-image node [{Index}] {Name} | " + - "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + - "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + - "RayOn={RayOn} Voltage={Voltage}kV Power={Power}W ImageFile={ImageFile}", - sni.Index, sni.Name, - sni.MotionState.StageX, sni.MotionState.StageY, - sni.MotionState.SourceZ, sni.MotionState.DetectorZ, - sni.MotionState.DetectorSwing, sni.MotionState.FDD, - sni.MotionState.FOD, sni.MotionState.Magnification, - sni.RaySourceState.IsOn, sni.RaySourceState.Voltage, sni.RaySourceState.Power, - sni.ImageFileName); - break; - - case WaitDelayNode waitNode: - try - { - await ExecuteWaitDelayWithProgressAsync(waitNode, progress, linkedCts.Token); - } - catch (OperationCanceledException) - { - cancelled = true; - } - break; - - case PauseDialogNode pauseNode: - await Application.Current.Dispatcher.InvokeAsync(() => - MessageBox.Show(pauseNode.DialogMessage, pauseNode.DialogTitle)); - if (linkedCts.Token.IsCancellationRequested) - cancelled = true; - break; - - case InspectionModuleNode inspectionNode: - try - { - var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, currentSourceImage, linkedCts.Token); - if (img != null) lastResultImage = img; - } - catch (Exception ex) - { - _logger.ForModule().Error(ex, - "Failed to append node result for node '{0}' (Id={1})", inspectionNode.Name, inspectionNode.Id); - nodeSucceeded = false; - } - break; - - case CompleteProgramNode: - // Report Succeeded before terminating the loop - progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Succeeded)); - goto endLoop; - - default: - // Unknown node types are treated as succeeded - break; - } - } - catch (Exception ex) - { - _logger.ForModule().Error(ex, - "Unexpected error executing node '{0}' (Id={1})", node.Name, node.Id); - if (linkedCts.Token.IsCancellationRequested) - cancelled = true; - else - nodeSucceeded = false; - } - - if (cancelled) - { - progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Failed)); - break; - } - - // Carry the latest inspection result image so the ViewModel can cache it on the node. - var nodeResultImage = node is InspectionModuleNode ? lastResultImage : null; - var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed; - progress?.Report(new CncNodeExecutionProgress(node.Id, finalState, nodeResultImage)); - - if (!nodeSucceeded) - allSucceeded = false; - } - - endLoop: + try { await _imagePersistenceService.WriteSummaryAsync(batchResult, program.Name, CancellationToken.None); } + catch (Exception ex) { _logger.ForModule().Error(ex, "写入批次摘要失败"); } bool? overallPass = cancelled ? null : (bool?)allSucceeded; - - try - { - await _store.CompleteRunAsync(runId, overallPass); - } - catch (Exception ex) - { - _logger.ForModule().Error(ex, - "Failed to complete inspection run '{0}'", runId); - } + try { await _store.CompleteRunAsync(runId, overallPass); } + catch (Exception ex) { _logger.ForModule().Error(ex, "完成检测运行记录失败"); } } // end try finally { @@ -631,9 +309,10 @@ namespace XplorePlane.Services.Cnc } /// - /// Moves all axes to the target position specified by the MotionState. - /// MotionState positions are in micrometers (μm); ILinearAxis uses millimeters (mm). - /// Returns MotionResult.Ok() if motion service is not available (graceful degradation). + /// 将所有轴移动到目标位置。 + /// MotionState 中的坐标单位为微米(μm),运动控制接口使用毫米(mm),需除以 1000。 + /// 旋转轴单位为度(°),无需转换。 + /// 运动服务不可用时返回成功(优雅降级,适用于仿真模式)。 /// private Task MoveToPositionAsync(MotionState target, CancellationToken ct) { @@ -667,9 +346,9 @@ namespace XplorePlane.Services.Cnc } /// - /// Polls all axes until they are settled (Status == Idle). - /// Returns true when all axes are idle, false on timeout (30s). - /// Returns true immediately if motion system is not available (graceful degradation). + /// 轮询所有轴状态,等待全部到位(Status == Idle)。 + /// 每 50ms 检查一次,超时 30 秒返回 false。 + /// 运动系统不可用时直接返回 true(优雅降级)。 /// private async Task WaitForAxesSettledAsync(CancellationToken ct) { @@ -716,6 +395,11 @@ namespace XplorePlane.Services.Cnc return false; } + /// + /// 获取当前可用的源图像。 + /// 优先级:手动加载图像 > 主视口显示图像 > 探测器原始帧(Gray16 转 BitmapSource)。 + /// 所有源均不可用时返回 null。 + /// private BitmapSource TryGetSourceImage() { // ── 优先级 1:MainViewportService 中的手动图像或当前显示图像 ── @@ -765,7 +449,7 @@ namespace XplorePlane.Services.Cnc } /// - /// Loads a BitmapSource from a local file path. Returns null if loading fails. + /// 从本地文件加载图像。加载失败返回 null。 /// private static BitmapSource LoadImageFromFile(string filePath) { @@ -785,6 +469,10 @@ namespace XplorePlane.Services.Cnc } } + /// + /// 执行检测模块节点:运行图像处理流水线,保存输入/输出图像,归档节点结果。 + /// 返回流水线输出图像(用于 UI 显示和后续节点),执行失败时返回 null。 + /// private async Task ExecuteInspectionNodeAsync( Guid runId, InspectionModuleNode inspectionNode, @@ -913,6 +601,10 @@ namespace XplorePlane.Services.Cnc return resultImage; } + /// + /// 将 PipelineModel(数据模型)转换为 PipelineNodeViewModel 列表(执行模型)。 + /// 加载每个算子的参数定义和保存值,处理 JsonElement 类型转换。 + /// private System.Collections.Generic.IEnumerable BuildPipelineNodeViewModels(PipelineModel pipeline) { var nodes = new System.Collections.Generic.List(); @@ -947,6 +639,10 @@ namespace XplorePlane.Services.Cnc return nodes; } + /// + /// 将 JsonElement 转换为目标类型(int/double/bool/string)。 + /// 用于反序列化流水线参数值。 + /// private static object ConvertSavedValue(object savedValue, Type targetType) { if (savedValue is not JsonElement jsonElement) @@ -966,6 +662,9 @@ namespace XplorePlane.Services.Cnc } } + /// + /// 将 BitmapSource 编码为 BMP 格式字节数组(用于持久化存储)。 + /// private static byte[] EncodeBitmapToBmp(BitmapSource bitmap) { using var ms = new MemoryStream(); @@ -975,6 +674,10 @@ namespace XplorePlane.Services.Cnc return ms.ToArray(); } + /// + /// 执行延时等待节点,每 50ms 报告一次进度百分比。 + /// 支持取消(抛出 OperationCanceledException)。 + /// private static async Task ExecuteWaitDelayWithProgressAsync( WaitDelayNode waitNode, IProgress progress,