516 lines
17 KiB
C#
516 lines
17 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 readonly DispatcherTimer _safetyReadingsTimer;
|
||
private bool _safetyReadingsBusy;
|
||
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();
|
||
_safetyReadingsTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
|
||
_safetyReadingsTimer.Tick += async (_, _) => await RefreshSafetyReadingsAsync().ConfigureAwait(true);
|
||
|
||
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 _safetyPanelOpen;
|
||
[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 _pci2312ConnectionStatus = "检查中";
|
||
[ObservableProperty] private string _safetyStatusText = "未启动";
|
||
[ObservableProperty] private string _safetyDetailText = "设备安全监控未启动";
|
||
[ObservableProperty] private string _safetyPowerCurrentText = "--";
|
||
[ObservableProperty] private string _safetyLoadCurrentText = "--";
|
||
[ObservableProperty] private bool _alarmLampActive;
|
||
[ObservableProperty] private string _testingMessage = "";
|
||
[ObservableProperty] private TestSessionViewModel? _currentSession;
|
||
|
||
partial void OnStateChanged(AppState value) => RebuildTree();
|
||
partial void OnCurrentProjectChanged(Project? value) => RebuildTree();
|
||
partial void OnSafetyPanelOpenChanged(bool value)
|
||
{
|
||
if (value)
|
||
{
|
||
_safetyReadingsTimer.Start();
|
||
_ = RefreshSafetyReadingsAsync();
|
||
}
|
||
else
|
||
{
|
||
_safetyReadingsTimer.Stop();
|
||
}
|
||
}
|
||
|
||
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.IsEmergencyActive || _safetyMonitor.IsAlarmOutputOn;
|
||
});
|
||
}
|
||
|
||
private async Task RefreshSafetyReadingsAsync()
|
||
{
|
||
if (_safetyReadingsBusy) return;
|
||
_safetyReadingsBusy = true;
|
||
try
|
||
{
|
||
SafetyPowerCurrentText = await ReadPowerCurrentTextAsync().ConfigureAwait(true);
|
||
SafetyLoadCurrentText = await ReadLoadCurrentTextAsync().ConfigureAwait(true);
|
||
}
|
||
finally
|
||
{
|
||
_safetyReadingsBusy = false;
|
||
}
|
||
}
|
||
|
||
private async Task<string> ReadPowerCurrentTextAsync()
|
||
{
|
||
if (!_power.IsConnected) return "未连接";
|
||
try
|
||
{
|
||
var (_, amps) = await _power.ReadAsync().ConfigureAwait(true);
|
||
return $"{amps:F3} A";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "Read UDP5080 current for safety panel failed.");
|
||
return "读取失败";
|
||
}
|
||
}
|
||
|
||
private async Task<string> ReadLoadCurrentTextAsync()
|
||
{
|
||
if (!_load.IsConnected) return "未连接";
|
||
try
|
||
{
|
||
var (_, amps) = await _load.ReadAsync().ConfigureAwait(true);
|
||
return $"{amps:F3} A";
|
||
}
|
||
catch (Exception ex)
|
||
{
|
||
_logger.LogDebug(ex, "Read IT8702P current for safety panel failed.");
|
||
return "读取失败";
|
||
}
|
||
}
|
||
|
||
private static string DeviceStatusText(IDeviceConnection device) =>
|
||
device.IsConnected ? "已连接" : "未连接";
|
||
|
||
[RelayCommand]
|
||
private void ToggleDevicePanel()
|
||
{
|
||
RefreshDeviceStatus();
|
||
DevicePanelOpen = !DevicePanelOpen;
|
||
}
|
||
|
||
[RelayCommand]
|
||
private void ToggleSafetyPanel()
|
||
{
|
||
RefreshSafetyStatus();
|
||
SafetyPanelOpen = !SafetyPanelOpen;
|
||
}
|
||
|
||
[RelayCommand]
|
||
private async Task ResetSafetyAlarm()
|
||
{
|
||
await _safetyMonitor.ResetAsync().ConfigureAwait(false);
|
||
RefreshSafetyStatus();
|
||
}
|
||
|
||
[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 = "固态功率控制器",
|
||
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 }
|