调整硬件appstate 保持与硬件库层面定义一致
This commit is contained in:
@@ -1,7 +1,12 @@
|
|||||||
using Moq;
|
using Moq;
|
||||||
|
using Prism.Events;
|
||||||
using System;
|
using System;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
|
using XP.Hardware.MotionControl.Abstractions;
|
||||||
|
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||||
|
using XP.Hardware.MotionControl.Abstractions.Events;
|
||||||
|
using XP.Hardware.MotionControl.Services;
|
||||||
using XP.Hardware.RaySource.Services;
|
using XP.Hardware.RaySource.Services;
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
using XplorePlane.Services.AppState;
|
using XplorePlane.Services.AppState;
|
||||||
@@ -11,30 +16,62 @@ using Xunit.Abstractions;
|
|||||||
namespace XplorePlane.Tests.Services
|
namespace XplorePlane.Tests.Services
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// AppStateService 单元测试。
|
/// AppStateService unit tests.
|
||||||
/// 验证默认状态值、Dispose 后行为、null 参数校验、CalibrationMatrix 缺失时的错误处理。
|
/// Verifies default values, null guards, dispose behavior, and hardware-driven motion-state sync.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppStateServiceTests : IDisposable
|
public class AppStateServiceTests : IDisposable
|
||||||
{
|
{
|
||||||
private readonly AppStateService _service;
|
private readonly AppStateService _service;
|
||||||
private readonly Mock<IRaySourceService> _mockRaySource;
|
private readonly Mock<IRaySourceService> _mockRaySource;
|
||||||
|
private readonly Mock<IMotionSystem> _mockMotionSystem;
|
||||||
|
private readonly Mock<IMotionControlService> _mockMotionControlService;
|
||||||
|
private readonly Mock<ILinearAxis> _mockStageX;
|
||||||
|
private readonly Mock<ILinearAxis> _mockStageY;
|
||||||
|
private readonly Mock<ILinearAxis> _mockSourceZ;
|
||||||
|
private readonly Mock<ILinearAxis> _mockDetectorZ;
|
||||||
|
private readonly Mock<IRotaryAxis> _mockDetectorSwing;
|
||||||
private readonly Mock<ILoggerService> _mockLogger;
|
private readonly Mock<ILoggerService> _mockLogger;
|
||||||
|
private readonly EventAggregator _eventAggregator;
|
||||||
private readonly ITestOutputHelper _output;
|
private readonly ITestOutputHelper _output;
|
||||||
|
|
||||||
public AppStateServiceTests(ITestOutputHelper output)
|
public AppStateServiceTests(ITestOutputHelper output)
|
||||||
{
|
{
|
||||||
_output = output;
|
_output = output;
|
||||||
|
|
||||||
// Ensure WPF Application exists for Dispatcher
|
|
||||||
if (Application.Current == null)
|
if (Application.Current == null)
|
||||||
{
|
{
|
||||||
new Application();
|
new Application();
|
||||||
}
|
}
|
||||||
|
|
||||||
_mockRaySource = new Mock<IRaySourceService>();
|
_mockRaySource = new Mock<IRaySourceService>();
|
||||||
|
_mockMotionSystem = new Mock<IMotionSystem>();
|
||||||
|
_mockMotionControlService = new Mock<IMotionControlService>();
|
||||||
|
_mockStageX = CreateLinearAxis(AxisId.StageX, 0);
|
||||||
|
_mockStageY = CreateLinearAxis(AxisId.StageY, 0);
|
||||||
|
_mockSourceZ = CreateLinearAxis(AxisId.SourceZ, 0);
|
||||||
|
_mockDetectorZ = CreateLinearAxis(AxisId.DetectorZ, 0);
|
||||||
|
_mockDetectorSwing = CreateRotaryAxis(RotaryAxisId.DetectorSwing, 0);
|
||||||
_mockLogger = new Mock<ILoggerService>();
|
_mockLogger = new Mock<ILoggerService>();
|
||||||
|
_eventAggregator = new EventAggregator();
|
||||||
|
|
||||||
|
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.StageX)).Returns(_mockStageX.Object);
|
||||||
|
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.StageY)).Returns(_mockStageY.Object);
|
||||||
|
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.SourceZ)).Returns(_mockSourceZ.Object);
|
||||||
|
_mockMotionSystem.Setup(x => x.GetLinearAxis(AxisId.DetectorZ)).Returns(_mockDetectorZ.Object);
|
||||||
|
_mockMotionSystem.Setup(x => x.GetRotaryAxis(RotaryAxisId.DetectorSwing)).Returns(_mockDetectorSwing.Object);
|
||||||
|
|
||||||
|
_mockMotionControlService
|
||||||
|
.Setup(x => x.GetCurrentGeometry())
|
||||||
|
.Returns((0d, 0d, 1d));
|
||||||
|
|
||||||
_mockLogger.Setup(l => l.ForModule<AppStateService>()).Returns(_mockLogger.Object);
|
_mockLogger.Setup(l => l.ForModule<AppStateService>()).Returns(_mockLogger.Object);
|
||||||
_service = new AppStateService(_mockRaySource.Object, _mockLogger.Object);
|
|
||||||
|
_service = new AppStateService(
|
||||||
|
_mockRaySource.Object,
|
||||||
|
_mockMotionSystem.Object,
|
||||||
|
_mockMotionControlService.Object,
|
||||||
|
_eventAggregator,
|
||||||
|
_mockLogger.Object);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -42,13 +79,15 @@ namespace XplorePlane.Tests.Services
|
|||||||
_service.Dispose();
|
_service.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 默认状态值验证 ──
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void DefaultState_MotionState_IsDefault()
|
public void DefaultState_MotionState_IsHardwareSnapshot()
|
||||||
{
|
{
|
||||||
_output.WriteLine($"MotionState == MotionState.Default: {ReferenceEquals(MotionState.Default, _service.MotionState)}");
|
Assert.Equal(0, _service.MotionState.XM);
|
||||||
Assert.Same(MotionState.Default, _service.MotionState);
|
Assert.Equal(0, _service.MotionState.YM);
|
||||||
|
Assert.Equal(0, _service.MotionState.ZT);
|
||||||
|
Assert.Equal(0, _service.MotionState.ZD);
|
||||||
|
Assert.Equal(0, _service.MotionState.TiltD);
|
||||||
|
Assert.Equal(0, _service.MotionState.Dist);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
@@ -72,8 +111,6 @@ namespace XplorePlane.Tests.Services
|
|||||||
Assert.Null(_service.CalibrationMatrix);
|
Assert.Null(_service.CalibrationMatrix);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── null 参数抛出 ArgumentNullException ──
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void UpdateMotionState_NullArgument_ThrowsArgumentNullException()
|
public void UpdateMotionState_NullArgument_ThrowsArgumentNullException()
|
||||||
{
|
{
|
||||||
@@ -102,36 +139,66 @@ namespace XplorePlane.Tests.Services
|
|||||||
_output.WriteLine($"UpdateSystemState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
|
_output.WriteLine($"UpdateSystemState(null) threw: {ex.GetType().Name}, Param={ex.ParamName}");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dispose 后 Update 被忽略 ──
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Dispose_ThenUpdate_IsIgnored()
|
public void Dispose_ThenUpdate_IsIgnored()
|
||||||
{
|
{
|
||||||
var originalState = _service.MotionState;
|
var originalState = _service.MotionState;
|
||||||
_service.Dispose();
|
_service.Dispose();
|
||||||
_output.WriteLine("Service disposed, attempting UpdateMotionState...");
|
|
||||||
|
|
||||||
// Should not throw, and state should remain unchanged
|
|
||||||
var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
|
var newState = new MotionState(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
|
||||||
_service.UpdateMotionState(newState);
|
_service.UpdateMotionState(newState);
|
||||||
|
|
||||||
_output.WriteLine($"State unchanged after dispose: {ReferenceEquals(originalState, _service.MotionState)}");
|
|
||||||
Assert.Same(originalState, _service.MotionState);
|
Assert.Same(originalState, _service.MotionState);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── CalibrationMatrix 为 null 时 RequestLinkedView 设置错误状态 ──
|
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void RequestLinkedView_NoCalibrationMatrix_SetsErrorState()
|
public void RequestLinkedView_NoCalibrationMatrix_SetsErrorState()
|
||||||
{
|
{
|
||||||
// CalibrationMatrix is null by default
|
|
||||||
Assert.Null(_service.CalibrationMatrix);
|
Assert.Null(_service.CalibrationMatrix);
|
||||||
|
|
||||||
_service.RequestLinkedView(100.0, 200.0);
|
_service.RequestLinkedView(100.0, 200.0);
|
||||||
|
|
||||||
_output.WriteLine($"RequestLinkedView(100, 200) without CalibrationMatrix: HasError={_service.SystemState.HasError}, ErrorMessage='{_service.SystemState.ErrorMessage}'");
|
|
||||||
Assert.True(_service.SystemState.HasError);
|
Assert.True(_service.SystemState.HasError);
|
||||||
Assert.NotEmpty(_service.SystemState.ErrorMessage);
|
Assert.NotEmpty(_service.SystemState.ErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void GeometryUpdatedEvent_RefreshesMotionStateFromHardware()
|
||||||
|
{
|
||||||
|
_mockStageX.SetupGet(x => x.ActualPosition).Returns(12.5);
|
||||||
|
_mockStageY.SetupGet(x => x.ActualPosition).Returns(34.5);
|
||||||
|
_mockSourceZ.SetupGet(x => x.ActualPosition).Returns(56.5);
|
||||||
|
_mockDetectorZ.SetupGet(x => x.ActualPosition).Returns(78.5);
|
||||||
|
_mockDetectorSwing.SetupGet(x => x.ActualAngle).Returns(9.5);
|
||||||
|
|
||||||
|
_eventAggregator.GetEvent<GeometryUpdatedEvent>()
|
||||||
|
.Publish(new GeometryData(100, 222.2, 2.22));
|
||||||
|
|
||||||
|
Assert.Equal(12.5, _service.MotionState.XM);
|
||||||
|
Assert.Equal(34.5, _service.MotionState.YM);
|
||||||
|
Assert.Equal(56.5, _service.MotionState.ZT);
|
||||||
|
Assert.Equal(78.5, _service.MotionState.ZD);
|
||||||
|
Assert.Equal(9.5, _service.MotionState.TiltD);
|
||||||
|
Assert.Equal(222.2, _service.MotionState.Dist);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mock<ILinearAxis> CreateLinearAxis(AxisId axisId, double position)
|
||||||
|
{
|
||||||
|
var axis = new Mock<ILinearAxis>();
|
||||||
|
axis.SetupGet(x => x.Id).Returns(axisId);
|
||||||
|
axis.SetupGet(x => x.ActualPosition).Returns(position);
|
||||||
|
axis.SetupGet(x => x.Status).Returns(AxisStatus.Idle);
|
||||||
|
return axis;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Mock<IRotaryAxis> CreateRotaryAxis(RotaryAxisId axisId, double angle)
|
||||||
|
{
|
||||||
|
var axis = new Mock<IRotaryAxis>();
|
||||||
|
axis.SetupGet(x => x.Id).Returns(axisId);
|
||||||
|
axis.SetupGet(x => x.ActualAngle).Returns(angle);
|
||||||
|
axis.SetupGet(x => x.Status).Returns(AxisStatus.Idle);
|
||||||
|
axis.SetupGet(x => x.Enabled).Returns(true);
|
||||||
|
return axis;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,40 @@
|
|||||||
|
using Prism.Events;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using System;
|
using System;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using System.Windows.Threading;
|
using System.Windows.Threading;
|
||||||
using XP.Common.Logging.Interfaces;
|
using XP.Common.Logging.Interfaces;
|
||||||
|
using XP.Hardware.MotionControl.Abstractions;
|
||||||
|
using XP.Hardware.MotionControl.Abstractions.Enums;
|
||||||
|
using XP.Hardware.MotionControl.Abstractions.Events;
|
||||||
|
using XP.Hardware.MotionControl.Services;
|
||||||
using XP.Hardware.RaySource.Services;
|
using XP.Hardware.RaySource.Services;
|
||||||
using XplorePlane.Models;
|
using XplorePlane.Models;
|
||||||
|
|
||||||
namespace XplorePlane.Services.AppState
|
namespace XplorePlane.Services.AppState
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 全局应用状态管理服务实现。
|
/// Global application state service.
|
||||||
/// 继承 BindableBase 以支持 WPF 数据绑定,使用 Interlocked.Exchange 保证线程安全写入,
|
/// Motion state is synchronized from the motion hardware service layer and
|
||||||
/// 通过 Dispatcher.BeginInvoke 将事件调度到 UI 线程。
|
/// mapped into the legacy business model for compatibility.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public class AppStateService : BindableBase, IAppStateService
|
public class AppStateService : BindableBase, IAppStateService
|
||||||
{
|
{
|
||||||
private readonly Dispatcher _dispatcher;
|
private readonly Dispatcher _dispatcher;
|
||||||
private readonly IRaySourceService _raySourceService;
|
private readonly IRaySourceService _raySourceService;
|
||||||
|
private readonly IMotionSystem _motionSystem;
|
||||||
|
private readonly IMotionControlService _motionControlService;
|
||||||
|
private readonly IEventAggregator _eventAggregator;
|
||||||
private readonly ILoggerService _logger;
|
private readonly ILoggerService _logger;
|
||||||
|
private readonly SubscriptionToken _axisStatusChangedToken;
|
||||||
|
private readonly SubscriptionToken _geometryUpdatedToken;
|
||||||
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
private GeometryData _latestGeometry;
|
||||||
|
|
||||||
// ── 状态字段(通过 Interlocked.Exchange 原子替换)──
|
// ── 状态字段(通过 Interlocked.Exchange 原子替换)──
|
||||||
private MotionState _motionState = MotionState.Default;
|
private MotionState _motionState = MotionState.Default;
|
||||||
|
|
||||||
private RaySourceState _raySourceState = RaySourceState.Default;
|
private RaySourceState _raySourceState = RaySourceState.Default;
|
||||||
private DetectorState _detectorState = DetectorState.Default;
|
private DetectorState _detectorState = DetectorState.Default;
|
||||||
private SystemState _systemState = SystemState.Default;
|
private SystemState _systemState = SystemState.Default;
|
||||||
@@ -34,24 +45,16 @@ namespace XplorePlane.Services.AppState
|
|||||||
|
|
||||||
// ── 类型化状态变更事件 ──
|
// ── 类型化状态变更事件 ──
|
||||||
public event EventHandler<StateChangedEventArgs<MotionState>> MotionStateChanged;
|
public event EventHandler<StateChangedEventArgs<MotionState>> MotionStateChanged;
|
||||||
|
|
||||||
public event EventHandler<StateChangedEventArgs<RaySourceState>> RaySourceStateChanged;
|
public event EventHandler<StateChangedEventArgs<RaySourceState>> RaySourceStateChanged;
|
||||||
|
|
||||||
public event EventHandler<StateChangedEventArgs<DetectorState>> DetectorStateChanged;
|
public event EventHandler<StateChangedEventArgs<DetectorState>> DetectorStateChanged;
|
||||||
|
|
||||||
public event EventHandler<StateChangedEventArgs<SystemState>> SystemStateChanged;
|
public event EventHandler<StateChangedEventArgs<SystemState>> SystemStateChanged;
|
||||||
|
|
||||||
public event EventHandler<StateChangedEventArgs<CameraState>> CameraStateChanged;
|
public event EventHandler<StateChangedEventArgs<CameraState>> CameraStateChanged;
|
||||||
|
|
||||||
public event EventHandler<StateChangedEventArgs<LinkedViewState>> LinkedViewStateChanged;
|
public event EventHandler<StateChangedEventArgs<LinkedViewState>> LinkedViewStateChanged;
|
||||||
|
|
||||||
public event EventHandler<StateChangedEventArgs<RecipeExecutionState>> RecipeExecutionStateChanged;
|
public event EventHandler<StateChangedEventArgs<RecipeExecutionState>> RecipeExecutionStateChanged;
|
||||||
|
|
||||||
public event EventHandler<LinkedViewRequestEventArgs> LinkedViewRequested;
|
public event EventHandler<LinkedViewRequestEventArgs> LinkedViewRequested;
|
||||||
|
|
||||||
// ── 状态属性(只读)──
|
// ── 状态属性(只读)──
|
||||||
public MotionState MotionState => _motionState;
|
public MotionState MotionState => _motionState;
|
||||||
|
|
||||||
public RaySourceState RaySourceState => _raySourceState;
|
public RaySourceState RaySourceState => _raySourceState;
|
||||||
public DetectorState DetectorState => _detectorState;
|
public DetectorState DetectorState => _detectorState;
|
||||||
public SystemState SystemState => _systemState;
|
public SystemState SystemState => _systemState;
|
||||||
@@ -62,17 +65,34 @@ namespace XplorePlane.Services.AppState
|
|||||||
|
|
||||||
public AppStateService(
|
public AppStateService(
|
||||||
IRaySourceService raySourceService,
|
IRaySourceService raySourceService,
|
||||||
|
IMotionSystem motionSystem,
|
||||||
|
IMotionControlService motionControlService,
|
||||||
|
IEventAggregator eventAggregator,
|
||||||
ILoggerService logger)
|
ILoggerService logger)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(raySourceService);
|
ArgumentNullException.ThrowIfNull(raySourceService);
|
||||||
|
ArgumentNullException.ThrowIfNull(motionSystem);
|
||||||
|
ArgumentNullException.ThrowIfNull(motionControlService);
|
||||||
|
ArgumentNullException.ThrowIfNull(eventAggregator);
|
||||||
ArgumentNullException.ThrowIfNull(logger);
|
ArgumentNullException.ThrowIfNull(logger);
|
||||||
|
|
||||||
_raySourceService = raySourceService;
|
_raySourceService = raySourceService;
|
||||||
|
_motionSystem = motionSystem;
|
||||||
|
_motionControlService = motionControlService;
|
||||||
|
_eventAggregator = eventAggregator;
|
||||||
_logger = logger.ForModule<AppStateService>();
|
_logger = logger.ForModule<AppStateService>();
|
||||||
_dispatcher = Application.Current.Dispatcher;
|
_dispatcher = Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher;
|
||||||
|
|
||||||
|
_geometryUpdatedToken = _eventAggregator
|
||||||
|
.GetEvent<GeometryUpdatedEvent>()
|
||||||
|
.Subscribe(OnGeometryUpdated);
|
||||||
|
|
||||||
|
_axisStatusChangedToken = _eventAggregator
|
||||||
|
.GetEvent<AxisStatusChangedEvent>()
|
||||||
|
.Subscribe(OnAxisStatusChanged);
|
||||||
|
|
||||||
SubscribeToExistingServices();
|
SubscribeToExistingServices();
|
||||||
_logger.Info("AppStateService 已初始化");
|
_logger.Info("AppStateService initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 状态更新方法 ──
|
// ── 状态更新方法 ──
|
||||||
@@ -80,17 +100,30 @@ namespace XplorePlane.Services.AppState
|
|||||||
public void UpdateMotionState(MotionState newState)
|
public void UpdateMotionState(MotionState newState)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(newState);
|
ArgumentNullException.ThrowIfNull(newState);
|
||||||
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateMotionState 调用"); return; }
|
if (_disposed)
|
||||||
|
{
|
||||||
|
_logger.Warn("AppStateService is disposed, ignoring UpdateMotionState");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var old = Interlocked.Exchange(ref _motionState, newState);
|
// Keep the legacy API surface, but let the hardware service layer
|
||||||
if (ReferenceEquals(old, newState)) return;
|
// remain the source of truth whenever a fresh hardware snapshot is available.
|
||||||
RaiseOnDispatcher(old, newState, MotionStateChanged, nameof(MotionState));
|
if (TryRefreshMotionStateFromHardware("UpdateMotionState"))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetMotionState(newState);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateRaySourceState(RaySourceState newState)
|
public void UpdateRaySourceState(RaySourceState newState)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(newState);
|
ArgumentNullException.ThrowIfNull(newState);
|
||||||
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateRaySourceState 调用"); return; }
|
if (_disposed)
|
||||||
|
{
|
||||||
|
_logger.Warn("AppStateService is disposed, ignoring UpdateRaySourceState");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var old = Interlocked.Exchange(ref _raySourceState, newState);
|
var old = Interlocked.Exchange(ref _raySourceState, newState);
|
||||||
if (ReferenceEquals(old, newState)) return;
|
if (ReferenceEquals(old, newState)) return;
|
||||||
@@ -100,7 +133,11 @@ namespace XplorePlane.Services.AppState
|
|||||||
public void UpdateDetectorState(DetectorState newState)
|
public void UpdateDetectorState(DetectorState newState)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(newState);
|
ArgumentNullException.ThrowIfNull(newState);
|
||||||
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateDetectorState 调用"); return; }
|
if (_disposed)
|
||||||
|
{
|
||||||
|
_logger.Warn("AppStateService is disposed, ignoring UpdateDetectorState");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var old = Interlocked.Exchange(ref _detectorState, newState);
|
var old = Interlocked.Exchange(ref _detectorState, newState);
|
||||||
if (ReferenceEquals(old, newState)) return;
|
if (ReferenceEquals(old, newState)) return;
|
||||||
@@ -110,7 +147,11 @@ namespace XplorePlane.Services.AppState
|
|||||||
public void UpdateSystemState(SystemState newState)
|
public void UpdateSystemState(SystemState newState)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(newState);
|
ArgumentNullException.ThrowIfNull(newState);
|
||||||
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateSystemState 调用"); return; }
|
if (_disposed)
|
||||||
|
{
|
||||||
|
_logger.Warn("AppStateService is disposed, ignoring UpdateSystemState");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var old = Interlocked.Exchange(ref _systemState, newState);
|
var old = Interlocked.Exchange(ref _systemState, newState);
|
||||||
if (ReferenceEquals(old, newState)) return;
|
if (ReferenceEquals(old, newState)) return;
|
||||||
@@ -120,7 +161,11 @@ namespace XplorePlane.Services.AppState
|
|||||||
public void UpdateCameraState(CameraState newState)
|
public void UpdateCameraState(CameraState newState)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(newState);
|
ArgumentNullException.ThrowIfNull(newState);
|
||||||
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateCameraState 调用"); return; }
|
if (_disposed)
|
||||||
|
{
|
||||||
|
_logger.Warn("AppStateService is disposed, ignoring UpdateCameraState");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var old = Interlocked.Exchange(ref _cameraState, newState);
|
var old = Interlocked.Exchange(ref _cameraState, newState);
|
||||||
if (ReferenceEquals(old, newState)) return;
|
if (ReferenceEquals(old, newState)) return;
|
||||||
@@ -130,21 +175,26 @@ namespace XplorePlane.Services.AppState
|
|||||||
public void UpdateCalibrationMatrix(CalibrationMatrix newMatrix)
|
public void UpdateCalibrationMatrix(CalibrationMatrix newMatrix)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(newMatrix);
|
ArgumentNullException.ThrowIfNull(newMatrix);
|
||||||
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateCalibrationMatrix 调用"); return; }
|
if (_disposed)
|
||||||
|
{
|
||||||
|
_logger.Warn("AppStateService is disposed, ignoring UpdateCalibrationMatrix");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var old = Interlocked.Exchange(ref _calibrationMatrix, newMatrix);
|
var old = Interlocked.Exchange(ref _calibrationMatrix, newMatrix);
|
||||||
if (ReferenceEquals(old, newMatrix)) return;
|
if (ReferenceEquals(old, newMatrix)) return;
|
||||||
|
|
||||||
_dispatcher.BeginInvoke(() =>
|
_dispatcher.BeginInvoke(() => RaisePropertyChanged(nameof(CalibrationMatrix)));
|
||||||
{
|
|
||||||
RaisePropertyChanged(nameof(CalibrationMatrix));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void UpdateLinkedViewState(LinkedViewState newState)
|
public void UpdateLinkedViewState(LinkedViewState newState)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(newState);
|
ArgumentNullException.ThrowIfNull(newState);
|
||||||
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateLinkedViewState 调用"); return; }
|
if (_disposed)
|
||||||
|
{
|
||||||
|
_logger.Warn("AppStateService is disposed, ignoring UpdateLinkedViewState");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var old = Interlocked.Exchange(ref _linkedViewState, newState);
|
var old = Interlocked.Exchange(ref _linkedViewState, newState);
|
||||||
if (ReferenceEquals(old, newState)) return;
|
if (ReferenceEquals(old, newState)) return;
|
||||||
@@ -154,7 +204,11 @@ namespace XplorePlane.Services.AppState
|
|||||||
public void UpdateRecipeExecutionState(RecipeExecutionState newState)
|
public void UpdateRecipeExecutionState(RecipeExecutionState newState)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(newState);
|
ArgumentNullException.ThrowIfNull(newState);
|
||||||
if (_disposed) { _logger.Warn("AppStateService 已释放,忽略 UpdateRecipeExecutionState 调用"); return; }
|
if (_disposed)
|
||||||
|
{
|
||||||
|
_logger.Warn("AppStateService is disposed, ignoring UpdateRecipeExecutionState");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
var old = Interlocked.Exchange(ref _recipeExecutionState, newState);
|
var old = Interlocked.Exchange(ref _recipeExecutionState, newState);
|
||||||
if (ReferenceEquals(old, newState)) return;
|
if (ReferenceEquals(old, newState)) return;
|
||||||
@@ -168,11 +222,11 @@ namespace XplorePlane.Services.AppState
|
|||||||
var matrix = _calibrationMatrix;
|
var matrix = _calibrationMatrix;
|
||||||
if (matrix is null)
|
if (matrix is null)
|
||||||
{
|
{
|
||||||
_logger.Warn("CalibrationMatrix 未设置,无法执行画面联动 (pixelX={PixelX}, pixelY={PixelY})", pixelX, pixelY);
|
_logger.Warn("CalibrationMatrix is not configured, cannot execute linked view request (pixelX={PixelX}, pixelY={PixelY})", pixelX, pixelY);
|
||||||
UpdateSystemState(SystemState with
|
UpdateSystemState(SystemState with
|
||||||
{
|
{
|
||||||
HasError = true,
|
HasError = true,
|
||||||
ErrorMessage = "CalibrationMatrix 未设置,无法执行画面联动"
|
ErrorMessage = "CalibrationMatrix is not configured, cannot execute linked view request"
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -191,10 +245,38 @@ namespace XplorePlane.Services.AppState
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 内部辅助方法 ──
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
|
||||||
private void RaiseOnDispatcher<T>(T oldVal, T newVal,
|
if (_axisStatusChangedToken is not null)
|
||||||
EventHandler<StateChangedEventArgs<T>> handler, string propertyName)
|
{
|
||||||
|
_eventAggregator.GetEvent<AxisStatusChangedEvent>().Unsubscribe(_axisStatusChangedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_geometryUpdatedToken is not null)
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<GeometryUpdatedEvent>().Unsubscribe(_geometryUpdatedToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
MotionStateChanged = null;
|
||||||
|
RaySourceStateChanged = null;
|
||||||
|
DetectorStateChanged = null;
|
||||||
|
SystemStateChanged = null;
|
||||||
|
CameraStateChanged = null;
|
||||||
|
LinkedViewStateChanged = null;
|
||||||
|
RecipeExecutionStateChanged = null;
|
||||||
|
LinkedViewRequested = null;
|
||||||
|
|
||||||
|
_logger.Info("AppStateService disposed");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RaiseOnDispatcher<T>(
|
||||||
|
T oldVal,
|
||||||
|
T newVal,
|
||||||
|
EventHandler<StateChangedEventArgs<T>> handler,
|
||||||
|
string propertyName)
|
||||||
{
|
{
|
||||||
_dispatcher.BeginInvoke(() =>
|
_dispatcher.BeginInvoke(() =>
|
||||||
{
|
{
|
||||||
@@ -205,34 +287,85 @@ namespace XplorePlane.Services.AppState
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
_logger.Error(ex, "状态变更事件处理器抛出异常 (property={PropertyName})", propertyName);
|
_logger.Error(ex, "State changed handler failed (property={PropertyName})", propertyName);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SubscribeToExistingServices()
|
private void SubscribeToExistingServices()
|
||||||
{
|
{
|
||||||
_logger.Info("AppStateService 已准备好接收外部服务状态更新");
|
if (TryRefreshMotionStateFromHardware("initialization"))
|
||||||
|
{
|
||||||
|
_logger.Info("AppStateService subscribed to motion hardware state");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_logger.Warn("AppStateService could not initialize motion state from hardware");
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Dispose ──
|
private void OnAxisStatusChanged(AxisStatusChangedData _)
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
{
|
||||||
if (_disposed) return;
|
if (_disposed) return;
|
||||||
_disposed = true;
|
TryRefreshMotionStateFromHardware("axis-status-changed");
|
||||||
|
}
|
||||||
|
|
||||||
// 清除所有事件订阅
|
private void OnGeometryUpdated(GeometryData geometry)
|
||||||
MotionStateChanged = null;
|
{
|
||||||
RaySourceStateChanged = null;
|
if (_disposed) return;
|
||||||
DetectorStateChanged = null;
|
|
||||||
SystemStateChanged = null;
|
|
||||||
CameraStateChanged = null;
|
|
||||||
LinkedViewStateChanged = null;
|
|
||||||
RecipeExecutionStateChanged = null;
|
|
||||||
LinkedViewRequested = null;
|
|
||||||
|
|
||||||
_logger.Info("AppStateService 已释放");
|
_latestGeometry = geometry;
|
||||||
|
TryRefreshMotionStateFromHardware("geometry-updated");
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryRefreshMotionStateFromHardware(string reason)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (_latestGeometry is null)
|
||||||
|
{
|
||||||
|
var geometry = _motionControlService.GetCurrentGeometry();
|
||||||
|
_latestGeometry = new GeometryData(geometry.FOD, geometry.FDD, geometry.Magnification);
|
||||||
|
}
|
||||||
|
|
||||||
|
SetMotionState(BuildMotionStateSnapshot(_latestGeometry));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.Warn("Failed to refresh motion state from hardware during {Reason}: {Message}", reason, ex.Message);
|
||||||
|
_logger.Error(ex, "Motion state refresh exception during {Reason}", reason);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private MotionState BuildMotionStateSnapshot(GeometryData geometry)
|
||||||
|
{
|
||||||
|
var stageX = _motionSystem.GetLinearAxis(AxisId.StageX);
|
||||||
|
var stageY = _motionSystem.GetLinearAxis(AxisId.StageY);
|
||||||
|
var sourceZ = _motionSystem.GetLinearAxis(AxisId.SourceZ);
|
||||||
|
var detectorZ = _motionSystem.GetLinearAxis(AxisId.DetectorZ);
|
||||||
|
var detectorSwing = _motionSystem.GetRotaryAxis(RotaryAxisId.DetectorSwing);
|
||||||
|
|
||||||
|
return new MotionState(
|
||||||
|
XM: stageX.ActualPosition,
|
||||||
|
YM: stageY.ActualPosition,
|
||||||
|
ZT: sourceZ.ActualPosition,
|
||||||
|
ZD: detectorZ.ActualPosition,
|
||||||
|
TiltD: detectorSwing.ActualAngle,
|
||||||
|
Dist: geometry?.FDD ?? 0,
|
||||||
|
XMSpeed: 0,
|
||||||
|
YMSpeed: 0,
|
||||||
|
ZTSpeed: 0,
|
||||||
|
ZDSpeed: 0,
|
||||||
|
TiltDSpeed: 0,
|
||||||
|
DistSpeed: 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetMotionState(MotionState newState)
|
||||||
|
{
|
||||||
|
var old = Interlocked.Exchange(ref _motionState, newState);
|
||||||
|
if (ReferenceEquals(old, newState)) return;
|
||||||
|
RaiseOnDispatcher(old, newState, MotionStateChanged, nameof(MotionState));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
using System;
|
using System;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
using Prism.Mvvm;
|
using Prism.Mvvm;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.Win32;
|
using Microsoft.Win32;
|
||||||
using Prism.Commands;
|
using Prism.Commands;
|
||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Window
|
<Window
|
||||||
x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
|
x:Class="XplorePlane.Views.Cnc.CncEditorWindow"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<UserControl
|
<UserControl
|
||||||
x:Class="XplorePlane.Views.PipelineEditorView"
|
x:Class="XplorePlane.Views.PipelineEditorView"
|
||||||
x:Name="RootControl"
|
x:Name="RootControl"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<UserControl
|
<UserControl
|
||||||
x:Class="XplorePlane.Views.ImagePanelView"
|
x:Class="XplorePlane.Views.ImagePanelView"
|
||||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<OutputType>WinExe</OutputType>
|
<OutputType>WinExe</OutputType>
|
||||||
<TargetFramework>net8.0-windows</TargetFramework>
|
<TargetFramework>net8.0-windows</TargetFramework>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
<Project ToolsVersion="Current" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||||
<PropertyGroup />
|
<PropertyGroup />
|
||||||
</Project>
|
</Project>
|
||||||
Reference in New Issue
Block a user