主视口没有可用图像时,回退到 IAppStateService.LatestDetectorFrame

This commit is contained in:
zhengxuan.zhang
2026-05-06 20:31:07 +08:00
parent b740f8d453
commit e3a1184805
2 changed files with 51 additions and 24 deletions
@@ -3,14 +3,18 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using FsCheck; using FsCheck;
using FsCheck.Fluent; using FsCheck.Fluent;
using FsCheck.Xunit; using FsCheck.Xunit;
using Moq; using Moq;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.Cnc; using XplorePlane.Services.Cnc;
using XplorePlane.Services; using XplorePlane.Services;
using XplorePlane.Services.AppState;
using XplorePlane.Services.InspectionResults; using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.MainViewport; using XplorePlane.Services.MainViewport;
using Xunit; using Xunit;
@@ -172,12 +176,13 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
public class CncExecutionServiceTests public class CncExecutionServiceTests
{ {
private static (CncExecutionService Service, Mock<IInspectionResultStore> Store, Mock<ILoggerService> Logger) private static (CncExecutionService Service, Mock<IInspectionResultStore> Store, Mock<ILoggerService> Logger, Mock<IMainViewportService> MainViewport, Mock<IAppStateService> AppState)
CreateService() CreateService()
{ {
var mockStore = new Mock<IInspectionResultStore>(); var mockStore = new Mock<IInspectionResultStore>();
var mockLogger = new Mock<ILoggerService>(); var mockLogger = new Mock<ILoggerService>();
var mockMainViewportService = new Mock<IMainViewportService>(); var mockMainViewportService = new Mock<IMainViewportService>();
var mockAppStateService = new Mock<IAppStateService>();
var mockPipelineExecutionService = new Mock<IPipelineExecutionService>(); var mockPipelineExecutionService = new Mock<IPipelineExecutionService>();
var mockImageProcessingService = new Mock<IImageProcessingService>(); var mockImageProcessingService = new Mock<IImageProcessingService>();
mockLogger.Setup(l => l.ForModule<CncExecutionService>()).Returns(mockLogger.Object); mockLogger.Setup(l => l.ForModule<CncExecutionService>()).Returns(mockLogger.Object);
@@ -204,9 +209,10 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
mockStore.Object, mockStore.Object,
mockLogger.Object, mockLogger.Object,
mockMainViewportService.Object, mockMainViewportService.Object,
mockAppStateService.Object,
mockPipelineExecutionService.Object, mockPipelineExecutionService.Object,
mockImageProcessingService.Object); mockImageProcessingService.Object);
return (service, mockStore, mockLogger); return (service, mockStore, mockLogger, mockMainViewportService, mockAppStateService);
} }
// ── Property 3: 预取消立即返回 ──────────────────────────────────────── // ── Property 3: 预取消立即返回 ────────────────────────────────────────
@@ -220,7 +226,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 10), CncProgramGenerators.CncProgramArb(1, 10),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
using var cts = new CancellationTokenSource(); using var cts = new CancellationTokenSource();
cts.Cancel(); cts.Cancel();
@@ -247,7 +253,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(2, 8), CncProgramGenerators.CncProgramArb(2, 8),
program => program =>
{ {
var (service, _, _) = CreateService(); var (service, _, _, _, _) = CreateService();
var runningReports = new List<Guid>(); var runningReports = new List<Guid>();
// Use SynchronousProgress to avoid async callback timing issues // Use SynchronousProgress to avoid async callback timing issues
@@ -304,7 +310,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
gen.ToArbitrary(), gen.ToArbitrary(),
program => program =>
{ {
var (service, _, _) = CreateService(); var (service, _, _, _, _) = CreateService();
var runningIds = new List<Guid>(); var runningIds = new List<Guid>();
var progress = new SynchronousProgress<CncNodeExecutionProgress>(p => var progress = new SynchronousProgress<CncNodeExecutionProgress>(p =>
@@ -355,7 +361,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 8), CncProgramGenerators.CncProgramArb(1, 8),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
InspectionRunRecord capturedRecord = null; InspectionRunRecord capturedRecord = null;
mockStore.Setup(s => s.BeginRunAsync( mockStore.Setup(s => s.BeginRunAsync(
@@ -386,7 +392,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 10), CncProgramGenerators.CncProgramArb(1, 10),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
service.ExecuteAsync(program, null, CancellationToken.None) service.ExecuteAsync(program, null, CancellationToken.None)
.GetAwaiter().GetResult(); .GetAwaiter().GetResult();
@@ -421,7 +427,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 10), CncProgramGenerators.CncProgramArb(1, 10),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
InspectionRunRecord capturedRecord = null; InspectionRunRecord capturedRecord = null;
mockStore.Setup(s => s.BeginRunAsync( mockStore.Setup(s => s.BeginRunAsync(
@@ -449,7 +455,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 8), CncProgramGenerators.CncProgramArb(1, 8),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
bool? capturedOverallPass = default; bool? capturedOverallPass = default;
bool callbackInvoked = false; bool callbackInvoked = false;
@@ -482,7 +488,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
// Validates: Requirements 4.4, 4.5 // Validates: Requirements 4.4, 4.5
public void CompleteRunAsync_CalledWithNullOverallPass_WhenCancelled() public void CompleteRunAsync_CalledWithNullOverallPass_WhenCancelled()
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
// Use a WaitDelayNode with long delay so cancellation happens during execution // Use a WaitDelayNode with long delay so cancellation happens during execution
var waitNode = new WaitDelayNode(Guid.NewGuid(), 0, "LongWait", 5000); var waitNode = new WaitDelayNode(Guid.NewGuid(), 0, "LongWait", 5000);
@@ -530,7 +536,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramWithInspectionNodesArb(2), CncProgramGenerators.CncProgramWithInspectionNodesArb(2),
program => program =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
// Make AppendNodeResultAsync always throw // Make AppendNodeResultAsync always throw
mockStore.Setup(s => s.AppendNodeResultAsync( mockStore.Setup(s => s.AppendNodeResultAsync(
@@ -587,7 +593,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
gen.ToArbitrary(), gen.ToArbitrary(),
waitNode => waitNode =>
{ {
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
bool? capturedOverallPass = default; bool? capturedOverallPass = default;
mockStore.Setup(s => s.CompleteRunAsync( mockStore.Setup(s => s.CompleteRunAsync(
@@ -630,7 +636,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
CncProgramGenerators.CncProgramArb(1, 8), CncProgramGenerators.CncProgramArb(1, 8),
program => program =>
{ {
var (service, _, _) = CreateService(); var (service, _, _, _, _) = CreateService();
// Build a map of NodeId → CncNodeViewModel // Build a map of NodeId → CncNodeViewModel
var nodeVms = program.Nodes var nodeVms = program.Nodes
@@ -700,7 +706,7 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
tuple => tuple =>
{ {
var (program, node, expectedPipelineName) = tuple; var (program, node, expectedPipelineName) = tuple;
var (service, mockStore, _) = CreateService(); var (service, mockStore, _, _, _) = CreateService();
PipelineExecutionSnapshot capturedSnapshot = null; PipelineExecutionSnapshot capturedSnapshot = null;
mockStore.Setup(s => s.AppendNodeResultAsync( mockStore.Setup(s => s.AppendNodeResultAsync(
+31 -10
View File
@@ -6,8 +6,10 @@ using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows; using System.Windows;
using System.Windows.Media.Imaging; using System.Windows.Media.Imaging;
using XP.Common.Converters;
using XP.Common.Logging.Interfaces; using XP.Common.Logging.Interfaces;
using XplorePlane.Models; using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.InspectionResults; using XplorePlane.Services.InspectionResults;
using XplorePlane.Services.MainViewport; using XplorePlane.Services.MainViewport;
using XplorePlane.ViewModels; using XplorePlane.ViewModels;
@@ -22,6 +24,7 @@ namespace XplorePlane.Services.Cnc
private readonly IInspectionResultStore _store; private readonly IInspectionResultStore _store;
private readonly ILoggerService _logger; private readonly ILoggerService _logger;
private readonly IMainViewportService _mainViewportService; private readonly IMainViewportService _mainViewportService;
private readonly IAppStateService _appStateService;
private readonly IPipelineExecutionService _pipelineExecutionService; private readonly IPipelineExecutionService _pipelineExecutionService;
private readonly IImageProcessingService _imageProcessingService; private readonly IImageProcessingService _imageProcessingService;
@@ -29,12 +32,14 @@ namespace XplorePlane.Services.Cnc
IInspectionResultStore store, IInspectionResultStore store,
ILoggerService logger, ILoggerService logger,
IMainViewportService mainViewportService, IMainViewportService mainViewportService,
IAppStateService appStateService,
IPipelineExecutionService pipelineExecutionService, IPipelineExecutionService pipelineExecutionService,
IImageProcessingService imageProcessingService) IImageProcessingService imageProcessingService)
{ {
_store = store ?? throw new ArgumentNullException(nameof(store)); _store = store ?? throw new ArgumentNullException(nameof(store));
_logger = logger ?? throw new ArgumentNullException(nameof(logger)); _logger = logger ?? throw new ArgumentNullException(nameof(logger));
_mainViewportService = mainViewportService; _mainViewportService = mainViewportService;
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
_pipelineExecutionService = pipelineExecutionService; _pipelineExecutionService = pipelineExecutionService;
_imageProcessingService = imageProcessingService; _imageProcessingService = imageProcessingService;
} }
@@ -46,10 +51,7 @@ namespace XplorePlane.Services.Cnc
return; return;
int inspectionNodeCount = program.Nodes.OfType<InspectionModuleNode>().Count(); int inspectionNodeCount = program.Nodes.OfType<InspectionModuleNode>().Count();
var sourceImage = TryGetSourceImage();
// 获取当前源图像(用于 run/source.bmp
var sourceImage = _mainViewportService?.LatestManualImage as BitmapSource
?? _mainViewportService?.CurrentDisplayImage as BitmapSource;
Guid runId; Guid runId;
try try
@@ -105,7 +107,7 @@ namespace XplorePlane.Services.Cnc
{ {
case ReferencePointNode rp: case ReferencePointNode rp:
_logger.ForModule<CncExecutionService>().Info( _logger.ForModule<CncExecutionService>().Info(
"执行参考点节点 [{Index}] {Name} | " + "Executing reference point node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"StageRotation={StageRotation} FixtureRotation={FixtureRotation} " + "StageRotation={StageRotation} FixtureRotation={FixtureRotation} " +
@@ -119,7 +121,7 @@ namespace XplorePlane.Services.Cnc
case SavePositionNode sp: case SavePositionNode sp:
_logger.ForModule<CncExecutionService>().Info( _logger.ForModule<CncExecutionService>().Info(
"执行保存位置节点 [{Index}] {Name} | " + "Executing save-position node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"StageRotation={StageRotation} FixtureRotation={FixtureRotation}", "StageRotation={StageRotation} FixtureRotation={FixtureRotation}",
@@ -133,7 +135,7 @@ namespace XplorePlane.Services.Cnc
case SaveNodeNode sn: case SaveNodeNode sn:
_logger.ForModule<CncExecutionService>().Info( _logger.ForModule<CncExecutionService>().Info(
"执行保存节点 [{Index}] {Name} | " + "Executing save node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W", "RayOn={RayOn} Voltage={Voltage}kV Power={Power}W",
@@ -147,7 +149,7 @@ namespace XplorePlane.Services.Cnc
case SaveNodeWithImageNode sni: case SaveNodeWithImageNode sni:
_logger.ForModule<CncExecutionService>().Info( _logger.ForModule<CncExecutionService>().Info(
"执行保存节点(图像) [{Index}] {Name} | " + "Executing save-with-image node [{Index}] {Name} | " +
"StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " + "StageX={StageX} StageY={StageY} SourceZ={SourceZ} DetectorZ={DetectorZ} " +
"DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " + "DetectorSwing={DetectorSwing} FDD={FDD} FOD={FOD} Magnification={Magnification} " +
"RayOn={RayOn} Voltage={Voltage}kV Power={Power}W ImageFile={ImageFile}", "RayOn={RayOn} Voltage={Voltage}kV Power={Power}W ImageFile={ImageFile}",
@@ -218,7 +220,7 @@ namespace XplorePlane.Services.Cnc
break; 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 nodeResultImage = node is InspectionModuleNode ? lastResultImage : null;
var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed; var finalState = nodeSucceeded ? NodeExecutionState.Succeeded : NodeExecutionState.Failed;
progress?.Report(new CncNodeExecutionProgress(node.Id, finalState, nodeResultImage)); 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<BitmapSource> ExecuteInspectionNodeAsync( private async Task<BitmapSource> ExecuteInspectionNodeAsync(
Guid runId, Guid runId,
InspectionModuleNode inspectionNode, InspectionModuleNode inspectionNode,
@@ -304,7 +325,7 @@ namespace XplorePlane.Services.Cnc
Height = resultImage.PixelHeight Height = resultImage.PixelHeight
}); });
nodeResult.Status = InspectionNodeStatus.Succeeded; nodeResult.Status = InspectionNodeStatus.Succeeded;
_mainViewportService?.SetManualImage(resultImage, $"CNC节点:{inspectionNode.Name}"); _mainViewportService?.SetManualImage(resultImage, $"CNC Node: {inspectionNode.Name}");
} }
} }
catch (Exception ex) catch (Exception ex)