SSPC-Tester/SSPCTester.UI/ViewModels/MainViewModel.cs

516 lines
18 KiB
C#
Raw Permalink Normal View History

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;
// Enter the alarm UI state as soon as an emergency is detected. The
// physical alarm output and the sequential device shutdown can take
// time, so waiting for the DO state makes the UI appear unresponsive.
AlarmLampActive = _safetyMonitor.IsEmergencyActive || _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
{
2026-07-02 17:43:32 +08:00
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);
2026-07-08 00:25:45 +08:00
vm.SetProjectRefreshAction(RefreshProjectsFromSettingsAsync, CanRefreshProjectsFromSettings);
CurrentView = vm;
DevicePanelOpen = false;
}
2026-07-08 00:25:45 +08:00
private bool CanRefreshProjectsFromSettings()
{
if (State != AppState.Testing) return true;
_logSink.Add(UiLogLevel.Warning, "测试中不可切换项目目录,请退出测试后再刷新工程。");
return false;
}
private async Task RefreshProjectsFromSettingsAsync(string projectsRoot)
{
if (!CanRefreshProjectsFromSettings()) return;
if (string.IsNullOrWhiteSpace(projectsRoot))
{
_logSink.Add(UiLogLevel.Error, "项目数据路径为空,无法刷新工程。");
return;
}
Directory.CreateDirectory(projectsRoot);
_store.ProjectsRoot = projectsRoot;
CurrentProject = null;
_viewBeforeSettings = _sp.GetRequiredService<HomeViewModel>();
await LoadProjectsAsync();
}
[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 }