From e3a1184805a18c895044e1336219910778e26a3c Mon Sep 17 00:00:00 2001 From: "zhengxuan.zhang" Date: Wed, 6 May 2026 20:31:07 +0800 Subject: [PATCH] =?UTF-8?q?=E4=B8=BB=E8=A7=86=E5=8F=A3=E6=B2=A1=E6=9C=89?= =?UTF-8?q?=E5=8F=AF=E7=94=A8=E5=9B=BE=E5=83=8F=E6=97=B6=EF=BC=8C=E5=9B=9E?= =?UTF-8?q?=E9=80=80=E5=88=B0=20IAppStateService.LatestDetectorFrame?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/CncExecutionServiceTests.cs | 34 ++++++++------- .../Services/Cnc/CncExecutionService.cs | 41 ++++++++++++++----- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/XplorePlane.Tests/Services/CncExecutionServiceTests.cs b/XplorePlane.Tests/Services/CncExecutionServiceTests.cs index b1ccd00..fab0e3f 100644 --- a/XplorePlane.Tests/Services/CncExecutionServiceTests.cs +++ b/XplorePlane.Tests/Services/CncExecutionServiceTests.cs @@ -3,14 +3,18 @@ using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; +using System.Windows.Media; +using System.Windows.Media.Imaging; using FsCheck; using FsCheck.Fluent; using FsCheck.Xunit; using Moq; using XP.Common.Logging.Interfaces; +using XP.Hardware.Detector.Abstractions; using XplorePlane.Models; using XplorePlane.Services.Cnc; using XplorePlane.Services; +using XplorePlane.Services.AppState; using XplorePlane.Services.InspectionResults; using XplorePlane.Services.MainViewport; using Xunit; @@ -172,12 +176,13 @@ internal sealed class SynchronousProgress : IProgress public class CncExecutionServiceTests { - private static (CncExecutionService Service, Mock Store, Mock Logger) + private static (CncExecutionService Service, Mock Store, Mock Logger, Mock MainViewport, Mock AppState) CreateService() { var mockStore = new Mock(); var mockLogger = new Mock(); var mockMainViewportService = new Mock(); + var mockAppStateService = new Mock(); var mockPipelineExecutionService = new Mock(); var mockImageProcessingService = new Mock(); mockLogger.Setup(l => l.ForModule()).Returns(mockLogger.Object); @@ -204,9 +209,10 @@ internal sealed class SynchronousProgress : IProgress mockStore.Object, mockLogger.Object, mockMainViewportService.Object, + mockAppStateService.Object, mockPipelineExecutionService.Object, mockImageProcessingService.Object); - return (service, mockStore, mockLogger); + return (service, mockStore, mockLogger, mockMainViewportService, mockAppStateService); } // ── Property 3: 预取消立即返回 ──────────────────────────────────────── @@ -220,7 +226,7 @@ internal sealed class SynchronousProgress : IProgress CncProgramGenerators.CncProgramArb(1, 10), program => { - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); using var cts = new CancellationTokenSource(); cts.Cancel(); @@ -247,7 +253,7 @@ internal sealed class SynchronousProgress : IProgress CncProgramGenerators.CncProgramArb(2, 8), program => { - var (service, _, _) = CreateService(); + var (service, _, _, _, _) = CreateService(); var runningReports = new List(); // Use SynchronousProgress to avoid async callback timing issues @@ -304,7 +310,7 @@ internal sealed class SynchronousProgress : IProgress gen.ToArbitrary(), program => { - var (service, _, _) = CreateService(); + var (service, _, _, _, _) = CreateService(); var runningIds = new List(); var progress = new SynchronousProgress(p => @@ -355,7 +361,7 @@ internal sealed class SynchronousProgress : IProgress CncProgramGenerators.CncProgramArb(1, 8), program => { - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); InspectionRunRecord capturedRecord = null; mockStore.Setup(s => s.BeginRunAsync( @@ -386,7 +392,7 @@ internal sealed class SynchronousProgress : IProgress CncProgramGenerators.CncProgramArb(1, 10), program => { - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); service.ExecuteAsync(program, null, CancellationToken.None) .GetAwaiter().GetResult(); @@ -421,7 +427,7 @@ internal sealed class SynchronousProgress : IProgress CncProgramGenerators.CncProgramArb(1, 10), program => { - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); InspectionRunRecord capturedRecord = null; mockStore.Setup(s => s.BeginRunAsync( @@ -449,7 +455,7 @@ internal sealed class SynchronousProgress : IProgress CncProgramGenerators.CncProgramArb(1, 8), program => { - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); bool? capturedOverallPass = default; bool callbackInvoked = false; @@ -482,7 +488,7 @@ internal sealed class SynchronousProgress : IProgress // Validates: Requirements 4.4, 4.5 public void CompleteRunAsync_CalledWithNullOverallPass_WhenCancelled() { - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); // Use a WaitDelayNode with long delay so cancellation happens during execution var waitNode = new WaitDelayNode(Guid.NewGuid(), 0, "LongWait", 5000); @@ -530,7 +536,7 @@ internal sealed class SynchronousProgress : IProgress CncProgramGenerators.CncProgramWithInspectionNodesArb(2), program => { - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); // Make AppendNodeResultAsync always throw mockStore.Setup(s => s.AppendNodeResultAsync( @@ -587,7 +593,7 @@ internal sealed class SynchronousProgress : IProgress gen.ToArbitrary(), waitNode => { - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); bool? capturedOverallPass = default; mockStore.Setup(s => s.CompleteRunAsync( @@ -630,7 +636,7 @@ internal sealed class SynchronousProgress : IProgress CncProgramGenerators.CncProgramArb(1, 8), program => { - var (service, _, _) = CreateService(); + var (service, _, _, _, _) = CreateService(); // Build a map of NodeId → CncNodeViewModel var nodeVms = program.Nodes @@ -700,7 +706,7 @@ internal sealed class SynchronousProgress : IProgress tuple => { var (program, node, expectedPipelineName) = tuple; - var (service, mockStore, _) = CreateService(); + var (service, mockStore, _, _, _) = CreateService(); PipelineExecutionSnapshot capturedSnapshot = null; mockStore.Setup(s => s.AppendNodeResultAsync( diff --git a/XplorePlane/Services/Cnc/CncExecutionService.cs b/XplorePlane/Services/Cnc/CncExecutionService.cs index da359e1..002c72e 100644 --- a/XplorePlane/Services/Cnc/CncExecutionService.cs +++ b/XplorePlane/Services/Cnc/CncExecutionService.cs @@ -6,8 +6,10 @@ using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media.Imaging; +using XP.Common.Converters; using XP.Common.Logging.Interfaces; using XplorePlane.Models; +using XplorePlane.Services.AppState; using XplorePlane.Services.InspectionResults; using XplorePlane.Services.MainViewport; using XplorePlane.ViewModels; @@ -22,6 +24,7 @@ namespace XplorePlane.Services.Cnc private readonly IInspectionResultStore _store; private readonly ILoggerService _logger; private readonly IMainViewportService _mainViewportService; + private readonly IAppStateService _appStateService; private readonly IPipelineExecutionService _pipelineExecutionService; private readonly IImageProcessingService _imageProcessingService; @@ -29,12 +32,14 @@ namespace XplorePlane.Services.Cnc IInspectionResultStore store, ILoggerService logger, IMainViewportService mainViewportService, + IAppStateService appStateService, IPipelineExecutionService pipelineExecutionService, IImageProcessingService imageProcessingService) { _store = store ?? throw new ArgumentNullException(nameof(store)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _mainViewportService = mainViewportService; + _appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService)); _pipelineExecutionService = pipelineExecutionService; _imageProcessingService = imageProcessingService; } @@ -46,10 +51,7 @@ namespace XplorePlane.Services.Cnc return; int inspectionNodeCount = program.Nodes.OfType().Count(); - - // 获取当前源图像(用于 run/source.bmp) - var sourceImage = _mainViewportService?.LatestManualImage as BitmapSource - ?? _mainViewportService?.CurrentDisplayImage as BitmapSource; + var sourceImage = TryGetSourceImage(); Guid runId; try @@ -105,7 +107,7 @@ namespace XplorePlane.Services.Cnc { case ReferencePointNode rp: _logger.ForModule().Info( - "执行参考点节点 [{Index}] {Name} | " + + "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} " + @@ -119,7 +121,7 @@ namespace XplorePlane.Services.Cnc case SavePositionNode sp: _logger.ForModule().Info( - "执行保存位置节点 [{Index}] {Name} | " + + "Executing save-position node [{Index}] {Name} | " + "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + "StageRotation={StageRotation} FixtureRotation={FixtureRotation}", @@ -133,7 +135,7 @@ namespace XplorePlane.Services.Cnc case SaveNodeNode sn: _logger.ForModule().Info( - "执行保存节点 [{Index}] {Name} | " + + "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", @@ -147,7 +149,7 @@ namespace XplorePlane.Services.Cnc case SaveNodeWithImageNode sni: _logger.ForModule().Info( - "执行保存节点(图像) [{Index}] {Name} | " + + "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}", @@ -218,7 +220,7 @@ namespace XplorePlane.Services.Cnc break; } - // InspectionModuleNode 完成时携带结果图像,供 ViewModel 缓存到节点上 + // 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)); @@ -242,6 +244,25 @@ namespace XplorePlane.Services.Cnc } } + private BitmapSource TryGetSourceImage() + { + var viewportImage = _mainViewportService?.LatestManualImage as BitmapSource + ?? _mainViewportService?.CurrentDisplayImage as BitmapSource; + if (viewportImage != null) + return viewportImage; + + var detectorFrame = _appStateService?.LatestDetectorFrame; + if (detectorFrame?.ImageData == null || detectorFrame.Width <= 0 || detectorFrame.Height <= 0) + return null; + + var bitmap = ImageConverter.ConvertGray16ToBitmapSource( + detectorFrame.ImageData, + (int)detectorFrame.Width, + (int)detectorFrame.Height); + bitmap.Freeze(); + return bitmap; + } + private async Task ExecuteInspectionNodeAsync( Guid runId, InspectionModuleNode inspectionNode, @@ -304,7 +325,7 @@ namespace XplorePlane.Services.Cnc Height = resultImage.PixelHeight }); nodeResult.Status = InspectionNodeStatus.Succeeded; - _mainViewportService?.SetManualImage(resultImage, $"CNC节点:{inspectionNode.Name}"); + _mainViewportService?.SetManualImage(resultImage, $"CNC Node: {inspectionNode.Name}"); } } catch (Exception ex)