CNC 检测模块输出的 数值类型如 bga检测和 孔隙检测的输出内容,要写入到 CNC 检测结果 manifest.json 合适的地址

This commit is contained in:
zhengxuan.zhang
2026-05-26 16:02:34 +08:00
parent cdd0db95ff
commit 84c1c5f16d
8 changed files with 153 additions and 33 deletions
+1
View File
@@ -109,3 +109,4 @@ dotnet build XplorePlane.sln -c Release
- [x] 打通与硬件层的调用流程
- [x] 打通与图像层的调用流程
- [ ] CNC的执行、存储逻辑的开发测试
- [ ] 涉及到图像校准,矩阵
@@ -30,7 +30,7 @@ namespace XP.Hardware.MotionControl.Implementations
/// <param name="max">最大位置(mm| Maximum position (mm)</param>
/// <param name="origin">原点偏移(mm| Origin offset (mm)</param>
/// <param name="defaultSpeed">默认速度(mm/s| Default speed (mm/s)</param>
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;
@@ -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<CncExecutionService>().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<CncExecutionService>().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<CncExecutionService>().Warn("旋转轴 FixtureRotation 移动跳过:{0}", fixtureRotationResult.ErrorMessage);
return Task.FromResult(MotionResult.Ok());
}
@@ -387,11 +391,17 @@ namespace XplorePlane.Services.Cnc
}
if (allIdle)
{
_logger.ForModule<CncExecutionService>().Info(
"所有轴已到位,等待耗时 {0}ms", sw.ElapsedMilliseconds);
return true;
}
await Task.Delay(pollIntervalMs, ct);
}
_logger.ForModule<CncExecutionService>().Warn(
"等待轴到位超时({0}ms),继续执行", timeoutMs);
return false;
}
@@ -516,6 +526,7 @@ namespace XplorePlane.Services.Cnc
}
BitmapSource resultImage = null;
IReadOnlyDictionary<string, object> 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;
}
/// <summary>
/// 从流水线最后一步的 OutputData 中提取检测指标(BGA空洞率、孔隙测量等)。
/// 写入 manifest.json 的 Metrics 部分。
/// </summary>
private static List<InspectionMetricResult> ExtractMetrics(
Guid runId,
InspectionModuleNode inspectionNode,
IReadOnlyDictionary<string, object> outputData)
{
var metrics = new List<InspectionMetricResult>();
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;
}
/// <summary>
/// 将 PipelineModel(数据模型)转换为 PipelineNodeViewModel 列表(执行模型)。
/// 加载每个算子的参数定义和保存值,处理 JsonElement 类型转换。
+36 -15
View File
@@ -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));
}
/// <summary>创建保存节点(含图像)| Create save node with image</summary>
@@ -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);
}
/// <summary>
/// 将 MotionState 中的位置/角度值四舍五入到 3 位小数。
/// 避免浮点精度噪声写入 CNC 程序文件。
/// </summary>
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
@@ -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);
@@ -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);
}
+27 -11
View File
@@ -634,43 +634,43 @@
<UniformGrid Margin="8,8,8,6" Columns="2">
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台 X (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageX, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageX, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台 Y (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageY, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageY, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="射线源 Z (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.SourceZ, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.SourceZ, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="探测器 Z (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorZ, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorZ, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="探测器摆动 (°)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorSwing, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.DetectorSwing, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="载物台旋转 (°)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageRotation, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.StageRotation, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="夹具旋转 (°)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FixtureRotation, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FixtureRotation, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="FOD (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FOD, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FOD, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel Margin="0,0,6,0">
<TextBlock Style="{StaticResource LabelStyle}" Text="FDD (μm)" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FDD, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.FDD, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
<StackPanel>
<TextBlock Style="{StaticResource LabelStyle}" Text="放大倍率" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Magnification, UpdateSourceTrigger=LostFocus}" />
<TextBox Style="{StaticResource EditorBox}" Text="{Binding SelectedNode.Magnification, UpdateSourceTrigger=LostFocus, StringFormat=F3}" />
</StackPanel>
</UniformGrid>
</GroupBox>
@@ -739,6 +739,7 @@
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox
Grid.Column="0"
@@ -751,7 +752,7 @@
Grid.Column="1"
Height="28"
Width="32"
Margin="0,0,0,8"
Margin="0,0,2,8"
Padding="0"
Content="…"
FontSize="13"
@@ -761,6 +762,21 @@
BorderBrush="#CFCFCF"
BorderThickness="1"
Cursor="Hand" />
<Button
Grid.Column="2"
Height="28"
Width="28"
Margin="0,0,0,8"
Padding="0"
Content="×"
FontSize="14"
Foreground="#E05050"
ToolTip="清空手动图像路径"
Click="ClearManualImagePath_Click"
Background="#F8F8F8"
BorderBrush="#CFCFCF"
BorderThickness="1"
Cursor="Hand" />
</Grid>
</StackPanel>
</GroupBox>
@@ -169,6 +169,15 @@ namespace XplorePlane.Views.Cnc
editor.Visibility = Visibility.Collapsed;
}
/// <summary>清空手动图像路径</summary>
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)