SSPC-Tester/SSPCTester.UI/ViewModels/MainViewModel.cs
2026-07-03 14:16:15 +08:00

488 lines
17 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 ISafetyDio _safetyDio;
private readonly SafetyMonitorService _safetyMonitor;
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,
ISafetyDio safetyDio, SafetyMonitorService safetyMonitor, UiLogSink logSink,
IOptions<DeviceOptions> deviceOptions)
{
_sp = sp; _store = store; _logger = logger;
_sspc = sspc; _power = power; _load = load; _daq = daq;
_safetyDio = safetyDio; _safetyMonitor = safetyMonitor;
_logSink = logSink;
_deviceOptions = deviceOptions.Value;
_sspc.ConnectionChanged += (_, _) => RefreshDeviceStatus();
_power.ConnectionChanged += (_, _) => RefreshDeviceStatus();
_load.ConnectionChanged += (_, _) => RefreshDeviceStatus();
_daq.ConnectionChanged += (_, _) => RefreshDeviceStatus();
_safetyDio.ConnectionChanged += (_, _) => RefreshDeviceStatus();
_safetyMonitor.StatusChanged += (_, _) => RefreshSafetyStatus();
_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();
_safetyMonitor.Start();
RefreshSafetyStatus();
}
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 bool _emergencyOperationBusy;
[ObservableProperty] private string _deviceSummary = "设备状态检查中";
[ObservableProperty] private string _sspcConnectionStatus = "检查中";
[ObservableProperty] private string _powerConnectionStatus = "检查中";
[ObservableProperty] private string _loadConnectionStatus = "检查中";
[ObservableProperty] private string _daqConnectionStatus = "检查中";
[ObservableProperty] private string _pci2312ConnectionStatus = "检查中";
[ObservableProperty] private string _safetyStatusText = "设备故障";
[ObservableProperty] private string _safetyDetailText = "设备安全监控未启动";
[ObservableProperty] private bool _alarmLampActive;
[ObservableProperty] private string _emergencyButtonText = "报警急停(状态重置)";
[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),
ConnectOneAsync("PCI2312", _safetyDio, _deviceOptions.Pci2312.Enabled && _deviceOptions.Pci2312.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) &&
(!_deviceOptions.Pci2312.Enabled || !_deviceOptions.Pci2312.AutoConnect || _safetyDio.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);
Pci2312ConnectionStatus = DeviceStatusText(_safetyDio);
DevicesConnected =
(!_deviceOptions.Sspc.AutoConnect || _sspc.IsConnected) &&
(!_deviceOptions.PowerSupply.AutoConnect || _power.IsConnected) &&
(!_deviceOptions.Load.AutoConnect || _load.IsConnected) &&
(!_deviceOptions.Daq.AutoConnect || _daq.IsConnected) &&
(!_deviceOptions.Pci2312.Enabled || !_deviceOptions.Pci2312.AutoConnect || _safetyDio.IsConnected);
int connected = new[] { _sspc.IsConnected, _power.IsConnected, _load.IsConnected, _daq.IsConnected, _safetyDio.IsConnected }.Count(x => x);
DeviceSummary = connected == 5 ? "设备已连接" : $"设备 {connected}/5 已连接";
});
}
private void RefreshSafetyStatus()
{
Application.Current?.Dispatcher.Invoke(() =>
{
SafetyStatusText = _safetyMonitor.StatusText;
SafetyDetailText = _safetyMonitor.DetailText;
AlarmLampActive = _safetyMonitor.IsAlarmOutputOn;
EmergencyButtonText = AlarmLampActive ? "状态重置" : "报警急停";
});
}
private static string DeviceStatusText(IDeviceConnection device) =>
device.IsConnected ? "已连接" : "未连接";
[RelayCommand]
private void ToggleDevicePanel()
{
RefreshDeviceStatus();
DevicePanelOpen = !DevicePanelOpen;
}
[RelayCommand]
private async Task ToggleEmergencyAlarm()
{
if (EmergencyOperationBusy) return;
EmergencyOperationBusy = true;
try
{
if (AlarmLampActive)
{
var result = await _safetyMonitor.ResetAsync().ConfigureAwait(false);
RefreshSafetyStatus();
if (!result.Success)
{
Application.Current?.Dispatcher.Invoke(() =>
MessageBox.Show(
Application.Current?.MainWindow,
result.Message,
"状态重置受阻",
MessageBoxButton.OK,
MessageBoxImage.Warning));
}
}
else
{
await _safetyMonitor.TriggerSoftwareEmergencyAsync().ConfigureAwait(false);
RefreshSafetyStatus();
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Toggle emergency alarm failed.");
_logSink.Add(UiLogLevel.Error, $"报警急停操作失败:{ex.Message}");
Application.Current?.Dispatcher.Invoke(() =>
MessageBox.Show(
Application.Current?.MainWindow,
$"报警急停操作失败:{ex.Message}",
"报警急停",
MessageBoxButton.OK,
MessageBoxImage.Error));
RefreshSafetyStatus();
}
finally
{
EmergencyOperationBusy = false;
}
}
[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),
ConnectOneAsync("PCI2312", _safetyDio, _deviceOptions.Pci2312.Enabled));
}
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),
SafeDisconnectAsync("PCI2312", _safetyDio));
}
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.ProductName,
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 _store.SaveAsync(p);
_logSink.Add(UiLogLevel.Info, "已进入测试页面,请在各测试页手动执行当前项目。");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to enter test view.");
_logSink.Add(UiLogLevel.Error, "进入测试页面出错:" + ex.Message);
}
}
[RelayCommand]
private async Task AbortTest()
{
CurrentSession?.Cancel();
if (CurrentView is TestHostViewModel host && host.Basic.IsBatchRunning)
await host.Basic.CancelRunAllCommand.ExecuteAsync(null);
if (CurrentProject != null)
await _store.SaveAsync(CurrentProject);
CurrentSession = null;
TestingMessage = string.Empty;
State = AppState.Browsing;
if (CurrentProject != null)
OpenProjectInfo(CurrentProject);
}
[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 }