294 lines
11 KiB
C#
294 lines
11 KiB
C#
using Microsoft.Win32;
|
|
using Prism.Commands;
|
|
using Prism.Mvvm;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Collections.ObjectModel;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Reflection;
|
|
using System.Text.Json;
|
|
using System.Windows;
|
|
using System.Windows.Threading;
|
|
using XP.Common.Logging.Interfaces;
|
|
using XplorePlane.Models;
|
|
using XplorePlane.Services.AppState;
|
|
using XplorePlane.Views.Debug;
|
|
|
|
namespace XplorePlane.ViewModels.Debug
|
|
{
|
|
public class SnapshotViewModel : BindableBase
|
|
{
|
|
private bool _isSelected;
|
|
|
|
public StateSnapshot Snapshot { get; set; }
|
|
|
|
public bool IsSelected
|
|
{
|
|
get => _isSelected;
|
|
set => SetProperty(ref _isSelected, value);
|
|
}
|
|
}
|
|
|
|
public class SnapshotManagerViewModel : BindableBase, IDisposable
|
|
{
|
|
private const int MaxSnapshots = 50;
|
|
|
|
private readonly IAppStateService _appStateService;
|
|
private readonly ILoggerService _logger;
|
|
private readonly Dispatcher _dispatcher;
|
|
private readonly JsonSerializerOptions _jsonOptions = new() { WriteIndented = true };
|
|
private SnapshotViewModel _selectedSnapshot;
|
|
|
|
public ObservableCollection<SnapshotViewModel> Snapshots { get; } = new();
|
|
public ObservableCollection<StateNodeViewModel> SnapshotDetails { get; } = new();
|
|
|
|
public SnapshotViewModel SelectedSnapshot
|
|
{
|
|
get => _selectedSnapshot;
|
|
set
|
|
{
|
|
if (SetProperty(ref _selectedSnapshot, value))
|
|
{
|
|
UpdateSnapshotDetails();
|
|
}
|
|
}
|
|
}
|
|
|
|
public DelegateCommand CaptureSnapshotCommand { get; }
|
|
public DelegateCommand CompareSnapshotsCommand { get; }
|
|
public DelegateCommand ExportSnapshotCommand { get; }
|
|
public DelegateCommand DeleteSnapshotCommand { get; }
|
|
|
|
public SnapshotManagerViewModel(IAppStateService appStateService, ILoggerService loggerService, Dispatcher dispatcher)
|
|
{
|
|
_appStateService = appStateService ?? throw new ArgumentNullException(nameof(appStateService));
|
|
_logger = (loggerService ?? throw new ArgumentNullException(nameof(loggerService))).ForModule<SnapshotManagerViewModel>();
|
|
_dispatcher = dispatcher ?? throw new ArgumentNullException(nameof(dispatcher));
|
|
|
|
CaptureSnapshotCommand = new DelegateCommand(CaptureSnapshot);
|
|
CompareSnapshotsCommand = new DelegateCommand(CompareSnapshots);
|
|
ExportSnapshotCommand = new DelegateCommand(ExportSnapshot);
|
|
DeleteSnapshotCommand = new DelegateCommand(DeleteSnapshot);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
Snapshots.Clear();
|
|
SnapshotDetails.Clear();
|
|
SelectedSnapshot = null;
|
|
}
|
|
|
|
public void CaptureSnapshot()
|
|
{
|
|
try
|
|
{
|
|
var snapshot = new StateSnapshot
|
|
{
|
|
Id = Guid.NewGuid(),
|
|
Timestamp = DateTime.Now,
|
|
MotionState = _appStateService.MotionState,
|
|
RaySourceState = _appStateService.RaySourceState,
|
|
DetectorState = _appStateService.DetectorState,
|
|
SystemState = _appStateService.SystemState,
|
|
CameraState = _appStateService.CameraState,
|
|
LinkedViewState = _appStateService.LinkedViewState,
|
|
RecipeExecutionState = _appStateService.RecipeExecutionState,
|
|
CalibrationMatrix = _appStateService.CalibrationMatrix
|
|
};
|
|
|
|
var item = new SnapshotViewModel { Snapshot = snapshot };
|
|
Snapshots.Insert(0, item);
|
|
SelectedSnapshot = item;
|
|
|
|
while (Snapshots.Count > MaxSnapshots)
|
|
{
|
|
Snapshots.RemoveAt(Snapshots.Count - 1);
|
|
}
|
|
|
|
_logger.Info("快照已捕获 {Id} | Snapshot captured {Id}", snapshot.Id);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "捕获调试快照失败 | Failed to capture debug snapshot");
|
|
}
|
|
}
|
|
|
|
public IReadOnlyList<StateDifference> ComputeDifference(StateSnapshot first, StateSnapshot second)
|
|
{
|
|
var differences = new List<StateDifference>();
|
|
if (first == null || second == null)
|
|
{
|
|
return differences;
|
|
}
|
|
|
|
CompareObject("MotionState", first.MotionState, second.MotionState, differences);
|
|
CompareObject("RaySourceState", first.RaySourceState, second.RaySourceState, differences);
|
|
CompareObject("DetectorState", first.DetectorState, second.DetectorState, differences);
|
|
CompareObject("SystemState", first.SystemState, second.SystemState, differences);
|
|
CompareObject("CameraState", first.CameraState, second.CameraState, differences);
|
|
CompareObject("LinkedViewState", first.LinkedViewState, second.LinkedViewState, differences);
|
|
CompareObject("RecipeExecutionState", first.RecipeExecutionState, second.RecipeExecutionState, differences);
|
|
CompareObject("CalibrationMatrix", first.CalibrationMatrix, second.CalibrationMatrix, differences);
|
|
|
|
return differences;
|
|
}
|
|
|
|
private void CompareSnapshots()
|
|
{
|
|
try
|
|
{
|
|
var selected = Snapshots.Where(s => s.IsSelected).Take(2).ToList();
|
|
if (selected.Count != 2)
|
|
{
|
|
MessageBox.Show("Please select exactly two snapshots to compare.", "Compare Snapshots", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
var differences = ComputeDifference(selected[0].Snapshot, selected[1].Snapshot);
|
|
var viewModel = new SnapshotDiffViewModel(differences, _logger, _dispatcher);
|
|
var window = new SnapshotDiffWindow
|
|
{
|
|
DataContext = viewModel,
|
|
Owner = Application.Current?.MainWindow
|
|
};
|
|
|
|
window.Show();
|
|
_logger.Info("快照对比已打开 | Snapshot comparison window opened");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "比较快照失败 | Failed to compare snapshots");
|
|
MessageBox.Show($"Failed to compare snapshots: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private void ExportSnapshot()
|
|
{
|
|
try
|
|
{
|
|
if (SelectedSnapshot?.Snapshot == null)
|
|
{
|
|
MessageBox.Show("Please select a snapshot first.", "Export Snapshot", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
return;
|
|
}
|
|
|
|
var dialog = new SaveFileDialog
|
|
{
|
|
Title = "Export Snapshot",
|
|
Filter = "JSON files (*.json)|*.json",
|
|
FileName = $"snapshot-{SelectedSnapshot.Snapshot.Timestamp:yyyyMMdd-HHmmss}.json"
|
|
};
|
|
|
|
if (dialog.ShowDialog() != true)
|
|
{
|
|
return;
|
|
}
|
|
|
|
File.WriteAllText(dialog.FileName, JsonSerializer.Serialize(SelectedSnapshot.Snapshot, _jsonOptions));
|
|
_logger.Info("快照已导出到 {Path} | Snapshot exported to {Path}", dialog.FileName);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "导出快照失败 | Failed to export snapshot");
|
|
MessageBox.Show($"Failed to export snapshot: {ex.Message}", "Error", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
private void DeleteSnapshot()
|
|
{
|
|
try
|
|
{
|
|
if (SelectedSnapshot == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var result = MessageBox.Show("Delete selected snapshot?", "Delete Snapshot", MessageBoxButton.YesNo, MessageBoxImage.Question);
|
|
if (result != MessageBoxResult.Yes)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var snapshot = SelectedSnapshot;
|
|
var index = Snapshots.IndexOf(snapshot);
|
|
Snapshots.Remove(snapshot);
|
|
SelectedSnapshot = index >= 0 && index < Snapshots.Count ? Snapshots[index] : Snapshots.FirstOrDefault();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.Error(ex, "删除快照失败 | Failed to delete snapshot");
|
|
}
|
|
}
|
|
|
|
private void UpdateSnapshotDetails()
|
|
{
|
|
SnapshotDetails.Clear();
|
|
if (SelectedSnapshot?.Snapshot == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AddSnapshotRoot("MotionState", SelectedSnapshot.Snapshot.MotionState);
|
|
AddSnapshotRoot("RaySourceState", SelectedSnapshot.Snapshot.RaySourceState);
|
|
AddSnapshotRoot("DetectorState", SelectedSnapshot.Snapshot.DetectorState);
|
|
AddSnapshotRoot("SystemState", SelectedSnapshot.Snapshot.SystemState);
|
|
AddSnapshotRoot("CameraState", SelectedSnapshot.Snapshot.CameraState);
|
|
AddSnapshotRoot("LinkedViewState", SelectedSnapshot.Snapshot.LinkedViewState);
|
|
AddSnapshotRoot("RecipeExecutionState", SelectedSnapshot.Snapshot.RecipeExecutionState);
|
|
AddSnapshotRoot("CalibrationMatrix", SelectedSnapshot.Snapshot.CalibrationMatrix);
|
|
}
|
|
|
|
private void AddSnapshotRoot(string category, object value)
|
|
{
|
|
var root = new StateNodeViewModel
|
|
{
|
|
Name = category,
|
|
Category = category
|
|
};
|
|
|
|
if (value != null)
|
|
{
|
|
foreach (var property in value.GetType().GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
|
{
|
|
root.Children.Add(new StateNodeViewModel
|
|
{
|
|
Name = property.Name,
|
|
Category = category,
|
|
Value = DebugPanelStateFormatter.FormatValue(property.GetValue(value))
|
|
});
|
|
}
|
|
}
|
|
|
|
SnapshotDetails.Add(root);
|
|
}
|
|
|
|
private static void CompareObject(string category, object first, object second, ICollection<StateDifference> differences)
|
|
{
|
|
var type = first?.GetType() ?? second?.GetType();
|
|
if (type == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var property in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
|
|
{
|
|
var firstValue = first == null ? null : property.GetValue(first);
|
|
var secondValue = second == null ? null : property.GetValue(second);
|
|
if (Equals(firstValue, secondValue))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
differences.Add(new StateDifference
|
|
{
|
|
Category = category,
|
|
FieldName = property.Name,
|
|
Value1 = DebugPanelStateFormatter.FormatValue(firstValue),
|
|
Value2 = DebugPanelStateFormatter.FormatValue(secondValue)
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|