516 lines
18 KiB
C#
516 lines
18 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 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
|
||
{
|
||
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);
|
||
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<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 }
|