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; /// 主窗口 VM:持有项目集合、当前页面、应用状态。 public partial class MainViewModel : ObservableObject { private readonly IServiceProvider _sp; private readonly ProjectStore _store; private readonly ILogger _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 logger, ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, ISafetyDio safetyDio, SafetyMonitorService safetyMonitor, UiLogSink logSink, IOptions 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(); TreeRoots = new ObservableCollection(); } /// 由 MainWindow.Loaded 调用,启动背景加载(避免在 DI 构造器中触发重入)。 public void Initialize() { if (_initialized) return; _initialized = true; _ = LoadProjectsAsync(); _ = ConnectConfiguredDevicesAsync(); _safetyMonitor.Start(); RefreshSafetyStatus(); } private bool _initialized; public ObservableCollection AllProjects { get; } public ObservableCollection 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(); } [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(); vm.SetProject(p, StartTesting); CurrentView = vm; } public void OpenTestHost(Project p) { CurrentProject = p; var vm = _sp.GetRequiredService(); vm.SetProject(p); CurrentView = vm; } public void OpenReport(Project p) { CurrentProject = p; var vm = _sp.GetRequiredService(); vm.SetProject(p); CurrentView = vm; } [RelayCommand] private void OpenSettings() { if (CurrentView is SettingsViewModel) { CloseSettings(); return; } _viewBeforeSettings = CurrentView; var vm = _sp.GetRequiredService(); vm.SetCloseAction(CloseSettings); vm.SetProjectRefreshAction(RefreshProjectsFromSettingsAsync, CanRefreshProjectsFromSettings); CurrentView = vm; DevicePanelOpen = false; } 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(); await LoadProjectsAsync(); } [RelayCommand] private void OpenSettingsFromDevicePanel() { DevicePanelOpen = false; OpenSettings(); } private void CloseSettings() { CurrentView = _viewBeforeSettings ?? _sp.GetRequiredService(); _viewBeforeSettings = null; } /// 从项目信息页点击"开始测试"调用。 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(); 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); } /// 由 View 双击树节点时调用。 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; } } } /// 侧栏 TreeView 用的节点。 public sealed class ProjectNode { public Project Project { get; } public NodeKind Kind { get; } public string Title { get; } public ObservableCollection 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 }