diff --git a/README.md b/README.md
index f1d6278..4ac9fcd 100644
--- a/README.md
+++ b/README.md
@@ -109,3 +109,4 @@ dotnet build XplorePlane.sln -c Release
- [x] 打通与硬件层的调用流程
- [x] 打通与图像层的调用流程
- [ ] CNC的执行、存储逻辑的开发测试
+- [ ] 涉及到图像校准,矩阵
diff --git a/XP.Hardware.MotionControl/Implementations/SimulatedLinearAxis.cs b/XP.Hardware.MotionControl/Implementations/SimulatedLinearAxis.cs
index d2cd41b..92c4ea6 100644
--- a/XP.Hardware.MotionControl/Implementations/SimulatedLinearAxis.cs
+++ b/XP.Hardware.MotionControl/Implementations/SimulatedLinearAxis.cs
@@ -30,7 +30,7 @@ namespace XP.Hardware.MotionControl.Implementations
/// 最大位置(mm)| Maximum position (mm)
/// 原点偏移(mm)| Origin offset (mm)
/// 默认速度(mm/s)| Default speed (mm/s)
- public SimulatedLinearAxis(AxisId axisId, double min, double max, double origin, double defaultSpeed = 50.0)
+ public SimulatedLinearAxis(AxisId axisId, double min, double max, double origin, double defaultSpeed = 5.0)
: base(axisId, min, max, origin)
{
_defaultSpeed = defaultSpeed;
diff --git a/XplorePlane/Services/Cnc/CncExecutionService.cs b/XplorePlane/Services/Cnc/CncExecutionService.cs
index d8e8235..2c1bdcf 100644
--- a/XplorePlane/Services/Cnc/CncExecutionService.cs
+++ b/XplorePlane/Services/Cnc/CncExecutionService.cs
@@ -333,14 +333,18 @@ namespace XplorePlane.Services.Cnc
if (!detectorZResult.Success) return Task.FromResult(detectorZResult);
// Rotary axes: angles are already in degrees, no conversion needed
+ // 禁用轴返回失败时仅记录警告,不中断执行
var detectorSwingResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.DetectorSwing, target.DetectorSwing);
- if (!detectorSwingResult.Success) return Task.FromResult(detectorSwingResult);
+ if (!detectorSwingResult.Success)
+ _logger.ForModule().Warn("旋转轴 DetectorSwing 移动跳过:{0}", detectorSwingResult.ErrorMessage);
var stageRotationResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.StageRotation, target.StageRotation);
- if (!stageRotationResult.Success) return Task.FromResult(stageRotationResult);
+ if (!stageRotationResult.Success)
+ _logger.ForModule().Warn("旋转轴 StageRotation 移动跳过:{0}", stageRotationResult.ErrorMessage);
var fixtureRotationResult = _motionControlService.MoveRotaryToTarget(RotaryAxisId.FixtureRotation, target.FixtureRotation);
- if (!fixtureRotationResult.Success) return Task.FromResult(fixtureRotationResult);
+ if (!fixtureRotationResult.Success)
+ _logger.ForModule().Warn("旋转轴 FixtureRotation 移动跳过:{0}", fixtureRotationResult.ErrorMessage);
return Task.FromResult(MotionResult.Ok());
}
@@ -387,11 +391,17 @@ namespace XplorePlane.Services.Cnc
}
if (allIdle)
+ {
+ _logger.ForModule().Info(
+ "所有轴已到位,等待耗时 {0}ms", sw.ElapsedMilliseconds);
return true;
+ }
await Task.Delay(pollIntervalMs, ct);
}
+ _logger.ForModule().Warn(
+ "等待轴到位超时({0}ms),继续执行", timeoutMs);
return false;
}
@@ -516,6 +526,7 @@ namespace XplorePlane.Services.Cnc
}
BitmapSource resultImage = null;
+ IReadOnlyDictionary lastOutputData = null;
if (_pipelineExecutionService != null && inspectionNode.Pipeline?.Nodes?.Count > 0 && sourceImage != null)
{
try
@@ -524,6 +535,7 @@ namespace XplorePlane.Services.Cnc
var execResult = await _pipelineExecutionService.ExecutePipelineAsync(
pipelineNodes, sourceImage, null, cancellationToken);
resultImage = execResult.Image;
+ lastOutputData = execResult.LastStepOutputData;
if (resultImage != null)
{
@@ -597,10 +609,70 @@ namespace XplorePlane.Services.Cnc
}
}
- await _store.AppendNodeResultAsync(nodeResult, pipelineSnapshot: pipelineSnapshot, assets: assets);
+ await _store.AppendNodeResultAsync(nodeResult, metrics: ExtractMetrics(runId, inspectionNode, lastOutputData), pipelineSnapshot: pipelineSnapshot, assets: assets);
return resultImage;
}
+ ///
+ /// 从流水线最后一步的 OutputData 中提取检测指标(BGA空洞率、孔隙测量等)。
+ /// 写入 manifest.json 的 Metrics 部分。
+ ///
+ private static List ExtractMetrics(
+ Guid runId,
+ InspectionModuleNode inspectionNode,
+ IReadOnlyDictionary outputData)
+ {
+ var metrics = new List();
+ if (outputData == null) return metrics;
+
+ var nodeId = inspectionNode.Id;
+ int order = 0;
+
+ // ── BGA 空洞率检测指标 ──
+ if (outputData.ContainsKey("BgaVoidResult"))
+ {
+ if (outputData.TryGetValue("BgaCount", out var bgaCount))
+ metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "BgaCount", MetricName = "BGA焊球数", MetricValue = Convert.ToDouble(bgaCount), Unit = "个", IsPass = true, DisplayOrder = order++ });
+
+ if (outputData.TryGetValue("VoidRate", out var voidRate))
+ {
+ var rate = Convert.ToDouble(voidRate);
+ var limit = outputData.TryGetValue("VoidLimit", out var vl) ? Convert.ToDouble(vl) : 50.0;
+ var classification = outputData.TryGetValue("Classification", out var cls) ? cls?.ToString() : "";
+ metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "BgaVoidRate", MetricName = "BGA空洞率", MetricValue = Math.Round(rate, 2), Unit = "%", UpperLimit = limit, IsPass = classification == "PASS", DisplayOrder = order++ });
+ }
+
+ if (outputData.TryGetValue("FillRate", out var fillRate))
+ metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "BgaFillRate", MetricName = "BGA填充率", MetricValue = Math.Round(Convert.ToDouble(fillRate), 2), Unit = "%", IsPass = true, DisplayOrder = order++ });
+
+ if (outputData.TryGetValue("TotalVoidCount", out var voidCount))
+ metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "BgaTotalVoidCount", MetricName = "气泡总数", MetricValue = Convert.ToDouble(voidCount), Unit = "个", IsPass = true, DisplayOrder = order++ });
+ }
+
+ // ── 孔隙测量指标 ──
+ if (outputData.ContainsKey("VoidMeasurementResult"))
+ {
+ if (outputData.TryGetValue("VoidRate", out var voidRate))
+ {
+ var rate = Convert.ToDouble(voidRate);
+ var limit = outputData.TryGetValue("VoidLimit", out var vl) ? Convert.ToDouble(vl) : 50.0;
+ var classification = outputData.TryGetValue("Classification", out var cls) ? cls?.ToString() : "";
+ metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "VoidRate", MetricName = "空隙率", MetricValue = Math.Round(rate, 2), Unit = "%", UpperLimit = limit, IsPass = classification == "PASS", DisplayOrder = order++ });
+ }
+
+ if (outputData.TryGetValue("VoidCount", out var voidCount))
+ metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "VoidCount", MetricName = "空隙数量", MetricValue = Convert.ToDouble(voidCount), Unit = "个", IsPass = true, DisplayOrder = order++ });
+
+ if (outputData.TryGetValue("MaxVoidArea", out var maxArea))
+ metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "MaxVoidArea", MetricName = "最大空隙面积", MetricValue = Math.Round(Convert.ToDouble(maxArea), 1), Unit = "px²", IsPass = true, DisplayOrder = order++ });
+
+ if (outputData.TryGetValue("RoiArea", out var roiArea))
+ metrics.Add(new InspectionMetricResult { RunId = runId, NodeId = nodeId, MetricKey = "RoiArea", MetricName = "ROI面积", MetricValue = Math.Round(Convert.ToDouble(roiArea), 1), Unit = "px²", IsPass = true, DisplayOrder = order++ });
+ }
+
+ return metrics;
+ }
+
///
/// 将 PipelineModel(数据模型)转换为 PipelineNodeViewModel 列表(执行模型)。
/// 加载每个算子的参数定义和保存值,处理 JsonElement 类型转换。
diff --git a/XplorePlane/Services/Cnc/CncProgramService.cs b/XplorePlane/Services/Cnc/CncProgramService.cs
index d922e9b..44ee7d3 100644
--- a/XplorePlane/Services/Cnc/CncProgramService.cs
+++ b/XplorePlane/Services/Cnc/CncProgramService.cs
@@ -389,19 +389,19 @@ namespace XplorePlane.Services.Cnc
var raySource = _appStateService.RaySourceState;
return new ReferencePointNode(
id, index, $"参考点_{index}",
- StageX: motion.StageX,
- StageY: motion.StageY,
- SourceZ: motion.SourceZ,
- DetectorZ: motion.DetectorZ,
- DetectorSwing: motion.DetectorSwing,
- FDD: motion.FDD,
+ StageX: Math.Round(motion.StageX, 3),
+ StageY: Math.Round(motion.StageY, 3),
+ SourceZ: Math.Round(motion.SourceZ, 3),
+ DetectorZ: Math.Round(motion.DetectorZ, 3),
+ DetectorSwing: Math.Round(motion.DetectorSwing, 3),
+ FDD: Math.Round(motion.FDD, 3),
IsRayOn: raySource.IsOn,
- Voltage: raySource.Voltage,
- Current: TryReadCurrent(),
- StageRotation: motion.StageRotation,
- FixtureRotation: motion.FixtureRotation,
- FOD: motion.FOD,
- Magnification: motion.Magnification);
+ Voltage: Math.Round(raySource.Voltage, 3),
+ Current: Math.Round(TryReadCurrent(), 3),
+ StageRotation: Math.Round(motion.StageRotation, 3),
+ FixtureRotation: Math.Round(motion.FixtureRotation, 3),
+ FOD: Math.Round(motion.FOD, 3),
+ Magnification: Math.Round(motion.Magnification, 3));
}
/// 创建保存节点(含图像)| Create save node with image
@@ -409,7 +409,7 @@ namespace XplorePlane.Services.Cnc
{
return new SaveNodeWithImageNode(
id, index, $"保存节点_图像_{index}",
- MotionState: _appStateService.MotionState,
+ MotionState: RoundMotionState(_appStateService.MotionState),
RaySourceState: _appStateService.RaySourceState,
DetectorState: _appStateService.DetectorState,
ImageFileName: "");
@@ -420,7 +420,7 @@ namespace XplorePlane.Services.Cnc
{
return new SaveNodeNode(
id, index, $"保存节点_{index}",
- MotionState: _appStateService.MotionState,
+ MotionState: RoundMotionState(_appStateService.MotionState),
RaySourceState: _appStateService.RaySourceState,
DetectorState: _appStateService.DetectorState);
}
@@ -430,9 +430,30 @@ namespace XplorePlane.Services.Cnc
{
return new SavePositionNode(
id, index, $"检测位置_{index}",
- MotionState: _appStateService.MotionState,
+ MotionState: RoundMotionState(_appStateService.MotionState),
SaveImage: false);
}
+
+ ///
+ /// 将 MotionState 中的位置/角度值四舍五入到 3 位小数。
+ /// 避免浮点精度噪声写入 CNC 程序文件。
+ ///
+ private static MotionState RoundMotionState(MotionState s, int decimals = 3)
+ {
+ return s with
+ {
+ StageX = Math.Round(s.StageX, decimals),
+ StageY = Math.Round(s.StageY, decimals),
+ SourceZ = Math.Round(s.SourceZ, decimals),
+ DetectorZ = Math.Round(s.DetectorZ, decimals),
+ DetectorSwing = Math.Round(s.DetectorSwing, decimals),
+ FDD = Math.Round(s.FDD, decimals),
+ StageRotation = Math.Round(s.StageRotation, decimals),
+ FixtureRotation = Math.Round(s.FixtureRotation, decimals),
+ FOD = Math.Round(s.FOD, decimals),
+ Magnification = Math.Round(s.Magnification, decimals)
+ };
+ }
private double TryReadCurrent()
{
try
diff --git a/XplorePlane/Services/Cnc/ImagePersistenceService.cs b/XplorePlane/Services/Cnc/ImagePersistenceService.cs
index 13e49c6..667dbce 100644
--- a/XplorePlane/Services/Cnc/ImagePersistenceService.cs
+++ b/XplorePlane/Services/Cnc/ImagePersistenceService.cs
@@ -103,7 +103,8 @@ namespace XplorePlane.Services.Cnc
var options = new JsonSerializerOptions
{
- WriteIndented = true
+ WriteIndented = true,
+ Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping
};
var json = JsonSerializer.Serialize(summary, options);
diff --git a/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs b/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs
index fbd1588..54fb4e9 100644
--- a/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs
+++ b/XplorePlane/Services/MainViewport/DetectorFramePipelineService.cs
@@ -105,7 +105,7 @@ namespace XplorePlane.Services.MainViewport
{
EnqueueBounded(_processQueue, frame, ProcessQueueCapacity, ref _processQueueCount);
_processSignal.Release();
- _logger.Info(
+ _logger.Debug(
"[图像链路] 帧 #{N} 已入处理队列(每 {Every} 帧采样一次),累计接收 {Total} 帧",
args.FrameNumber, ProcessEveryNFrames, sequence);
}
diff --git a/XplorePlane/Views/Cnc/CncPageView.xaml b/XplorePlane/Views/Cnc/CncPageView.xaml
index 9e042a2..d16833a 100644
--- a/XplorePlane/Views/Cnc/CncPageView.xaml
+++ b/XplorePlane/Views/Cnc/CncPageView.xaml
@@ -634,43 +634,43 @@
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
@@ -739,6 +739,7 @@
+
+
diff --git a/XplorePlane/Views/Cnc/CncPageView.xaml.cs b/XplorePlane/Views/Cnc/CncPageView.xaml.cs
index 6a0230f..8481deb 100644
--- a/XplorePlane/Views/Cnc/CncPageView.xaml.cs
+++ b/XplorePlane/Views/Cnc/CncPageView.xaml.cs
@@ -169,6 +169,15 @@ namespace XplorePlane.Views.Cnc
editor.Visibility = Visibility.Collapsed;
}
+ /// 清空手动图像路径
+ private void ClearManualImagePath_Click(object sender, RoutedEventArgs e)
+ {
+ if (DataContext is CncEditorViewModel viewModel && viewModel.SelectedNode != null)
+ {
+ viewModel.SelectedNode.ManualImagePath = string.Empty;
+ }
+ }
+
private void CncTreeView_PreviewKeyDown(object sender, KeyEventArgs e)
{
if (e.Key != Key.Delete || DataContext is not CncEditorViewModel viewModel)