CNC 检测模块输出的 数值类型如 bga检测和 孔隙检测的输出内容,要写入到 CNC 检测结果 manifest.json 合适的地址
This commit is contained in:
@@ -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 类型转换。
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user