From 84c1c5f16db591819b57edc247f5580aecf16329 Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Tue, 26 May 2026 16:02:34 +0800 Subject: [PATCH] =?UTF-8?q?CNC=20=E6=A3=80=E6=B5=8B=E6=A8=A1=E5=9D=97?= =?UTF-8?q?=E8=BE=93=E5=87=BA=E7=9A=84=20=E6=95=B0=E5=80=BC=E7=B1=BB?= =?UTF-8?q?=E5=9E=8B=E5=A6=82=20bga=E6=A3=80=E6=B5=8B=E5=92=8C=20=E5=AD=94?= =?UTF-8?q?=E9=9A=99=E6=A3=80=E6=B5=8B=E7=9A=84=E8=BE=93=E5=87=BA=E5=86=85?= =?UTF-8?q?=E5=AE=B9=EF=BC=8C=E8=A6=81=E5=86=99=E5=85=A5=E5=88=B0=20CNC=20?= =?UTF-8?q?=E6=A3=80=E6=B5=8B=E7=BB=93=E6=9E=9C=20manifest.json=20?= =?UTF-8?q?=E5=90=88=E9=80=82=E7=9A=84=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + .../Implementations/SimulatedLinearAxis.cs | 2 +- .../Services/Cnc/CncExecutionService.cs | 80 ++++++++++++++++++- XplorePlane/Services/Cnc/CncProgramService.cs | 51 ++++++++---- .../Services/Cnc/ImagePersistenceService.cs | 3 +- .../DetectorFramePipelineService.cs | 2 +- XplorePlane/Views/Cnc/CncPageView.xaml | 38 ++++++--- XplorePlane/Views/Cnc/CncPageView.xaml.cs | 9 +++ 8 files changed, 153 insertions(+), 33 deletions(-) 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 @@ + +