调试CNC执行
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<CncNodeExecutionProgress> 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<CncNodeExecutionProgress>` | 进度回调(通知 UI 更新节点状态) |
|
||||
| `cancellationToken` | `CancellationToken` | 外部取消令牌(用户点击停止) |
|
||||
|
||||
**关键行为:**
|
||||
- 使用 `LinkedCancellationTokenSource` 将外部取消和探测器断连事件合并
|
||||
- 每个节点执行前检查取消状态
|
||||
- 失败节点不中断整体执行(容错设计),仅标记为 Failed 并继续
|
||||
|
||||
---
|
||||
|
||||
### `MoveToPositionAsync` — 运动控制
|
||||
|
||||
```csharp
|
||||
private Task<MotionResult> MoveToPositionAsync(MotionState target, CancellationToken ct)
|
||||
```
|
||||
|
||||
- 将 `MotionState` 中的微米(μm)坐标转换为毫米(mm)后发送给运动控制服务
|
||||
- 依次移动:StageX → StageY → SourceZ → DetectorZ → DetectorSwing → StageRotation → FixtureRotation
|
||||
- 任一轴移动失败立即返回错误
|
||||
- 运动服务不可用时返回成功(优雅降级)
|
||||
|
||||
---
|
||||
|
||||
### `WaitForAxesSettledAsync` — 等待轴到位
|
||||
|
||||
```csharp
|
||||
private async Task<bool> 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<BitmapSource> 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<PipelineNodeViewModel> BuildPipelineNodeViewModels(PipelineModel pipeline)
|
||||
```
|
||||
|
||||
- 将 `PipelineModel.Nodes`(数据模型)转换为 `PipelineNodeViewModel`(执行模型)
|
||||
- 加载每个算子的参数定义和保存值
|
||||
- 处理 JSON 参数值的类型转换(`JsonElement` → `int/double/bool/string`)
|
||||
|
||||
---
|
||||
|
||||
### `ExecuteWaitDelayWithProgressAsync` — 延时等待
|
||||
|
||||
```csharp
|
||||
private static async Task ExecuteWaitDelayWithProgressAsync(
|
||||
WaitDelayNode waitNode, IProgress<CncNodeExecutionProgress> progress, CancellationToken ct)
|
||||
```
|
||||
|
||||
- 每 50ms 报告一次进度百分比
|
||||
- 支持取消(抛出 `OperationCanceledException`)
|
||||
|
||||
---
|
||||
|
||||
## 取消机制
|
||||
|
||||
| 取消源 | 触发方式 | 处理 |
|
||||
|--------|----------|------|
|
||||
| 用户点击停止 | `CancellationToken` 从 ViewModel 传入 | 各节点执行前检查 |
|
||||
| 探测器断连 | `DetectorDisconnectedEvent` → `_executionCts.Cancel()` | 通过 LinkedCTS 传播 |
|
||||
| 运动失败 | `MoveToPositionAsync` 返回失败 | 标记节点 Failed,继续下一个 |
|
||||
|
||||
---
|
||||
|
||||
## 进度报告
|
||||
|
||||
通过 `IProgress<CncNodeExecutionProgress>` 回调通知 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()
|
||||
```
|
||||
@@ -26,23 +26,27 @@ using XplorePlane.ViewModels;
|
||||
namespace XplorePlane.Services.Cnc
|
||||
{
|
||||
/// <summary>
|
||||
/// CNC 程序执行服务。
|
||||
/// 按节点顺序执行整个检测程序,协调运动控制、图像采集、流水线处理和结果归档。
|
||||
/// Executes a CNC program node-by-node, reporting progress and persisting inspection results.
|
||||
/// </summary>
|
||||
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) { }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CNC 程序主执行入口。
|
||||
/// 按节点顺序执行:先处理所有 SavePositionNode(多位置循环),再处理其余节点。
|
||||
/// 支持取消(用户停止 + 探测器断连)、进度报告和容错。
|
||||
/// </summary>
|
||||
public async Task ExecuteAsync(CncProgram program, IProgress<CncNodeExecutionProgress> 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<PositionResult>();
|
||||
|
||||
// Task 5.1: Get all SavePositionNodes ordered by Index ascending for multi-position execution
|
||||
var savePositionNodes = program.Nodes
|
||||
.OfType<SavePositionNode>()
|
||||
.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<CncExecutionService>().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<CncExecutionService>().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) ──
|
||||
try
|
||||
{
|
||||
switch (node)
|
||||
{
|
||||
// ── 保存位置节点:运动 → 采集 → 保存图像 ──
|
||||
case SavePositionNode sp:
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().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)
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().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;
|
||||
nodeSucceeded = false;
|
||||
positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Failed", ErrorMessage = moveResult.ErrorMessage });
|
||||
break;
|
||||
}
|
||||
await WaitForAxesSettledAsync(linkedCts.Token);
|
||||
|
||||
// Wait for all axes to settle
|
||||
var settled = await WaitForAxesSettledAsync(linkedCts.Token);
|
||||
if (!settled)
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().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))
|
||||
{
|
||||
// Task 5.2: ManualImagePath is set - validate and load from file
|
||||
var validationResult = ManualImageValidator.Validate(sp.ManualImagePath);
|
||||
|
||||
if (validationResult == ManualImageValidationResult.Valid)
|
||||
{
|
||||
var vr = ManualImageValidator.Validate(sp.ManualImagePath);
|
||||
if (vr == ManualImageValidationResult.Valid)
|
||||
positionImage = LoadImageFromFile(sp.ManualImagePath);
|
||||
if (positionImage != null)
|
||||
{
|
||||
currentSourceImage = positionImage;
|
||||
_logger.ForModule<CncExecutionService>().Info(
|
||||
"Loaded manual image from '{0}' for node '{1}'",
|
||||
sp.ManualImagePath, sp.Name);
|
||||
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<CncExecutionService>().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;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Validation failed - show error dialog, mark Failed, continue
|
||||
var errorMessage = validationResult switch
|
||||
{
|
||||
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}"
|
||||
};
|
||||
|
||||
_logger.ForModule<CncExecutionService>().Warn(
|
||||
"Image acquisition failed for node '{0}' at index {1}: manual image validation failed ({2})",
|
||||
sp.Name, positionIndex, validationResult);
|
||||
|
||||
await Application.Current.Dispatcher.InvokeAsync(() =>
|
||||
MessageBox.Show(errorMessage, "\u624b\u52a8\u56fe\u50cf\u52a0\u8f7d\u5931\u8d25", MessageBoxButton.OK, MessageBoxImage.Error));
|
||||
|
||||
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<CncExecutionService>().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;
|
||||
if (positionImage == null) { nodeSucceeded = false; positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Failed", ErrorMessage = "探测器无有效帧" }); break; }
|
||||
}
|
||||
currentSourceImage = positionImage;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Unexpected exception during image acquisition - mark Failed, continue
|
||||
_logger.ForModule<CncExecutionService>().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;
|
||||
}
|
||||
|
||||
// ── Step 2: Image Persistence (with error tolerance - Task 5.4) ──
|
||||
if (sp.SaveImage && positionImage != null)
|
||||
if (sp.SaveImage)
|
||||
{
|
||||
try
|
||||
{
|
||||
var imageBytes = EncodeBitmapToBmp(positionImage);
|
||||
var saveResult = await _imagePersistenceService.SaveImageAsync(
|
||||
imageBytes, sp.Name, program.Name, linkedCts.Token);
|
||||
|
||||
if (saveResult.Success)
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().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;
|
||||
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
|
||||
{
|
||||
// Save returned failure - log error, mark failed, but continue with pipeline
|
||||
_logger.ForModule<CncExecutionService>().Error(
|
||||
new InvalidOperationException(saveResult.ErrorMessage),
|
||||
"Image save failed for node '{0}': {1}",
|
||||
sp.Name, saveResult.ErrorMessage);
|
||||
nodeSucceeded = false;
|
||||
nodeErrorMessage = saveResult.ErrorMessage;
|
||||
positionResults.Add(new PositionResult { NodeName = sp.Name, NodeIndex = sp.Index, Status = "Success" });
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Exception during save - log error, mark failed, but continue with pipeline
|
||||
_logger.ForModule<CncExecutionService>().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
|
||||
break;
|
||||
}
|
||||
|
||||
// ── 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)
|
||||
// ── 检测模块节点:使用当前源图像执行流水线 ──
|
||||
case InspectionModuleNode inspectionNode:
|
||||
{
|
||||
if (currentSourceImage == null) { nodeSucceeded = false; break; }
|
||||
try
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().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;
|
||||
var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, currentSourceImage, linkedCts.Token);
|
||||
if (img != null) lastResultImage = img;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Pipeline execution exception - log error, mark failed, continue
|
||||
_logger.ForModule<CncExecutionService>().Error(ex,
|
||||
"Pipeline execution failed for node '{0}' at index {1}: {2}",
|
||||
sp.Name, positionIndex, ex.Message);
|
||||
_logger.ForModule<CncExecutionService>().Error(ex, "检测模块 '{0}' 执行失败", inspectionNode.Name);
|
||||
nodeSucceeded = false;
|
||||
nodeErrorMessage = $"Pipeline execution failed: {ex.Message}";
|
||||
}
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
// ── Step 4: Report final state ──
|
||||
var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed;
|
||||
progress?.Report(new CncNodeExecutionProgress(sp.Id, finalState,
|
||||
PositionIndex: positionIndex, TotalPositions: totalPositions));
|
||||
case ReferencePointNode rp:
|
||||
_logger.ForModule<CncExecutionService>().Info("执行参考点 [{Index}] {Name}", rp.Index, rp.Name);
|
||||
break;
|
||||
|
||||
// Task 5.5: Track position result
|
||||
positionResults.Add(new PositionResult
|
||||
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 CompleteProgramNode:
|
||||
progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Succeeded));
|
||||
goto endLoop;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
NodeName = sp.Name,
|
||||
NodeIndex = sp.Index,
|
||||
Status = nodeSucceeded ? "Success" : "Failed",
|
||||
ErrorMessage = nodeErrorMessage,
|
||||
ImagePath = savedImagePath
|
||||
});
|
||||
|
||||
if (!nodeSucceeded)
|
||||
allSucceeded = false;
|
||||
_logger.ForModule<CncExecutionService>().Error(ex, "节点 '{0}' 执行异常", node.Name);
|
||||
if (linkedCts.Token.IsCancellationRequested) cancelled = true;
|
||||
else nodeSucceeded = false;
|
||||
}
|
||||
|
||||
// ── Task 5.5: Build BatchCaptureResult and write summary ──
|
||||
var wasCancelled = cancelled;
|
||||
if (cancelled) { progress?.Report(new CncNodeExecutionProgress(node.Id, NodeExecutionState.Failed)); break; }
|
||||
|
||||
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:
|
||||
|
||||
// ── 写入批次结果摘要 ──
|
||||
int totalPositions = program.Nodes.OfType<SavePositionNode>().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<CncExecutionService>().Error(
|
||||
null,
|
||||
"Failed to write batch capture summary for program '{0}'",
|
||||
program.Name);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().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<CncExecutionService>().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<CncExecutionService>().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<CncExecutionService>().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<CncExecutionService>().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<CncExecutionService>().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<CncExecutionService>().Error(ex, "写入批次摘要失败"); }
|
||||
|
||||
bool? overallPass = cancelled ? null : (bool?)allSucceeded;
|
||||
|
||||
try
|
||||
{
|
||||
await _store.CompleteRunAsync(runId, overallPass);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.ForModule<CncExecutionService>().Error(ex,
|
||||
"Failed to complete inspection run '{0}'", runId);
|
||||
}
|
||||
try { await _store.CompleteRunAsync(runId, overallPass); }
|
||||
catch (Exception ex) { _logger.ForModule<CncExecutionService>().Error(ex, "完成检测运行记录失败"); }
|
||||
} // end try
|
||||
finally
|
||||
{
|
||||
@@ -631,9 +309,10 @@ namespace XplorePlane.Services.Cnc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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。
|
||||
/// 旋转轴单位为度(°),无需转换。
|
||||
/// 运动服务不可用时返回成功(优雅降级,适用于仿真模式)。
|
||||
/// </summary>
|
||||
private Task<MotionResult> MoveToPositionAsync(MotionState target, CancellationToken ct)
|
||||
{
|
||||
@@ -667,9 +346,9 @@ namespace XplorePlane.Services.Cnc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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(优雅降级)。
|
||||
/// </summary>
|
||||
private async Task<bool> WaitForAxesSettledAsync(CancellationToken ct)
|
||||
{
|
||||
@@ -716,6 +395,11 @@ namespace XplorePlane.Services.Cnc
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取当前可用的源图像。
|
||||
/// 优先级:手动加载图像 > 主视口显示图像 > 探测器原始帧(Gray16 转 BitmapSource)。
|
||||
/// 所有源均不可用时返回 null。
|
||||
/// </summary>
|
||||
private BitmapSource TryGetSourceImage()
|
||||
{
|
||||
// ── 优先级 1:MainViewportService 中的手动图像或当前显示图像 ──
|
||||
@@ -765,7 +449,7 @@ namespace XplorePlane.Services.Cnc
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Loads a BitmapSource from a local file path. Returns null if loading fails.
|
||||
/// 从本地文件加载图像。加载失败返回 null。
|
||||
/// </summary>
|
||||
private static BitmapSource LoadImageFromFile(string filePath)
|
||||
{
|
||||
@@ -785,6 +469,10 @@ namespace XplorePlane.Services.Cnc
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行检测模块节点:运行图像处理流水线,保存输入/输出图像,归档节点结果。
|
||||
/// 返回流水线输出图像(用于 UI 显示和后续节点),执行失败时返回 null。
|
||||
/// </summary>
|
||||
private async Task<BitmapSource> ExecuteInspectionNodeAsync(
|
||||
Guid runId,
|
||||
InspectionModuleNode inspectionNode,
|
||||
@@ -913,6 +601,10 @@ namespace XplorePlane.Services.Cnc
|
||||
return resultImage;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 PipelineModel(数据模型)转换为 PipelineNodeViewModel 列表(执行模型)。
|
||||
/// 加载每个算子的参数定义和保存值,处理 JsonElement 类型转换。
|
||||
/// </summary>
|
||||
private System.Collections.Generic.IEnumerable<PipelineNodeViewModel> BuildPipelineNodeViewModels(PipelineModel pipeline)
|
||||
{
|
||||
var nodes = new System.Collections.Generic.List<PipelineNodeViewModel>();
|
||||
@@ -947,6 +639,10 @@ namespace XplorePlane.Services.Cnc
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 JsonElement 转换为目标类型(int/double/bool/string)。
|
||||
/// 用于反序列化流水线参数值。
|
||||
/// </summary>
|
||||
private static object ConvertSavedValue(object savedValue, Type targetType)
|
||||
{
|
||||
if (savedValue is not JsonElement jsonElement)
|
||||
@@ -966,6 +662,9 @@ namespace XplorePlane.Services.Cnc
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 将 BitmapSource 编码为 BMP 格式字节数组(用于持久化存储)。
|
||||
/// </summary>
|
||||
private static byte[] EncodeBitmapToBmp(BitmapSource bitmap)
|
||||
{
|
||||
using var ms = new MemoryStream();
|
||||
@@ -975,6 +674,10 @@ namespace XplorePlane.Services.Cnc
|
||||
return ms.ToArray();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行延时等待节点,每 50ms 报告一次进度百分比。
|
||||
/// 支持取消(抛出 OperationCanceledException)。
|
||||
/// </summary>
|
||||
private static async Task ExecuteWaitDelayWithProgressAsync(
|
||||
WaitDelayNode waitNode,
|
||||
IProgress<CncNodeExecutionProgress> progress,
|
||||
|
||||
Reference in New Issue
Block a user