探测器双队列的打通与实时按钮的切换

This commit is contained in:
zhengxuan.zhang
2026-05-06 23:18:28 +08:00
parent f9be56b99f
commit bd9b24beb1
10 changed files with 223 additions and 11388 deletions
File diff suppressed because it is too large Load Diff
@@ -1,11 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="Newtonsoft.Json" publicKeyToken="30ad4fe6b2a6aeed" culture="neutral" />
<bindingRedirect oldVersion="0.0.0.0-13.0.0.0" newVersion="13.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>
@@ -9,8 +9,10 @@ using FsCheck;
using FsCheck.Fluent;
using FsCheck.Xunit;
using Moq;
using Prism.Events;
using XP.Common.Logging.Interfaces;
using XP.Hardware.Detector.Abstractions;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.Cnc;
using XplorePlane.Services;
@@ -185,8 +187,14 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
var mockAppStateService = new Mock<IAppStateService>();
var mockPipelineExecutionService = new Mock<IPipelineExecutionService>();
var mockImageProcessingService = new Mock<IImageProcessingService>();
var mockEventAggregator = new Mock<IEventAggregator>();
mockLogger.Setup(l => l.ForModule<CncExecutionService>()).Returns(mockLogger.Object);
// Set up GetEvent<DetectorDisconnectedEvent>() so the constructor subscription doesn't throw
mockEventAggregator
.Setup(ea => ea.GetEvent<DetectorDisconnectedEvent>())
.Returns(new DetectorDisconnectedEvent());
mockStore.Setup(s => s.BeginRunAsync(
It.IsAny<InspectionRunRecord>(),
It.IsAny<InspectionAssetWriteRequest>()))
@@ -211,7 +219,8 @@ internal sealed class SynchronousProgress<T> : IProgress<T>
mockMainViewportService.Object,
mockAppStateService.Object,
mockPipelineExecutionService.Object,
mockImageProcessingService.Object);
mockImageProcessingService.Object,
mockEventAggregator.Object);
return (service, mockStore, mockLogger, mockMainViewportService, mockAppStateService);
}
@@ -14,6 +14,7 @@ using XP.Hardware.MotionControl.Abstractions.Enums;
using XP.Hardware.MotionControl.Abstractions.Events;
using XP.Hardware.MotionControl.Services;
using XP.Hardware.RaySource.Services;
using XplorePlane.Events;
using XplorePlane.Models;
namespace XplorePlane.Services.AppState
@@ -367,6 +368,9 @@ namespace XplorePlane.Services.AppState
{
if (_disposed) return;
// 在更新状态前记录当前连接状态,用于检测断连转换
bool wasConnected = _detectorState?.IsConnected ?? false;
// 从 IDetectorService 读取分辨率等补充信息
string resolution = string.Empty;
double frameRate = 0;
@@ -396,6 +400,13 @@ namespace XplorePlane.Services.AppState
"探测器状态已同步:{Status} → IsConnected={IsConnected} IsAcquiring={IsAcquiring} | " +
"Detector state synced: {Status} → IsConnected={IsConnected} IsAcquiring={IsAcquiring}",
status, isConnected, isAcquiring);
// 检测从已连接变为断开,发布断连事件
if (wasConnected && !isConnected)
{
_eventAggregator.GetEvent<DetectorDisconnectedEvent>().Publish();
_logger.Warn("探测器已断连,发布 DetectorDisconnectedEvent | Detector disconnected, publishing DetectorDisconnectedEvent");
}
}
/// <summary>
@@ -6,8 +6,10 @@ using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media.Imaging;
using Prism.Events;
using XP.Common.Converters;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.InspectionResults;
@@ -27,6 +29,10 @@ namespace XplorePlane.Services.Cnc
private readonly IAppStateService _appStateService;
private readonly IPipelineExecutionService _pipelineExecutionService;
private readonly IImageProcessingService _imageProcessingService;
private readonly IEventAggregator _eventAggregator;
// Task 4.2: volatile field so reads/writes are not reordered across threads
private volatile CancellationTokenSource _executionCts;
public CncExecutionService(
IInspectionResultStore store,
@@ -34,7 +40,8 @@ namespace XplorePlane.Services.Cnc
IMainViewportService mainViewportService,
IAppStateService appStateService,
IPipelineExecutionService pipelineExecutionService,
IImageProcessingService imageProcessingService)
IImageProcessingService imageProcessingService,
IEventAggregator eventAggregator)
{
_store = store ?? throw new ArgumentNullException(nameof(store));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
@@ -42,12 +49,36 @@ namespace XplorePlane.Services.Cnc
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
_pipelineExecutionService = pipelineExecutionService;
_imageProcessingService = imageProcessingService;
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
// Task 4.3: subscribe to DetectorDisconnectedEvent on a background thread
_eventAggregator.GetEvent<DetectorDisconnectedEvent>()
.Subscribe(OnDetectorDisconnected, ThreadOption.BackgroundThread);
}
// Task 4.3: callback cancel the running execution when the detector disconnects
private void OnDetectorDisconnected()
{
var cts = _executionCts;
if (cts == null) return;
try
{
cts.Cancel();
_logger.ForModule<CncExecutionService>().Warn("探测器断连,已取消当前 CNC 执行");
}
catch (ObjectDisposedException) { }
}
public async Task ExecuteAsync(CncProgram program, IProgress<CncNodeExecutionProgress> progress, CancellationToken cancellationToken)
{
// Task 4.4: create a linked CTS so DetectorDisconnectedEvent can cancel independently
using var linkedCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executionCts = linkedCts;
_mainViewportService?.SetCncRunning(true);
try
{
// Pre-cancellation check - do NOT call BeginRunAsync if already cancelled
if (cancellationToken.IsCancellationRequested)
if (linkedCts.Token.IsCancellationRequested)
return;
int inspectionNodeCount = program.Nodes.OfType<InspectionModuleNode>().Count();
@@ -91,7 +122,7 @@ namespace XplorePlane.Services.Cnc
foreach (var node in program.Nodes.OrderBy(n => n.Index))
{
if (cancellationToken.IsCancellationRequested)
if (linkedCts.Token.IsCancellationRequested)
{
cancelled = true;
break;
@@ -165,7 +196,7 @@ namespace XplorePlane.Services.Cnc
case WaitDelayNode waitNode:
try
{
await ExecuteWaitDelayWithProgressAsync(waitNode, progress, cancellationToken);
await ExecuteWaitDelayWithProgressAsync(waitNode, progress, linkedCts.Token);
}
catch (OperationCanceledException)
{
@@ -176,14 +207,14 @@ namespace XplorePlane.Services.Cnc
case PauseDialogNode pauseNode:
await Application.Current.Dispatcher.InvokeAsync(() =>
MessageBox.Show(pauseNode.DialogMessage, pauseNode.DialogTitle));
if (cancellationToken.IsCancellationRequested)
if (linkedCts.Token.IsCancellationRequested)
cancelled = true;
break;
case InspectionModuleNode inspectionNode:
try
{
var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, sourceImage, cancellationToken);
var img = await ExecuteInspectionNodeAsync(runId, inspectionNode, sourceImage, linkedCts.Token);
if (img != null) lastResultImage = img;
}
catch (Exception ex)
@@ -208,7 +239,7 @@ namespace XplorePlane.Services.Cnc
{
_logger.ForModule<CncExecutionService>().Error(ex,
"Unexpected error executing node '{0}' (Id={1})", node.Name, node.Id);
if (cancellationToken.IsCancellationRequested)
if (linkedCts.Token.IsCancellationRequested)
cancelled = true;
else
nodeSucceeded = false;
@@ -242,6 +273,12 @@ namespace XplorePlane.Services.Cnc
_logger.ForModule<CncExecutionService>().Error(ex,
"Failed to complete inspection run '{0}'", runId);
}
} // end try
finally
{
_executionCts = null;
_mainViewportService?.SetCncRunning(false);
}
}
private BitmapSource TryGetSourceImage()
@@ -255,7 +292,7 @@ namespace XplorePlane.Services.Cnc
if (detectorFrame?.ImageData == null || detectorFrame.Width <= 0 || detectorFrame.Height <= 0)
return null;
var bitmap = ImageConverter.ConvertGray16ToBitmapSource(
var bitmap = XP.Common.Converters.ImageConverter.ConvertGray16ToBitmapSource(
detectorFrame.ImageData,
(int)detectorFrame.Width,
(int)detectorFrame.Height);
@@ -29,14 +29,32 @@ namespace XplorePlane.Services.MainViewport
IEventAggregator eventAggregator,
IMainViewportService mainViewportService,
ILoggerService logger)
: this(eventAggregator, mainViewportService, logger,
ReadInt("DetectorPipeline:AcquireQueueCapacity", 16, 1),
ReadInt("DetectorPipeline:ProcessQueueCapacity", 8, 1),
ReadInt("DetectorPipeline:ProcessEveryNFrames", 1, 1))
{
}
/// <summary>
/// Internal constructor for testing: accepts capacity and sampling values directly,
/// bypassing App.config reads.
/// </summary>
internal DetectorFramePipelineService(
IEventAggregator eventAggregator,
IMainViewportService mainViewportService,
ILoggerService logger,
int acquireQueueCapacity,
int processQueueCapacity,
int processEveryNFrames)
{
ArgumentNullException.ThrowIfNull(eventAggregator);
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
_logger = logger?.ForModule<DetectorFramePipelineService>() ?? throw new ArgumentNullException(nameof(logger));
AcquireQueueCapacity = ReadInt("DetectorPipeline:AcquireQueueCapacity", 16, 1);
ProcessQueueCapacity = ReadInt("DetectorPipeline:ProcessQueueCapacity", 8, 1);
ProcessEveryNFrames = ReadInt("DetectorPipeline:ProcessEveryNFrames", 1, 1);
AcquireQueueCapacity = Math.Max(1, acquireQueueCapacity);
ProcessQueueCapacity = Math.Max(1, processQueueCapacity);
ProcessEveryNFrames = Math.Max(1, processEveryNFrames);
eventAggregator.GetEvent<ImageCapturedEvent>()
.Subscribe(OnImageCaptured, ThreadOption.BackgroundThread);
@@ -7,6 +7,7 @@ namespace XplorePlane.Services.MainViewport
{
MainViewportSourceMode CurrentSourceMode { get; }
bool IsRealtimeDisplayEnabled { get; }
bool IsCncRunning { get; }
ImageSource CurrentDisplayImage { get; }
string CurrentDisplayInfo { get; }
ImageSource LatestDetectorImage { get; }
@@ -21,5 +22,11 @@ namespace XplorePlane.Services.MainViewport
void UpdateDetectorFrame(DetectorFrame frame);
void SetManualImage(ImageSource image, string filePath);
/// <summary>
/// 通知 MainViewportService 当前 CNC 运行状态。
/// CNC 开始运行时传入 true,结束时传入 false。
/// </summary>
void SetCncRunning(bool isRunning);
}
}
@@ -13,6 +13,7 @@ namespace XplorePlane.Services.MainViewport
private MainViewportSourceMode _currentSourceMode = MainViewportSourceMode.LiveDetector;
private bool _isRealtimeDisplayEnabled;
private bool _isCncRunning;
private ImageSource _currentDisplayImage;
private string _currentDisplayInfo = "等待探测器图像...";
private ImageSource _latestDetectorImage;
@@ -48,6 +49,17 @@ namespace XplorePlane.Services.MainViewport
}
}
public bool IsCncRunning
{
get
{
lock (_syncRoot)
{
return _isCncRunning;
}
}
}
public ImageSource CurrentDisplayImage
{
get
@@ -99,11 +111,21 @@ namespace XplorePlane.Services.MainViewport
bool changed;
lock (_syncRoot)
{
changed = _isRealtimeDisplayEnabled != isEnabled;
if (!isEnabled && _isCncRunning)
{
_logger.Warn("CNC 正在运行,忽略 SetRealtimeDisplayEnabled(false) 调用");
return;
}
changed = _isRealtimeDisplayEnabled != isEnabled
|| (isEnabled && _currentSourceMode == MainViewportSourceMode.ManualImage);
_isRealtimeDisplayEnabled = isEnabled;
if (_currentSourceMode == MainViewportSourceMode.LiveDetector && isEnabled)
if (isEnabled)
{
// 开启实时:无论当前是 ManualImage 还是 LiveDetector,都切回实时帧显示
_currentSourceMode = MainViewportSourceMode.LiveDetector;
ApplyLiveDetectorDisplay_NoLock();
}
}
@@ -173,6 +195,12 @@ namespace XplorePlane.Services.MainViewport
lock (_syncRoot)
{
if (_isCncRunning)
{
_logger.Warn("CNC 正在运行,忽略 SetManualImage 调用");
return;
}
_latestManualImage = image;
_latestManualInfo = $"手动加载图像 {fileName}";
_currentSourceMode = MainViewportSourceMode.ManualImage;
@@ -184,6 +212,23 @@ namespace XplorePlane.Services.MainViewport
RaiseStateChanged();
}
public void SetCncRunning(bool isRunning)
{
bool modeChanged = false;
lock (_syncRoot)
{
_isCncRunning = isRunning;
if (isRunning && _currentSourceMode == MainViewportSourceMode.ManualImage)
{
_currentSourceMode = MainViewportSourceMode.LiveDetector;
ApplyLiveDetectorDisplay_NoLock();
modeChanged = true;
}
}
_logger.Info("CNC 运行状态已更新:{IsRunning}", isRunning);
if (modeChanged) RaiseStateChanged();
}
private void ApplyLiveDetectorDisplay_NoLock()
{
_currentDisplayImage = _latestDetectorImage;
@@ -6,6 +6,8 @@ using System.Windows;
using System.Windows.Media;
using XP.Common.Logging.Interfaces;
using XplorePlane.Events;
using XplorePlane.Models;
using XplorePlane.Services.AppState;
using XplorePlane.Services.MainViewport;
namespace XplorePlane.ViewModels
@@ -18,6 +20,7 @@ namespace XplorePlane.ViewModels
private readonly ILoggerService _logger;
private readonly IMainViewportService _mainViewportService;
private readonly IEventAggregator _eventAggregator;
private readonly IAppStateService _appStateService;
private ImageSource _imageSource;
private string _imageInfo = "等待探测器图像...";
@@ -26,21 +29,42 @@ namespace XplorePlane.ViewModels
private Point? _measurePoint2;
private string _measurementResult;
// Task 5.2: IsRealtimeEnabled backing field
private bool _isRealtimeEnabled;
// Task 5.3: IsDetectorConnected backing field
private bool _isDetectorConnected = true;
// Task 5.4: IsCncRunning backing field
private bool _isCncRunning;
public ViewportPanelViewModel(
IMainViewportService mainViewportService,
IEventAggregator eventAggregator,
IAppStateService appStateService,
ILoggerService logger)
{
_mainViewportService = mainViewportService ?? throw new ArgumentNullException(nameof(mainViewportService));
_eventAggregator = eventAggregator ?? throw new ArgumentNullException(nameof(eventAggregator));
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
_logger = logger?.ForModule<ViewportPanelViewModel>() ?? throw new ArgumentNullException(nameof(logger));
CancelMeasurementCommand = new DelegateCommand(ExecuteCancelMeasurement);
// Task 5.5: ToggleRealtimeCommand
ToggleRealtimeCommand = new DelegateCommand(() => IsRealtimeEnabled = !IsRealtimeEnabled);
_mainViewportService.StateChanged += OnMainViewportStateChanged;
_eventAggregator.GetEvent<MeasurementToolEvent>()
.Subscribe(OnMeasurementToolActivated, ThreadOption.UIThread);
// Task 5.6: Subscribe to DetectorStateChanged
_appStateService.DetectorStateChanged += OnDetectorStateChanged;
// Task 5.7: Subscribe to DetectorDisconnectedEvent on UI thread
_eventAggregator.GetEvent<DetectorDisconnectedEvent>()
.Subscribe(OnDetectorDisconnectedForUI, ThreadOption.UIThread);
UpdateFromState(updateInfo: true);
}
@@ -56,6 +80,31 @@ namespace XplorePlane.ViewModels
set => SetProperty(ref _imageInfo, value);
}
// Task 5.2: IsRealtimeEnabled property (two-way binding)
public bool IsRealtimeEnabled
{
get => _isRealtimeEnabled;
set
{
if (SetProperty(ref _isRealtimeEnabled, value))
_mainViewportService.SetRealtimeDisplayEnabled(value);
}
}
// Task 5.3: IsDetectorConnected property (read-only, private setter)
public bool IsDetectorConnected
{
get => _isDetectorConnected;
private set
{
if (SetProperty(ref _isDetectorConnected, value))
RaisePropertyChanged(nameof(IsAnimatedSwitchEnabled));
}
}
// Task 5.4: IsAnimatedSwitchEnabled computed property
public bool IsAnimatedSwitchEnabled => _isDetectorConnected && !_isCncRunning;
public MeasurementToolMode CurrentMeasurementMode
{
get => _currentMeasurementMode;
@@ -100,6 +149,9 @@ namespace XplorePlane.ViewModels
public DelegateCommand CancelMeasurementCommand { get; }
// Task 5.5: ToggleRealtimeCommand
public DelegateCommand ToggleRealtimeCommand { get; }
public void ResetMeasurementState()
{
MeasurePoint1 = null;
@@ -151,10 +203,40 @@ namespace XplorePlane.ViewModels
{
ImageSource = _mainViewportService.CurrentDisplayImage;
// Task 5.8: Sync IsRealtimeEnabled from service
_isRealtimeEnabled = _mainViewportService.IsRealtimeDisplayEnabled;
RaisePropertyChanged(nameof(IsRealtimeEnabled));
// Task 5.8: Sync _isCncRunning from service and raise IsAnimatedSwitchEnabled
_isCncRunning = _mainViewportService.IsCncRunning;
RaisePropertyChanged(nameof(IsAnimatedSwitchEnabled));
if (updateInfo)
{
ImageInfo = _mainViewportService.CurrentDisplayInfo;
}
}
// Task 5.6: Handle DetectorStateChanged on background thread, dispatch to UI
private void OnDetectorStateChanged(object sender, StateChangedEventArgs<DetectorState> e)
{
Application.Current?.Dispatcher?.BeginInvoke(new Action(() =>
{
IsDetectorConnected = e.NewValue.IsConnected;
}));
}
// Task 5.7: Handle DetectorDisconnectedEvent on UI thread
private void OnDetectorDisconnectedForUI()
{
if (_isCncRunning)
{
MessageBox.Show(
"探测器已断连,CNC 已自动停止。请检查探测器连接后再继续操作。",
"探测器断连警告",
MessageBoxButton.OK,
MessageBoxImage.Warning);
}
}
}
}