410 lines
13 KiB
C#
410 lines
13 KiB
C#
|
|
using System.Collections.ObjectModel;
|
|||
|
|
using System.IO;
|
|||
|
|
using System.Windows;
|
|||
|
|
using System.Windows.Threading;
|
|||
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|||
|
|
using CommunityToolkit.Mvvm.Input;
|
|||
|
|
using Microsoft.Extensions.DependencyInjection;
|
|||
|
|
using Microsoft.Extensions.Logging;
|
|||
|
|
using SSPCTester.Devices.Interfaces;
|
|||
|
|
using SSPCTester.Devices.Config;
|
|||
|
|
using Microsoft.Extensions.Options;
|
|||
|
|
using SSPCTester.Logic.Models;
|
|||
|
|
using SSPCTester.Logic.Storage;
|
|||
|
|
using SSPCTester.UI.Services;
|
|||
|
|
|
|||
|
|
namespace SSPCTester.UI.ViewModels;
|
|||
|
|
|
|||
|
|
/// <summary>主窗口 VM:持有项目集合、当前页面、应用状态。</summary>
|
|||
|
|
public partial class MainViewModel : ObservableObject
|
|||
|
|
{
|
|||
|
|
private readonly IServiceProvider _sp;
|
|||
|
|
private readonly ProjectStore _store;
|
|||
|
|
private readonly ILogger<MainViewModel> _logger;
|
|||
|
|
private readonly ISspc _sspc;
|
|||
|
|
private readonly IPowerSupply _power;
|
|||
|
|
private readonly ILoad _load;
|
|||
|
|
private readonly IDaq _daq;
|
|||
|
|
private readonly UiLogSink _logSink;
|
|||
|
|
private readonly DeviceOptions _deviceOptions;
|
|||
|
|
private readonly DispatcherTimer _deviceMonitorTimer;
|
|||
|
|
private object? _viewBeforeSettings;
|
|||
|
|
|
|||
|
|
public MainViewModel(
|
|||
|
|
IServiceProvider sp, ProjectStore store, ILogger<MainViewModel> logger,
|
|||
|
|
ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, UiLogSink logSink,
|
|||
|
|
IOptions<DeviceOptions> deviceOptions)
|
|||
|
|
{
|
|||
|
|
_sp = sp; _store = store; _logger = logger;
|
|||
|
|
_sspc = sspc; _power = power; _load = load; _daq = daq;
|
|||
|
|
_logSink = logSink;
|
|||
|
|
_deviceOptions = deviceOptions.Value;
|
|||
|
|
_sspc.ConnectionChanged += (_, _) => RefreshDeviceStatus();
|
|||
|
|
_power.ConnectionChanged += (_, _) => RefreshDeviceStatus();
|
|||
|
|
_load.ConnectionChanged += (_, _) => RefreshDeviceStatus();
|
|||
|
|
_daq.ConnectionChanged += (_, _) => RefreshDeviceStatus();
|
|||
|
|
_deviceMonitorTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
|||
|
|
_deviceMonitorTimer.Tick += (_, _) => RefreshDeviceStatus();
|
|||
|
|
_deviceMonitorTimer.Start();
|
|||
|
|
|
|||
|
|
AllProjects = new ObservableCollection<Project>();
|
|||
|
|
TreeRoots = new ObservableCollection<ProjectNode>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>由 MainWindow.Loaded 调用,启动背景加载(避免在 DI 构造器中触发重入)。</summary>
|
|||
|
|
public void Initialize()
|
|||
|
|
{
|
|||
|
|
if (_initialized) return;
|
|||
|
|
_initialized = true;
|
|||
|
|
_ = LoadProjectsAsync();
|
|||
|
|
_ = ConnectConfiguredDevicesAsync();
|
|||
|
|
}
|
|||
|
|
private bool _initialized;
|
|||
|
|
|
|||
|
|
public ObservableCollection<Project> AllProjects { get; }
|
|||
|
|
public ObservableCollection<ProjectNode> TreeRoots { get; }
|
|||
|
|
|
|||
|
|
[ObservableProperty] private AppState _state = AppState.Browsing;
|
|||
|
|
[ObservableProperty] private Project? _currentProject;
|
|||
|
|
[ObservableProperty] private object? _currentView;
|
|||
|
|
[ObservableProperty] private bool _devicesConnected;
|
|||
|
|
[ObservableProperty] private bool _devicePanelOpen;
|
|||
|
|
[ObservableProperty] private bool _deviceOperationBusy;
|
|||
|
|
[ObservableProperty] private string _deviceSummary = "设备状态检查中";
|
|||
|
|
[ObservableProperty] private string _sspcConnectionStatus = "检查中";
|
|||
|
|
[ObservableProperty] private string _powerConnectionStatus = "检查中";
|
|||
|
|
[ObservableProperty] private string _loadConnectionStatus = "检查中";
|
|||
|
|
[ObservableProperty] private string _daqConnectionStatus = "检查中";
|
|||
|
|
[ObservableProperty] private string _testingMessage = "";
|
|||
|
|
[ObservableProperty] private TestSessionViewModel? _currentSession;
|
|||
|
|
|
|||
|
|
partial void OnStateChanged(AppState value) => RebuildTree();
|
|||
|
|
partial void OnCurrentProjectChanged(Project? value) => RebuildTree();
|
|||
|
|
|
|||
|
|
private async Task ConnectConfiguredDevicesAsync()
|
|||
|
|
{
|
|||
|
|
var jobs = new[]
|
|||
|
|
{
|
|||
|
|
ConnectOneAsync("SSPC", _sspc, _deviceOptions.Sspc.AutoConnect),
|
|||
|
|
ConnectOneAsync("电源", _power, _deviceOptions.PowerSupply.AutoConnect),
|
|||
|
|
ConnectOneAsync("负载", _load, _deviceOptions.Load.AutoConnect),
|
|||
|
|
ConnectOneAsync("PCIe8586", _daq, _deviceOptions.Daq.AutoConnect)
|
|||
|
|
};
|
|||
|
|
await Task.WhenAll(jobs);
|
|||
|
|
DevicesConnected =
|
|||
|
|
(!_deviceOptions.Sspc.AutoConnect || _sspc.IsConnected) &&
|
|||
|
|
(!_deviceOptions.PowerSupply.AutoConnect || _power.IsConnected) &&
|
|||
|
|
(!_deviceOptions.Load.AutoConnect || _load.IsConnected) &&
|
|||
|
|
(!_deviceOptions.Daq.AutoConnect || _daq.IsConnected);
|
|||
|
|
RefreshDeviceStatus();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private async Task ConnectOneAsync(string name, IDeviceConnection device, bool autoConnect)
|
|||
|
|
{
|
|||
|
|
if (!autoConnect) return;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
bool connected = await device.ConnectAsync();
|
|||
|
|
if (connected && device.IsConnected)
|
|||
|
|
{
|
|||
|
|
_logSink.Add(UiLogLevel.Success, $"{name} 已自动连接。");
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
_logSink.Add(UiLogLevel.Warning, $"{name} 自动连接未成功,设备保持未连接。");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "{Device} auto connect failed.", name);
|
|||
|
|
_logSink.Add(UiLogLevel.Error, $"{name} 自动连接失败:{ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void RefreshDeviceStatus()
|
|||
|
|
{
|
|||
|
|
Application.Current?.Dispatcher.Invoke(() =>
|
|||
|
|
{
|
|||
|
|
SspcConnectionStatus = DeviceStatusText(_sspc);
|
|||
|
|
PowerConnectionStatus = DeviceStatusText(_power);
|
|||
|
|
LoadConnectionStatus = DeviceStatusText(_load);
|
|||
|
|
DaqConnectionStatus = DeviceStatusText(_daq);
|
|||
|
|
DevicesConnected =
|
|||
|
|
(!_deviceOptions.Sspc.AutoConnect || _sspc.IsConnected) &&
|
|||
|
|
(!_deviceOptions.PowerSupply.AutoConnect || _power.IsConnected) &&
|
|||
|
|
(!_deviceOptions.Load.AutoConnect || _load.IsConnected) &&
|
|||
|
|
(!_deviceOptions.Daq.AutoConnect || _daq.IsConnected);
|
|||
|
|
int connected = new[] { _sspc.IsConnected, _power.IsConnected, _load.IsConnected, _daq.IsConnected }.Count(x => x);
|
|||
|
|
DeviceSummary = connected == 4 ? "设备已连接" : $"设备 {connected}/4 已连接";
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private static string DeviceStatusText(IDeviceConnection device) =>
|
|||
|
|
device.IsConnected ? "已连接" : "未连接";
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private void ToggleDevicePanel()
|
|||
|
|
{
|
|||
|
|
RefreshDeviceStatus();
|
|||
|
|
DevicePanelOpen = !DevicePanelOpen;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private async Task ConnectAllDevices()
|
|||
|
|
{
|
|||
|
|
if (DeviceOperationBusy) return;
|
|||
|
|
DeviceOperationBusy = true;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
await Task.WhenAll(
|
|||
|
|
ConnectOneAsync("SSPC", _sspc, true),
|
|||
|
|
ConnectOneAsync("电源", _power, true),
|
|||
|
|
ConnectOneAsync("负载", _load, true),
|
|||
|
|
ConnectOneAsync("PCIe8586", _daq, true));
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
DeviceOperationBusy = false;
|
|||
|
|
RefreshDeviceStatus();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private async Task DisconnectAllDevices()
|
|||
|
|
{
|
|||
|
|
if (DeviceOperationBusy) return;
|
|||
|
|
DeviceOperationBusy = true;
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
await Task.WhenAll(
|
|||
|
|
SafeDisconnectAsync("SSPC", _sspc),
|
|||
|
|
SafeDisconnectAsync("电源", _power),
|
|||
|
|
SafeDisconnectAsync("负载", _load),
|
|||
|
|
SafeDisconnectAsync("PCIe8586", _daq));
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
DeviceOperationBusy = false;
|
|||
|
|
RefreshDeviceStatus();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private void RefreshDevices() => RefreshDeviceStatus();
|
|||
|
|
|
|||
|
|
private async Task SafeDisconnectAsync(string name, IDeviceConnection device)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
await device.DisconnectAsync();
|
|||
|
|
_logSink.Add(UiLogLevel.Info, $"{name} 已断开。");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogWarning(ex, "{Device} disconnect failed.", name);
|
|||
|
|
_logSink.Add(UiLogLevel.Error, $"{name} 断开失败:{ex.Message}");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task LoadProjectsAsync()
|
|||
|
|
{
|
|||
|
|
AllProjects.Clear();
|
|||
|
|
var list = await _store.LoadAllAsync();
|
|||
|
|
foreach (var p in list) AllProjects.Add(p);
|
|||
|
|
RebuildTree();
|
|||
|
|
if (CurrentView == null) NavigateHome();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void RebuildTree()
|
|||
|
|
{
|
|||
|
|
TreeRoots.Clear();
|
|||
|
|
var src = State == AppState.Testing && CurrentProject != null
|
|||
|
|
? new[] { CurrentProject }
|
|||
|
|
: AllProjects.ToArray();
|
|||
|
|
foreach (var p in src)
|
|||
|
|
{
|
|||
|
|
var node = new ProjectNode(p);
|
|||
|
|
node.Children.Add(new ProjectNode(p, NodeKind.Info));
|
|||
|
|
node.Children.Add(new ProjectNode(p, NodeKind.Test));
|
|||
|
|
node.Children.Add(new ProjectNode(p, NodeKind.Report));
|
|||
|
|
TreeRoots.Add(node);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private void NavigateHome()
|
|||
|
|
{
|
|||
|
|
CurrentView = _sp.GetRequiredService<HomeViewModel>();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private async Task NewProject()
|
|||
|
|
{
|
|||
|
|
var dlg = new Views.NewProjectDialog { Owner = Application.Current?.MainWindow };
|
|||
|
|
if (dlg.ShowDialog() != true) return;
|
|||
|
|
|
|||
|
|
var p = new Project
|
|||
|
|
{
|
|||
|
|
Name = dlg.ProjectName,
|
|||
|
|
Dut = new DutInfo
|
|||
|
|
{
|
|||
|
|
ProductName = dlg.DeviceName,
|
|||
|
|
SerialNumber = dlg.SerialNumber,
|
|||
|
|
Model = dlg.Model,
|
|||
|
|
},
|
|||
|
|
Site = new TestSiteInfo
|
|||
|
|
{
|
|||
|
|
Tester = dlg.Tester,
|
|||
|
|
TestDate = DateTime.Today,
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
await _store.SaveAsync(p);
|
|||
|
|
AllProjects.Insert(0, p);
|
|||
|
|
CurrentProject = p;
|
|||
|
|
OpenProjectInfo(p);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void OpenProjectInfo(Project p)
|
|||
|
|
{
|
|||
|
|
CurrentProject = p;
|
|||
|
|
var vm = _sp.GetRequiredService<ProjectInfoViewModel>();
|
|||
|
|
vm.SetProject(p, StartTesting);
|
|||
|
|
CurrentView = vm;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void OpenTestHost(Project p)
|
|||
|
|
{
|
|||
|
|
CurrentProject = p;
|
|||
|
|
var vm = _sp.GetRequiredService<TestHostViewModel>();
|
|||
|
|
vm.SetProject(p);
|
|||
|
|
CurrentView = vm;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public void OpenReport(Project p)
|
|||
|
|
{
|
|||
|
|
CurrentProject = p;
|
|||
|
|
var vm = _sp.GetRequiredService<ReportViewModel>();
|
|||
|
|
vm.SetProject(p);
|
|||
|
|
CurrentView = vm;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private void OpenSettings()
|
|||
|
|
{
|
|||
|
|
if (CurrentView is SettingsViewModel)
|
|||
|
|
{
|
|||
|
|
CloseSettings();
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
_viewBeforeSettings = CurrentView;
|
|||
|
|
var vm = _sp.GetRequiredService<SettingsViewModel>();
|
|||
|
|
vm.SetCloseAction(CloseSettings);
|
|||
|
|
CurrentView = vm;
|
|||
|
|
DevicePanelOpen = false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private void OpenSettingsFromDevicePanel()
|
|||
|
|
{
|
|||
|
|
DevicePanelOpen = false;
|
|||
|
|
OpenSettings();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
private void CloseSettings()
|
|||
|
|
{
|
|||
|
|
CurrentView = _viewBeforeSettings ?? _sp.GetRequiredService<HomeViewModel>();
|
|||
|
|
_viewBeforeSettings = null;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>从项目信息页点击"开始测试"调用。</summary>
|
|||
|
|
public async void StartTesting(Project p)
|
|||
|
|
{
|
|||
|
|
CurrentProject = p;
|
|||
|
|
State = AppState.Testing;
|
|||
|
|
TestingMessage = $"测试进行中:{p.Name}";
|
|||
|
|
|
|||
|
|
var session = new TestSessionViewModel(_sp, p, _sspc, _power, _load, _daq, _logSink);
|
|||
|
|
CurrentSession = session;
|
|||
|
|
|
|||
|
|
var host = _sp.GetRequiredService<TestHostViewModel>();
|
|||
|
|
host.SetProject(p);
|
|||
|
|
host.AttachSession(session);
|
|||
|
|
CurrentView = host;
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
await session.RunAllAsync();
|
|||
|
|
_logSink.Add(UiLogLevel.Success, "全部测试完成。");
|
|||
|
|
}
|
|||
|
|
catch (OperationCanceledException)
|
|||
|
|
{
|
|||
|
|
_logSink.Add(UiLogLevel.Warning, "测试已中止。");
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "Test failed.");
|
|||
|
|
_logSink.Add(UiLogLevel.Error, "测试出错:" + ex.Message);
|
|||
|
|
}
|
|||
|
|
finally
|
|||
|
|
{
|
|||
|
|
await _store.SaveAsync(p);
|
|||
|
|
State = AppState.Browsing;
|
|||
|
|
CurrentSession = null;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private void AbortTest()
|
|||
|
|
{
|
|||
|
|
CurrentSession?.Cancel();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
[RelayCommand]
|
|||
|
|
private void OpenProjectsFolder()
|
|||
|
|
{
|
|||
|
|
if (Directory.Exists(_store.ProjectsRoot))
|
|||
|
|
System.Diagnostics.Process.Start("explorer.exe", _store.ProjectsRoot);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>由 View 双击树节点时调用。</summary>
|
|||
|
|
public void HandleNodeActivated(ProjectNode? node)
|
|||
|
|
{
|
|||
|
|
if (node == null) return;
|
|||
|
|
switch (node.Kind)
|
|||
|
|
{
|
|||
|
|
case NodeKind.Root:
|
|||
|
|
case NodeKind.Info:
|
|||
|
|
OpenProjectInfo(node.Project); break;
|
|||
|
|
case NodeKind.Test:
|
|||
|
|
OpenTestHost(node.Project); break;
|
|||
|
|
case NodeKind.Report:
|
|||
|
|
OpenReport(node.Project); break;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>侧栏 TreeView 用的节点。</summary>
|
|||
|
|
public sealed class ProjectNode
|
|||
|
|
{
|
|||
|
|
public Project Project { get; }
|
|||
|
|
public NodeKind Kind { get; }
|
|||
|
|
public string Title { get; }
|
|||
|
|
public ObservableCollection<ProjectNode> Children { get; } = new();
|
|||
|
|
|
|||
|
|
public ProjectNode(Project p, NodeKind kind = NodeKind.Root)
|
|||
|
|
{
|
|||
|
|
Project = p;
|
|||
|
|
Kind = kind;
|
|||
|
|
Title = kind switch
|
|||
|
|
{
|
|||
|
|
NodeKind.Root => p.Name,
|
|||
|
|
NodeKind.Info => "项目信息",
|
|||
|
|
NodeKind.Test => "测试",
|
|||
|
|
NodeKind.Report => "报告",
|
|||
|
|
_ => p.Name
|
|||
|
|
};
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public enum NodeKind { Root, Info, Test, Report }
|