新增调试页面
This commit is contained in:
@@ -0,0 +1,338 @@
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using Xunit;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.Debug;
|
||||
|
||||
namespace XplorePlane.Tests.Services
|
||||
{
|
||||
/// <summary>
|
||||
/// 测试 DebugPanelConfigService(任务 1.2)
|
||||
/// Tests DebugPanelConfigService (Task 1.2)
|
||||
///
|
||||
/// **Validates: Requirements 12.1-12.7**
|
||||
/// </summary>
|
||||
public class DebugPanelConfigServiceTests : IDisposable
|
||||
{
|
||||
private readonly string _tempDir;
|
||||
private readonly string _configPath;
|
||||
private readonly Mock<ILoggerService> _mockLogger;
|
||||
private readonly Mock<ILoggerService> _mockModuleLogger;
|
||||
private readonly DebugPanelConfigService _service;
|
||||
|
||||
public DebugPanelConfigServiceTests()
|
||||
{
|
||||
// 创建临时测试目录
|
||||
_tempDir = Path.Combine(Path.GetTempPath(), "XplorePlaneTests", Guid.NewGuid().ToString());
|
||||
Directory.CreateDirectory(_tempDir);
|
||||
_configPath = Path.Combine(_tempDir, "DebugPanel.config");
|
||||
|
||||
// 设置 mock logger
|
||||
_mockModuleLogger = new Mock<ILoggerService>();
|
||||
_mockLogger = new Mock<ILoggerService>();
|
||||
_mockLogger.Setup(l => l.ForModule<DebugPanelConfigService>())
|
||||
.Returns(_mockModuleLogger.Object);
|
||||
|
||||
// 使用反射创建服务实例并设置配置路径
|
||||
_service = new DebugPanelConfigService(_mockLogger.Object);
|
||||
|
||||
// 使用反射修改私有字段 _configPath
|
||||
var field = typeof(DebugPanelConfigService).GetField("_configPath",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
field?.SetValue(_service, _configPath);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
// 清理临时目录
|
||||
if (Directory.Exists(_tempDir))
|
||||
{
|
||||
Directory.Delete(_tempDir, true);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:当配置文件不存在时,LoadConfig 应返回默认配置
|
||||
/// **Validates: Requirement 12.7**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LoadConfig_WhenFileDoesNotExist_ReturnsDefaultConfig()
|
||||
{
|
||||
// Act
|
||||
var config = _service.LoadConfig();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(config);
|
||||
Assert.NotNull(config.Window);
|
||||
Assert.Equal(1200, config.Window.Width);
|
||||
Assert.Equal(800, config.Window.Height);
|
||||
Assert.Equal(WindowState.Normal, config.Window.State);
|
||||
Assert.NotNull(config.EventFilters);
|
||||
Assert.Equal(8, config.EventFilters.Count);
|
||||
Assert.True(config.EventFilters["MotionState"]);
|
||||
Assert.True(config.EventFilters["RaySourceState"]);
|
||||
Assert.True(config.EventFilters["DetectorState"]);
|
||||
Assert.True(config.EventFilters["SystemState"]);
|
||||
Assert.True(config.EventFilters["CameraState"]);
|
||||
Assert.True(config.EventFilters["LinkedViewState"]);
|
||||
Assert.True(config.EventFilters["RecipeExecutionState"]);
|
||||
Assert.True(config.EventFilters["CalibrationMatrix"]);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:SaveConfig 应成功保存配置到 JSON 文件
|
||||
/// **Validates: Requirements 12.1-12.5**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SaveConfig_WithValidConfig_SavesSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var config = new DebugPanelConfig
|
||||
{
|
||||
Window = new WindowConfig
|
||||
{
|
||||
Left = 150,
|
||||
Top = 200,
|
||||
Width = 1400,
|
||||
Height = 900,
|
||||
State = WindowState.Maximized
|
||||
},
|
||||
EventFilters = new Dictionary<string, bool>
|
||||
{
|
||||
["MotionState"] = false,
|
||||
["RaySourceState"] = true
|
||||
},
|
||||
DockingLayout = "<Layout>Test</Layout>"
|
||||
};
|
||||
|
||||
// Act
|
||||
_service.SaveConfig(config);
|
||||
|
||||
// Assert
|
||||
Assert.True(File.Exists(_configPath));
|
||||
var json = File.ReadAllText(_configPath);
|
||||
Assert.Contains("\"Width\": 1400", json);
|
||||
Assert.Contains("\"Height\": 900", json);
|
||||
Assert.Contains("\"State\": 2", json); // WindowState.Maximized = 2
|
||||
Assert.Contains("\"MotionState\": false", json);
|
||||
Assert.Contains("\"RaySourceState\": true", json);
|
||||
Assert.Contains("\"DockingLayout\": \"<Layout>Test</Layout>\"", json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:LoadConfig 应成功从 JSON 文件加载配置
|
||||
/// **Validates: Requirement 12.6**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LoadConfig_WithExistingFile_LoadsSuccessfully()
|
||||
{
|
||||
// Arrange
|
||||
var originalConfig = new DebugPanelConfig
|
||||
{
|
||||
Window = new WindowConfig
|
||||
{
|
||||
Left = 250,
|
||||
Top = 300,
|
||||
Width = 1600,
|
||||
Height = 1000,
|
||||
State = WindowState.Normal
|
||||
},
|
||||
EventFilters = new Dictionary<string, bool>
|
||||
{
|
||||
["MotionState"] = true,
|
||||
["RaySourceState"] = false,
|
||||
["DetectorState"] = true
|
||||
},
|
||||
DockingLayout = "<Layout>Custom</Layout>"
|
||||
};
|
||||
_service.SaveConfig(originalConfig);
|
||||
|
||||
// Act
|
||||
var loadedConfig = _service.LoadConfig();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(loadedConfig);
|
||||
Assert.NotNull(loadedConfig.Window);
|
||||
Assert.Equal(250, loadedConfig.Window.Left);
|
||||
Assert.Equal(300, loadedConfig.Window.Top);
|
||||
Assert.Equal(1600, loadedConfig.Window.Width);
|
||||
Assert.Equal(1000, loadedConfig.Window.Height);
|
||||
Assert.Equal(WindowState.Normal, loadedConfig.Window.State);
|
||||
Assert.NotNull(loadedConfig.EventFilters);
|
||||
Assert.True(loadedConfig.EventFilters["MotionState"]);
|
||||
Assert.False(loadedConfig.EventFilters["RaySourceState"]);
|
||||
Assert.True(loadedConfig.EventFilters["DetectorState"]);
|
||||
Assert.Equal("<Layout>Custom</Layout>", loadedConfig.DockingLayout);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:SaveConfig 应创建不存在的目录
|
||||
/// **Validates: Requirement 12.4**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SaveConfig_WhenDirectoryDoesNotExist_CreatesDirectory()
|
||||
{
|
||||
// Arrange
|
||||
var nestedPath = Path.Combine(_tempDir, "nested", "path", "DebugPanel.config");
|
||||
var field = typeof(DebugPanelConfigService).GetField("_configPath",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
field?.SetValue(_service, nestedPath);
|
||||
|
||||
var config = new DebugPanelConfig
|
||||
{
|
||||
Window = new WindowConfig { Width = 1200, Height = 800, State = WindowState.Normal },
|
||||
EventFilters = new Dictionary<string, bool>()
|
||||
};
|
||||
|
||||
// Act
|
||||
_service.SaveConfig(config);
|
||||
|
||||
// Assert
|
||||
Assert.True(File.Exists(nestedPath));
|
||||
Assert.True(Directory.Exists(Path.GetDirectoryName(nestedPath)));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:SaveConfig 使用 null 配置应记录警告并不抛出异常
|
||||
/// **Validates: Requirement 12.7**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SaveConfig_WithNullConfig_LogsWarningAndDoesNotThrow()
|
||||
{
|
||||
// Act & Assert
|
||||
var exception = Record.Exception(() => _service.SaveConfig(null));
|
||||
Assert.Null(exception);
|
||||
|
||||
// 验证记录了警告日志
|
||||
_mockModuleLogger.Verify(
|
||||
l => l.Warn(It.IsAny<string>(), It.IsAny<object[]>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:LoadConfig 遇到损坏的 JSON 文件应返回默认配置并记录警告
|
||||
/// **Validates: Requirement 12.7**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void LoadConfig_WithCorruptedFile_ReturnsDefaultConfigAndLogsWarning()
|
||||
{
|
||||
// Arrange
|
||||
File.WriteAllText(_configPath, "{ invalid json content }");
|
||||
|
||||
// Act
|
||||
var config = _service.LoadConfig();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(config);
|
||||
Assert.Equal(1200, config.Window.Width);
|
||||
Assert.Equal(800, config.Window.Height);
|
||||
|
||||
// 验证记录了错误日志
|
||||
_mockModuleLogger.Verify(
|
||||
l => l.Error(It.IsAny<Exception>(), It.IsAny<string>(), It.IsAny<object[]>()),
|
||||
Times.Once);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:配置文件路径应使用 %AppData%\XplorePlane\DebugPanel.config
|
||||
/// **Validates: Requirement 12.4**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_SetsCorrectConfigPath()
|
||||
{
|
||||
// Arrange
|
||||
var freshService = new DebugPanelConfigService(_mockLogger.Object);
|
||||
var field = typeof(DebugPanelConfigService).GetField("_configPath",
|
||||
System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
|
||||
// Act
|
||||
var actualPath = field?.GetValue(freshService) as string;
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(actualPath);
|
||||
var expectedPath = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"XplorePlane",
|
||||
"DebugPanel.config");
|
||||
Assert.Equal(expectedPath, actualPath);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:配置文件应使用 JSON 格式
|
||||
/// **Validates: Requirement 12.5**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SaveConfig_UsesJsonFormat()
|
||||
{
|
||||
// Arrange
|
||||
var config = new DebugPanelConfig
|
||||
{
|
||||
Window = new WindowConfig { Width = 1200, Height = 800, State = WindowState.Normal },
|
||||
EventFilters = new Dictionary<string, bool> { ["Test"] = true }
|
||||
};
|
||||
|
||||
// Act
|
||||
_service.SaveConfig(config);
|
||||
|
||||
// Assert
|
||||
var json = File.ReadAllText(_configPath);
|
||||
Assert.StartsWith("{", json.Trim());
|
||||
Assert.EndsWith("}", json.Trim());
|
||||
Assert.Contains("\"Window\":", json);
|
||||
Assert.Contains("\"EventFilters\":", json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:默认配置应包含所有 8 种状态类型的过滤器
|
||||
/// **Validates: Requirement 12.3**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetDefaultConfig_ContainsAllStateTypeFilters()
|
||||
{
|
||||
// Act
|
||||
var config = _service.LoadConfig();
|
||||
|
||||
// Assert
|
||||
Assert.NotNull(config.EventFilters);
|
||||
Assert.Equal(8, config.EventFilters.Count);
|
||||
Assert.Contains("MotionState", config.EventFilters.Keys);
|
||||
Assert.Contains("RaySourceState", config.EventFilters.Keys);
|
||||
Assert.Contains("DetectorState", config.EventFilters.Keys);
|
||||
Assert.Contains("SystemState", config.EventFilters.Keys);
|
||||
Assert.Contains("CameraState", config.EventFilters.Keys);
|
||||
Assert.Contains("LinkedViewState", config.EventFilters.Keys);
|
||||
Assert.Contains("RecipeExecutionState", config.EventFilters.Keys);
|
||||
Assert.Contains("CalibrationMatrix", config.EventFilters.Keys);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:默认窗口配置应为 1200×800 像素
|
||||
/// **Validates: Requirements 12.1**
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void GetDefaultConfig_HasCorrectWindowSize()
|
||||
{
|
||||
// Act
|
||||
var config = _service.LoadConfig();
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1200, config.Window.Width);
|
||||
Assert.Equal(800, config.Window.Height);
|
||||
Assert.Equal(WindowState.Normal, config.Window.State);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 测试:构造函数应拒绝 null logger
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void Constructor_WithNullLogger_ThrowsArgumentNullException()
|
||||
{
|
||||
// Act & Assert
|
||||
Assert.Throws<ArgumentNullException>(() => new DebugPanelConfigService(null));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows.Threading;
|
||||
using Xunit;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.ViewModels.Debug;
|
||||
|
||||
namespace XplorePlane.Tests.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// EventLogViewModel 单元测试
|
||||
/// Unit tests for EventLogViewModel
|
||||
/// </summary>
|
||||
public class EventLogViewModelTests
|
||||
{
|
||||
private readonly Mock<IAppStateService> _mockAppStateService;
|
||||
private readonly Mock<ILoggerService> _mockLoggerService;
|
||||
private readonly Dispatcher _dispatcher;
|
||||
|
||||
public EventLogViewModelTests()
|
||||
{
|
||||
_mockAppStateService = new Mock<IAppStateService>();
|
||||
_mockLoggerService = new Mock<ILoggerService>();
|
||||
|
||||
// 创建 Dispatcher(需要在 STA 线程上)
|
||||
// Create Dispatcher (needs to be on STA thread)
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
|
||||
// 设置 logger mock 返回自身(链式调用)
|
||||
// Setup logger mock to return itself (method chaining)
|
||||
_mockLoggerService.Setup(l => l.ForModule<EventLogViewModel>())
|
||||
.Returns(_mockLoggerService.Object);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesFilterOptionsWithAllStatesEnabled()
|
||||
{
|
||||
// Arrange & Act
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(8, viewModel.FilterOptions.Count);
|
||||
Assert.True(viewModel.FilterOptions["MotionState"]);
|
||||
Assert.True(viewModel.FilterOptions["RaySourceState"]);
|
||||
Assert.True(viewModel.FilterOptions["DetectorState"]);
|
||||
Assert.True(viewModel.FilterOptions["SystemState"]);
|
||||
Assert.True(viewModel.FilterOptions["CameraState"]);
|
||||
Assert.True(viewModel.FilterOptions["LinkedViewState"]);
|
||||
Assert.True(viewModel.FilterOptions["RecipeExecutionState"]);
|
||||
Assert.True(viewModel.FilterOptions["CalibrationMatrix"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ToggleFilterCommand_TogglesFilterState()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
var initialState = viewModel.FilterOptions["MotionState"];
|
||||
|
||||
// Act
|
||||
viewModel.ToggleFilterCommand.Execute("MotionState");
|
||||
|
||||
// Assert
|
||||
Assert.Equal(!initialState, viewModel.FilterOptions["MotionState"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SelectAllFiltersCommand_EnablesAllFilters()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// 先禁用所有过滤器
|
||||
// First disable all filters
|
||||
viewModel.ClearAllFiltersCommand.Execute();
|
||||
|
||||
// Act
|
||||
viewModel.SelectAllFiltersCommand.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.All(viewModel.FilterOptions.Values, value => Assert.True(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ClearAllFiltersCommand_DisablesAllFilters()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Act
|
||||
viewModel.ClearAllFiltersCommand.Execute();
|
||||
|
||||
// Assert
|
||||
Assert.All(viewModel.FilterOptions.Values, value => Assert.False(value));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ApplyFilter_FiltersEventLogCorrectly()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// 添加测试数据
|
||||
// Add test data
|
||||
viewModel.EventLog.Add(new EventLogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
EventType = "MotionStateChanged",
|
||||
FieldName = "StageX",
|
||||
OldValue = "0",
|
||||
NewValue = "100",
|
||||
Category = "MotionState"
|
||||
});
|
||||
|
||||
viewModel.EventLog.Add(new EventLogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
EventType = "RaySourceStateChanged",
|
||||
FieldName = "IsOn",
|
||||
OldValue = "False",
|
||||
NewValue = "True",
|
||||
Category = "RaySourceState"
|
||||
});
|
||||
|
||||
// 禁用 RaySourceState 过滤器
|
||||
// Disable RaySourceState filter
|
||||
viewModel.FilterOptions["RaySourceState"] = false;
|
||||
|
||||
// Act
|
||||
viewModel.ToggleFilterCommand.Execute("MotionState"); // 触发过滤器应用
|
||||
viewModel.ToggleFilterCommand.Execute("MotionState"); // 恢复状态
|
||||
|
||||
// Assert
|
||||
Assert.Single(viewModel.FilteredEventLog);
|
||||
Assert.Equal("MotionState", viewModel.FilteredEventLog[0].Category);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void EventLog_LimitsTo1000Records()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Act - 添加 1001 条记录
|
||||
// Act - Add 1001 records
|
||||
for (int i = 0; i < 1001; i++)
|
||||
{
|
||||
viewModel.EventLog.Add(new EventLogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
EventType = "MotionStateChanged",
|
||||
FieldName = "StageX",
|
||||
OldValue = i.ToString(),
|
||||
NewValue = (i + 1).ToString(),
|
||||
Category = "MotionState"
|
||||
});
|
||||
}
|
||||
|
||||
// Assert
|
||||
Assert.Equal(1000, viewModel.EventLog.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_ClearsCollections()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new EventLogViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
viewModel.EventLog.Add(new EventLogEntry
|
||||
{
|
||||
Timestamp = DateTime.Now,
|
||||
EventType = "MotionStateChanged",
|
||||
FieldName = "StageX",
|
||||
OldValue = "0",
|
||||
NewValue = "100",
|
||||
Category = "MotionState"
|
||||
});
|
||||
|
||||
// Act
|
||||
viewModel.Dispose();
|
||||
|
||||
// Assert
|
||||
Assert.Empty(viewModel.EventLog);
|
||||
Assert.Empty(viewModel.FilteredEventLog);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,320 @@
|
||||
using Moq;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Threading;
|
||||
using Xunit;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XplorePlane.Models;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.ViewModels.Debug;
|
||||
|
||||
namespace XplorePlane.Tests.ViewModels
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态显示 ViewModel 单元测试
|
||||
/// StateDisplayViewModel unit tests
|
||||
/// </summary>
|
||||
public class StateDisplayViewModelTests
|
||||
{
|
||||
private readonly Mock<IAppStateService> _mockAppStateService;
|
||||
private readonly Mock<ILoggerService> _mockLoggerService;
|
||||
private readonly Mock<ILoggerService> _mockModuleLogger;
|
||||
private readonly Dispatcher _dispatcher;
|
||||
|
||||
public StateDisplayViewModelTests()
|
||||
{
|
||||
_mockAppStateService = new Mock<IAppStateService>();
|
||||
_mockLoggerService = new Mock<ILoggerService>();
|
||||
_mockModuleLogger = new Mock<ILoggerService>();
|
||||
|
||||
// Setup logger to return module logger
|
||||
_mockLoggerService
|
||||
.Setup(l => l.ForModule<StateDisplayViewModel>())
|
||||
.Returns(_mockModuleLogger.Object);
|
||||
|
||||
// Create a dispatcher for the current thread
|
||||
_dispatcher = Dispatcher.CurrentDispatcher;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesStateTree_WithEightRootNodes()
|
||||
{
|
||||
// Arrange & Act
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Assert
|
||||
Assert.Equal(8, viewModel.StateTree.Count);
|
||||
Assert.Contains(viewModel.StateTree, n => n.Name == "MotionState");
|
||||
Assert.Contains(viewModel.StateTree, n => n.Name == "RaySourceState");
|
||||
Assert.Contains(viewModel.StateTree, n => n.Name == "DetectorState");
|
||||
Assert.Contains(viewModel.StateTree, n => n.Name == "SystemState");
|
||||
Assert.Contains(viewModel.StateTree, n => n.Name == "CameraState");
|
||||
Assert.Contains(viewModel.StateTree, n => n.Name == "LinkedViewState");
|
||||
Assert.Contains(viewModel.StateTree, n => n.Name == "RecipeExecutionState");
|
||||
Assert.Contains(viewModel.StateTree, n => n.Name == "CalibrationMatrix");
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Constructor_InitializesMotionStateFields_WithCorrectCount()
|
||||
{
|
||||
// Arrange & Act
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Assert
|
||||
var motionStateNode = viewModel.StateTree.First(n => n.Name == "MotionState");
|
||||
Assert.Equal(18, motionStateNode.Children.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Initialize_SubscribesToAllStateChangeEvents()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
|
||||
// Act
|
||||
viewModel.Initialize();
|
||||
|
||||
// Assert
|
||||
_mockAppStateService.VerifyAdd(s => s.MotionStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<MotionState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyAdd(s => s.RaySourceStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<RaySourceState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyAdd(s => s.DetectorStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<DetectorState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyAdd(s => s.SystemStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<SystemState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyAdd(s => s.CameraStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<CameraState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyAdd(s => s.LinkedViewStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<LinkedViewState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyAdd(s => s.RecipeExecutionStateChanged += It.IsAny<EventHandler<StateChangedEventArgs<RecipeExecutionState>>>(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dispose_UnsubscribesFromAllStateChangeEvents()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
viewModel.Initialize();
|
||||
|
||||
// Act
|
||||
viewModel.Dispose();
|
||||
|
||||
// Assert
|
||||
_mockAppStateService.VerifyRemove(s => s.MotionStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<MotionState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyRemove(s => s.RaySourceStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<RaySourceState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyRemove(s => s.DetectorStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<DetectorState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyRemove(s => s.SystemStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<SystemState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyRemove(s => s.CameraStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<CameraState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyRemove(s => s.LinkedViewStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<LinkedViewState>>>(), Times.Once);
|
||||
_mockAppStateService.VerifyRemove(s => s.RecipeExecutionStateChanged -= It.IsAny<EventHandler<StateChangedEventArgs<RecipeExecutionState>>>(), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StateChange_UpdatesNodeValue_AndSetsHighlight()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
viewModel.Initialize();
|
||||
|
||||
var oldState = new RaySourceState(false, 100.0, 5.0);
|
||||
var newState = new RaySourceState(true, 160.0, 8.0);
|
||||
|
||||
// Act
|
||||
_mockAppStateService.Raise(
|
||||
s => s.RaySourceStateChanged += null,
|
||||
_mockAppStateService.Object,
|
||||
new StateChangedEventArgs<RaySourceState>(oldState, newState));
|
||||
|
||||
// Process dispatcher queue
|
||||
DoEvents();
|
||||
|
||||
// Assert
|
||||
var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState");
|
||||
var isOnNode = raySourceNode.Children.First(n => n.Name == "IsOn");
|
||||
var voltageNode = raySourceNode.Children.First(n => n.Name == "Voltage");
|
||||
var powerNode = raySourceNode.Children.First(n => n.Name == "Power");
|
||||
|
||||
Assert.Equal("True", isOnNode.Value);
|
||||
Assert.True(isOnNode.IsHighlighted);
|
||||
Assert.Equal("Green", isOnNode.HighlightColor); // Boolean true -> Green
|
||||
|
||||
Assert.Equal("160.00", voltageNode.Value);
|
||||
Assert.True(voltageNode.IsHighlighted);
|
||||
Assert.Equal("Green", voltageNode.HighlightColor); // Increase -> Green
|
||||
|
||||
Assert.Equal("8.00", powerNode.Value);
|
||||
Assert.True(powerNode.IsHighlighted);
|
||||
Assert.Equal("Green", powerNode.HighlightColor); // Increase -> Green
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StateChange_BooleanFalse_SetsRedHighlight()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
viewModel.Initialize();
|
||||
|
||||
var oldState = new RaySourceState(true, 160.0, 8.0);
|
||||
var newState = new RaySourceState(false, 160.0, 8.0);
|
||||
|
||||
// Act
|
||||
_mockAppStateService.Raise(
|
||||
s => s.RaySourceStateChanged += null,
|
||||
_mockAppStateService.Object,
|
||||
new StateChangedEventArgs<RaySourceState>(oldState, newState));
|
||||
|
||||
// Process dispatcher queue
|
||||
DoEvents();
|
||||
|
||||
// Assert
|
||||
var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState");
|
||||
var isOnNode = raySourceNode.Children.First(n => n.Name == "IsOn");
|
||||
|
||||
Assert.Equal("False", isOnNode.Value);
|
||||
Assert.True(isOnNode.IsHighlighted);
|
||||
Assert.Equal("Red", isOnNode.HighlightColor); // Boolean false -> Red
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StateChange_NumericDecrease_SetsRedHighlight()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
viewModel.Initialize();
|
||||
|
||||
var oldState = new RaySourceState(true, 160.0, 8.0);
|
||||
var newState = new RaySourceState(true, 120.0, 5.0);
|
||||
|
||||
// Act
|
||||
_mockAppStateService.Raise(
|
||||
s => s.RaySourceStateChanged += null,
|
||||
_mockAppStateService.Object,
|
||||
new StateChangedEventArgs<RaySourceState>(oldState, newState));
|
||||
|
||||
// Process dispatcher queue
|
||||
DoEvents();
|
||||
|
||||
// Assert
|
||||
var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState");
|
||||
var voltageNode = raySourceNode.Children.First(n => n.Name == "Voltage");
|
||||
var powerNode = raySourceNode.Children.First(n => n.Name == "Power");
|
||||
|
||||
Assert.Equal("120.00", voltageNode.Value);
|
||||
Assert.True(voltageNode.IsHighlighted);
|
||||
Assert.Equal("Red", voltageNode.HighlightColor); // Decrease -> Red
|
||||
|
||||
Assert.Equal("5.00", powerNode.Value);
|
||||
Assert.True(powerNode.IsHighlighted);
|
||||
Assert.Equal("Red", powerNode.HighlightColor); // Decrease -> Red
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task StateChange_HighlightClearsAfterTwoSeconds()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
viewModel.Initialize();
|
||||
|
||||
var oldState = new RaySourceState(false, 100.0, 5.0);
|
||||
var newState = new RaySourceState(true, 160.0, 8.0);
|
||||
|
||||
// Act
|
||||
_mockAppStateService.Raise(
|
||||
s => s.RaySourceStateChanged += null,
|
||||
_mockAppStateService.Object,
|
||||
new StateChangedEventArgs<RaySourceState>(oldState, newState));
|
||||
|
||||
// Process dispatcher queue
|
||||
DoEvents();
|
||||
|
||||
var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState");
|
||||
var isOnNode = raySourceNode.Children.First(n => n.Name == "IsOn");
|
||||
|
||||
// Verify highlight is set
|
||||
Assert.True(isOnNode.IsHighlighted);
|
||||
|
||||
// Wait for 2.5 seconds (2 seconds delay + buffer)
|
||||
await Task.Delay(2500);
|
||||
DoEvents();
|
||||
|
||||
// Assert - highlight should be cleared
|
||||
Assert.False(isOnNode.IsHighlighted);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void StateChange_NoChange_DoesNotSetHighlight()
|
||||
{
|
||||
// Arrange
|
||||
var viewModel = new StateDisplayViewModel(
|
||||
_mockAppStateService.Object,
|
||||
_mockLoggerService.Object,
|
||||
_dispatcher);
|
||||
viewModel.Initialize();
|
||||
|
||||
var oldState = new RaySourceState(true, 160.0, 8.0);
|
||||
var newState = new RaySourceState(true, 160.0, 8.0);
|
||||
|
||||
// Act
|
||||
_mockAppStateService.Raise(
|
||||
s => s.RaySourceStateChanged += null,
|
||||
_mockAppStateService.Object,
|
||||
new StateChangedEventArgs<RaySourceState>(oldState, newState));
|
||||
|
||||
// Process dispatcher queue
|
||||
DoEvents();
|
||||
|
||||
// Assert
|
||||
var raySourceNode = viewModel.StateTree.First(n => n.Name == "RaySourceState");
|
||||
var isOnNode = raySourceNode.Children.First(n => n.Name == "IsOn");
|
||||
var voltageNode = raySourceNode.Children.First(n => n.Name == "Voltage");
|
||||
var powerNode = raySourceNode.Children.First(n => n.Name == "Power");
|
||||
|
||||
// Values should be updated but not highlighted
|
||||
Assert.Equal("True", isOnNode.Value);
|
||||
Assert.False(isOnNode.IsHighlighted);
|
||||
|
||||
Assert.Equal("160.00", voltageNode.Value);
|
||||
Assert.False(voltageNode.IsHighlighted);
|
||||
|
||||
Assert.Equal("8.00", powerNode.Value);
|
||||
Assert.False(powerNode.IsHighlighted);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 处理 Dispatcher 队列中的所有待处理消息
|
||||
/// Process all pending messages in the Dispatcher queue
|
||||
/// </summary>
|
||||
private void DoEvents()
|
||||
{
|
||||
var frame = new DispatcherFrame();
|
||||
_dispatcher.BeginInvoke(DispatcherPriority.Background, new DispatcherOperationCallback(
|
||||
delegate (object f)
|
||||
{
|
||||
((DispatcherFrame)f).Continue = false;
|
||||
return null;
|
||||
}), frame);
|
||||
Dispatcher.PushFrame(frame);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,7 @@ using XplorePlane.Services;
|
||||
using XplorePlane.Services.AppState;
|
||||
using XplorePlane.Services.Camera;
|
||||
using XplorePlane.Services.Cnc;
|
||||
using XplorePlane.Services.Debug;
|
||||
using XplorePlane.Services.InspectionResults;
|
||||
using XplorePlane.Services.MainViewport;
|
||||
using XplorePlane.Services.Matrix;
|
||||
@@ -45,8 +46,10 @@ using XplorePlane.Services.Recording;
|
||||
using XplorePlane.Services.Storage;
|
||||
using XplorePlane.ViewModels;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
using XplorePlane.ViewModels.Debug;
|
||||
using XplorePlane.Views;
|
||||
using XplorePlane.Views.Cnc;
|
||||
using XplorePlane.Views.Debug;
|
||||
|
||||
namespace XplorePlane
|
||||
{
|
||||
@@ -438,6 +441,9 @@ namespace XplorePlane
|
||||
// 注册全局状态服务(单例)
|
||||
containerRegistry.RegisterSingleton<IAppStateService, AppStateService>();
|
||||
containerRegistry.RegisterSingleton<IXpDataPathService, XpDataPathService>();
|
||||
containerRegistry.RegisterSingleton<IDebugPanelConfigService, DebugPanelConfigService>();
|
||||
containerRegistry.Register<DebugPanelViewModel>();
|
||||
containerRegistry.Register<DebugPanelWindow>();
|
||||
|
||||
// 注册检测配方服务(单例)
|
||||
containerRegistry.RegisterSingleton<IRecipeService, RecipeService>();
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace XplorePlane.Converters
|
||||
{
|
||||
public class HighlightColorBrushConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return value switch
|
||||
{
|
||||
"Green" => Brushes.LightGreen,
|
||||
"Red" => Brushes.LightCoral,
|
||||
"Yellow" => Brushes.Khaki,
|
||||
_ => Brushes.Transparent
|
||||
};
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace XplorePlane.Converters
|
||||
{
|
||||
public class TimestampFormatConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is DateTime timestamp)
|
||||
{
|
||||
return timestamp.ToString("HH:mm:ss.fff", CultureInfo.InvariantCulture);
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Windows.Data;
|
||||
using XplorePlane.ViewModels.Debug;
|
||||
|
||||
namespace XplorePlane.Converters
|
||||
{
|
||||
public class ValueFormatConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
return DebugPanelStateFormatter.FormatValue(value);
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
using Prism.Mvvm;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Windows;
|
||||
|
||||
namespace XplorePlane.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 状态树节点 ViewModel(用于 TreeView 绑定)
|
||||
/// State tree node ViewModel for TreeView binding
|
||||
/// </summary>
|
||||
public class StateNodeViewModel : BindableBase
|
||||
{
|
||||
private string _value;
|
||||
private bool _isHighlighted;
|
||||
private string _highlightColor;
|
||||
|
||||
/// <summary>字段名称 | Field name</summary>
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>字段值(格式化字符串)| Field value (formatted string)</summary>
|
||||
public string Value
|
||||
{
|
||||
get => _value;
|
||||
set => SetProperty(ref _value, value);
|
||||
}
|
||||
|
||||
/// <summary>状态类别(MotionState, RaySourceState 等)| State category</summary>
|
||||
public string Category { get; set; }
|
||||
|
||||
/// <summary>是否高亮 | Whether highlighted</summary>
|
||||
public bool IsHighlighted
|
||||
{
|
||||
get => _isHighlighted;
|
||||
set => SetProperty(ref _isHighlighted, value);
|
||||
}
|
||||
|
||||
/// <summary>高亮颜色(Green/Red/Yellow)| Highlight color</summary>
|
||||
public string HighlightColor
|
||||
{
|
||||
get => _highlightColor;
|
||||
set => SetProperty(ref _highlightColor, value);
|
||||
}
|
||||
|
||||
/// <summary>子节点集合 | Child nodes collection</summary>
|
||||
public ObservableCollection<StateNodeViewModel> Children { get; set; } = new ObservableCollection<StateNodeViewModel>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 事件日志条目(用于 GridView 绑定)
|
||||
/// Event log entry for GridView binding
|
||||
/// </summary>
|
||||
public class EventLogEntry
|
||||
{
|
||||
/// <summary>时间戳 | Timestamp</summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>事件类型(MotionStateChanged, RaySourceStateChanged 等)| Event type</summary>
|
||||
public string EventType { get; set; }
|
||||
|
||||
/// <summary>字段名称 | Field name</summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>旧值 | Old value</summary>
|
||||
public string OldValue { get; set; }
|
||||
|
||||
/// <summary>新值 | New value</summary>
|
||||
public string NewValue { get; set; }
|
||||
|
||||
/// <summary>状态类别(用于过滤)| State category (for filtering)</summary>
|
||||
public string Category { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态快照
|
||||
/// State snapshot
|
||||
/// </summary>
|
||||
public class StateSnapshot
|
||||
{
|
||||
/// <summary>快照唯一标识 | Snapshot unique identifier</summary>
|
||||
public Guid Id { get; set; }
|
||||
|
||||
/// <summary>捕获时间戳 | Capture timestamp</summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>运动控制状态 | Motion state</summary>
|
||||
public MotionState MotionState { get; set; }
|
||||
|
||||
/// <summary>射线源状态 | Ray source state</summary>
|
||||
public RaySourceState RaySourceState { get; set; }
|
||||
|
||||
/// <summary>探测器状态 | Detector state</summary>
|
||||
public DetectorState DetectorState { get; set; }
|
||||
|
||||
/// <summary>系统状态 | System state</summary>
|
||||
public SystemState SystemState { get; set; }
|
||||
|
||||
/// <summary>相机状态 | Camera state</summary>
|
||||
public CameraState CameraState { get; set; }
|
||||
|
||||
/// <summary>联动视图状态 | Linked view state</summary>
|
||||
public LinkedViewState LinkedViewState { get; set; }
|
||||
|
||||
/// <summary>配方执行状态 | Recipe execution state</summary>
|
||||
public RecipeExecutionState RecipeExecutionState { get; set; }
|
||||
|
||||
/// <summary>标定矩阵 | Calibration matrix</summary>
|
||||
public CalibrationMatrix CalibrationMatrix { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 状态差异(用于快照对比)
|
||||
/// State difference for snapshot comparison
|
||||
/// </summary>
|
||||
public class StateDifference
|
||||
{
|
||||
/// <summary>状态类别 | State category</summary>
|
||||
public string Category { get; set; }
|
||||
|
||||
/// <summary>字段名称 | Field name</summary>
|
||||
public string FieldName { get; set; }
|
||||
|
||||
/// <summary>快照1的值 | Value from snapshot 1</summary>
|
||||
public string Value1 { get; set; }
|
||||
|
||||
/// <summary>快照2的值 | Value from snapshot 2</summary>
|
||||
public string Value2 { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 性能指标(用于性能监控)
|
||||
/// Performance metric for performance monitoring
|
||||
/// </summary>
|
||||
public class PerformanceMetric : BindableBase
|
||||
{
|
||||
private double _eventsPerSecond;
|
||||
private double _averageLatency;
|
||||
|
||||
/// <summary>状态类型 | State type</summary>
|
||||
public string StateType { get; set; }
|
||||
|
||||
/// <summary>事件频率(事件/秒)| Events per second</summary>
|
||||
public double EventsPerSecond
|
||||
{
|
||||
get => _eventsPerSecond;
|
||||
set => SetProperty(ref _eventsPerSecond, value);
|
||||
}
|
||||
|
||||
/// <summary>平均延迟(毫秒)| Average latency (milliseconds)</summary>
|
||||
public double AverageLatency
|
||||
{
|
||||
get => _averageLatency;
|
||||
set => SetProperty(ref _averageLatency, value);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 趋势数据点(用于 Chart 绑定)
|
||||
/// Trend data point for Chart binding
|
||||
/// </summary>
|
||||
public class TrendDataPoint
|
||||
{
|
||||
/// <summary>时间戳 | Timestamp</summary>
|
||||
public DateTime Timestamp { get; set; }
|
||||
|
||||
/// <summary>数值 | Value</summary>
|
||||
public double Value { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 调试面板配置
|
||||
/// Debug panel configuration
|
||||
/// </summary>
|
||||
public class DebugPanelConfig
|
||||
{
|
||||
/// <summary>窗口配置 | Window configuration</summary>
|
||||
public WindowConfig Window { get; set; }
|
||||
|
||||
/// <summary>事件过滤器设置 | Event filter settings</summary>
|
||||
public Dictionary<string, bool> EventFilters { get; set; }
|
||||
|
||||
/// <summary>Telerik RadDocking 布局 XML | Telerik RadDocking layout XML</summary>
|
||||
public string DockingLayout { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口配置
|
||||
/// Window configuration
|
||||
/// </summary>
|
||||
public class WindowConfig
|
||||
{
|
||||
/// <summary>窗口左边距 | Window left position</summary>
|
||||
public double Left { get; set; }
|
||||
|
||||
/// <summary>窗口顶边距 | Window top position</summary>
|
||||
public double Top { get; set; }
|
||||
|
||||
/// <summary>窗口宽度 | Window width</summary>
|
||||
public double Width { get; set; }
|
||||
|
||||
/// <summary>窗口高度 | Window height</summary>
|
||||
public double Height { get; set; }
|
||||
|
||||
/// <summary>窗口状态 | Window state</summary>
|
||||
public WindowState State { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -17,8 +17,10 @@ using XplorePlane.Services.MainViewport;
|
||||
using XplorePlane.Services.Recording;
|
||||
using XplorePlane.Services.Storage;
|
||||
using XplorePlane.ViewModels.Cnc;
|
||||
using XplorePlane.ViewModels.Debug;
|
||||
using XplorePlane.Views;
|
||||
using XplorePlane.Views.Cnc;
|
||||
using XplorePlane.Views.Debug;
|
||||
using XP.Common.Logging.Interfaces;
|
||||
using XP.Common.GeneralForm.Views;
|
||||
using XP.Common.PdfViewer.Interfaces;
|
||||
@@ -104,6 +106,7 @@ namespace XplorePlane.ViewModels
|
||||
public DelegateCommand OpenUserManualCommand { get; }
|
||||
public DelegateCommand OpenCameraSettingsCommand { get; }
|
||||
public DelegateCommand OpenSettingsCommand { get; }
|
||||
public DelegateCommand OpenDebugPanelCommand { get; }
|
||||
public DelegateCommand BrowseDataRootPathCommand { get; }
|
||||
public DelegateCommand ResetDataRootPathCommand { get; }
|
||||
public DelegateCommand SaveDataRootPathCommand { get; }
|
||||
@@ -228,6 +231,7 @@ namespace XplorePlane.ViewModels
|
||||
private Window _toolboxWindow;
|
||||
private Window _raySourceConfigWindow;
|
||||
private Window _inspectionReportViewerWindow;
|
||||
private Window _debugPanelWindow;
|
||||
private object _imagePanelContent;
|
||||
private GridLength _viewportPanelWidth = new(1, GridUnitType.Star);
|
||||
private GridLength _imagePanelWidth = new(320);
|
||||
@@ -303,6 +307,7 @@ namespace XplorePlane.ViewModels
|
||||
OpenUserManualCommand = new DelegateCommand(ExecuteOpenUserManual);
|
||||
OpenCameraSettingsCommand = new DelegateCommand(ExecuteOpenCameraSettings);
|
||||
OpenSettingsCommand = new DelegateCommand(ExecuteOpenSettings);
|
||||
OpenDebugPanelCommand = new DelegateCommand(ExecuteOpenDebugPanel);
|
||||
BrowseDataRootPathCommand = new DelegateCommand(ExecuteBrowseDataRootPath);
|
||||
ResetDataRootPathCommand = new DelegateCommand(ExecuteResetDataRootPath);
|
||||
SaveDataRootPathCommand = new DelegateCommand(ExecuteSaveDataRootPath);
|
||||
@@ -569,6 +574,27 @@ namespace XplorePlane.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteOpenDebugPanel()
|
||||
{
|
||||
try
|
||||
{
|
||||
ShowOrActivate(_debugPanelWindow, w => _debugPanelWindow = w,
|
||||
() =>
|
||||
{
|
||||
var viewModel = _containerProvider.Resolve<DebugPanelViewModel>();
|
||||
var window = _containerProvider.Resolve<DebugPanelWindow>();
|
||||
window.DataContext = viewModel;
|
||||
return window;
|
||||
}, "Debug Panel");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Error(ex, "Failed to open debug panel");
|
||||
MessageBox.Show($"Failed to open debug panel: {ex.Message}",
|
||||
"Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
|
||||
private void ExecuteBrowseDataRootPath()
|
||||
{
|
||||
try
|
||||
|
||||
@@ -541,6 +541,20 @@
|
||||
Command="{Binding OpenRealTimeLogViewerCommand}"
|
||||
Text="查看日志" />
|
||||
</telerik:RadRibbonGroup>
|
||||
|
||||
<telerik:RadRibbonGroup Header="调试">
|
||||
<telerik:RadRibbonGroup.Variants>
|
||||
<telerik:GroupVariant Priority="0" Variant="Large" />
|
||||
</telerik:RadRibbonGroup.Variants>
|
||||
|
||||
<telerik:RadRibbonButton
|
||||
telerik:ScreenTip.Description="打开 AppState 可视化调试面板"
|
||||
telerik:ScreenTip.Title="调试面板"
|
||||
Size="Large"
|
||||
SmallImage="/Assets/Icons/tools.png"
|
||||
Command="{Binding OpenDebugPanelCommand}"
|
||||
Text="调试面板" />
|
||||
</telerik:RadRibbonGroup>
|
||||
</telerik:RadRibbonTab>
|
||||
<telerik:RadRibbonTab Header="关于">
|
||||
<telerik:RadRibbonGroup Header="关于">
|
||||
@@ -648,4 +662,4 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Window>
|
||||
</Window>
|
||||
|
||||
Reference in New Issue
Block a user