探测器双队列的打通与实时按钮的切换
This commit is contained in:
Binary file not shown.
File diff suppressed because it is too large
Load Diff
-11
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user