SSPC-Tester/SSPCTester.UI/ViewModels/SettingsViewModel.cs
2026-06-30 12:10:29 +08:00

460 lines
16 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.IO;
using System.Collections.ObjectModel;
using System.IO.Ports;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Options;
using Microsoft.Win32;
using SSPCTester.Devices.Config;
using SSPCTester.Devices.Interfaces;
using SSPCTester.UI.Services;
namespace SSPCTester.UI.ViewModels;
/// <summary>系统设置页。</summary>
public partial class SettingsViewModel : ObservableObject
{
private readonly UiLogSink _log;
private readonly ISspc _sspc;
private readonly IPowerSupply _power;
private readonly ILoad _load;
private readonly IDaq _daq;
private readonly DeviceOptions _devOpts;
private readonly StorageOptions _storeOpts;
private readonly ReportOptions _reportOpts;
private readonly UserSettingsStore _settingsStore;
private Action? _closeAction;
public SettingsViewModel(
IOptions<DeviceOptions> devOpts,
IOptions<StorageOptions> storeOpts,
IOptions<ReportOptions> reportOpts,
ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, UiLogSink log,
UserSettingsStore settingsStore)
{
_devOpts = devOpts.Value;
_storeOpts = storeOpts.Value;
_reportOpts = reportOpts.Value;
_sspc = sspc; _power = power; _load = load; _daq = daq; _log = log;
_settingsStore = settingsStore;
NormalizeDaqChannels();
LoadDaqChannelEditors();
RefreshPorts();
_sspc.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(SspcStatus));
_power.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(PowerStatus));
_load.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(LoadStatus));
_daq.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(DaqStatus));
_ = RefreshDaqDevices();
}
public DeviceOptions Devices => _devOpts;
public StorageOptions Storage => _storeOpts;
public ReportOptions Report => _reportOpts;
public ObservableCollection<string> PortNames { get; } = new();
public ObservableCollection<DaqDeviceDescriptor> DaqDevices { get; } = new();
public ObservableCollection<DaqChannelEditor> DaqChannelEditors { get; } = new();
public IReadOnlyList<int> DaqChannelCounts { get; } = new[] { 1, 2, 4, 8 };
public IReadOnlyList<string> DaqInputRanges { get; } = new[] { "PlusMinus5V", "PlusMinus1V" };
public IReadOnlyList<string> DaqRoles { get; } = Enum.GetNames<DaqChannelRole>();
public IReadOnlyList<LoadQueryMode> LoadQueryModes { get; } = Enum.GetValues<LoadQueryMode>();
public IReadOnlyList<TerminatorOption> TerminatorOptions { get; } = new[]
{
new TerminatorOption("LF (\\n)", "\n"),
new TerminatorOption("CRLF (\\r\\n)", "\r\n"),
new TerminatorOption("CR (\\r)", "\r")
};
public int SelectedDaqLogicalId
{
get => _devOpts.Daq.LogicalDeviceId;
set
{
if (_devOpts.Daq.LogicalDeviceId == value) return;
_devOpts.Daq.LogicalDeviceId = value;
OnPropertyChanged();
ApplyDaqChannelEditorsToOptions();
_settingsStore.ScheduleSave();
}
}
public string SspcStatus => _sspc.IsConnected ? "已连接" : "未连接";
public string PowerStatus => _power.IsConnected ? "已连接" : "未连接";
public string LoadStatus => _load.IsConnected ? "已连接" : "未连接";
public string DaqStatus => _daq.IsConnected ? "已连接" : "未连接";
public void SetCloseAction(Action closeAction) => _closeAction = closeAction;
public void ScheduleSave()
{
ApplyDaqChannelEditorsToOptions();
_settingsStore.ScheduleSave();
}
public async Task SaveSettingsAsync()
{
ApplyDaqChannelEditorsToOptions();
await _settingsStore.SaveAsync();
}
[RelayCommand]
private async Task Close()
{
ApplyDaqChannelEditorsToOptions();
await _settingsStore.SaveAsync();
_closeAction?.Invoke();
}
[RelayCommand]
private void RefreshPorts()
{
string? selected = _devOpts.Sspc.PortName;
PortNames.Clear();
foreach (string port in SerialPort.GetPortNames().OrderBy(x => x))
PortNames.Add(port);
if (!string.IsNullOrWhiteSpace(selected) && !PortNames.Contains(selected))
PortNames.Add(selected);
}
[RelayCommand]
private async Task SaveAndReconnect()
{
ApplyDaqChannelEditorsToOptions();
await _settingsStore.SaveAsync();
await Reconnect("SSPC", _sspc);
await Reconnect("电源", _power);
await Reconnect("负载", _load);
await TestDaq();
RaiseStatuses();
_log.Add(UiLogLevel.Success, "硬件配置已保存并重新连接。");
}
[RelayCommand]
private async Task TestSspc() { await Reconnect("SSPC", _sspc); OnPropertyChanged(nameof(SspcStatus)); }
[RelayCommand]
private async Task TestPower()
{
await Reconnect("电源", _power);
if (_power.IsConnected)
{
try
{
var id = await _power.QueryIdentityAsync();
_log.Add(UiLogLevel.Success, $"UDP5080 设备标识:{id}");
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Warning, $"UDP5080 已连接,但读取设备标识失败:{ex.Message}");
}
}
OnPropertyChanged(nameof(PowerStatus));
}
[RelayCommand]
private async Task TestLoad()
{
await Reconnect("负载", _load);
if (_load.IsConnected)
{
try
{
var id = await _load.QueryIdentityAsync();
var reading = await _load.ReadPowerAsync();
_log.Add(UiLogLevel.Success, $"IT8702P 设备标识:{id}V={reading.Volts:F3}V I={reading.Amps:F3}A P={reading.Watts:F3}W");
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Warning, $"IT8702P 已连接,但读取标识或测量值失败:{ex.Message}");
}
}
OnPropertyChanged(nameof(LoadStatus));
}
[RelayCommand]
private async Task TestDaq()
{
ApplyDaqChannelEditorsToOptions();
try { await _daq.DisconnectAsync(); } catch { }
await RefreshDaqDevices();
try
{
await _daq.ConnectAsync();
await _settingsStore.SaveAsync();
_log.Add(UiLogLevel.Success, "PCIe8586 连接成功。");
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Error, $"PCIe8586 连接失败:{ex.Message}");
}
OnPropertyChanged(nameof(DaqStatus));
}
[RelayCommand]
private async Task RefreshDaqDevices()
{
try
{
var devices = await _daq.EnumerateDevicesAsync();
if (devices.Count > 0)
{
int selectedId = _devOpts.Daq.LogicalDeviceId;
DaqDevices.Clear();
foreach (var device in devices) DaqDevices.Add(device);
if (devices.Any(x => x.LogicalId == selectedId))
_devOpts.Daq.LogicalDeviceId = selectedId;
else
SelectedDaqLogicalId = devices[0].LogicalId;
OnPropertyChanged(nameof(SelectedDaqLogicalId));
}
else if (!DaqDevices.Any(x => x.LogicalId == _devOpts.Daq.LogicalDeviceId))
{
DaqDevices.Add(new DaqDeviceDescriptor(
_devOpts.Daq.LogicalDeviceId, null, "当前配置的 PCIe8586", _devOpts.Daq.UseMock));
}
_log.Add(devices.Count > 0 ? UiLogLevel.Success : UiLogLevel.Error,
devices.Count > 0 ? $"发现 {devices.Count} 台 PCIe8586。" : "未发现 PCIe8586。");
}
catch (Exception ex)
{
if (!DaqDevices.Any(x => x.LogicalId == _devOpts.Daq.LogicalDeviceId))
DaqDevices.Add(new DaqDeviceDescriptor(
_devOpts.Daq.LogicalDeviceId, null, "当前配置的 PCIe8586", _devOpts.Daq.UseMock));
_log.Add(UiLogLevel.Error, $"枚举 PCIe8586 失败:{ex.Message}");
}
}
public double DaqActualSampleRateHz => _devOpts.Daq.ActualSampleRateHz;
public string DaqValidationMessage
{
get
{
if (_devOpts.Daq.UseMock)
return "当前为 Mock 模式,可直接验证继电器动作和曲线流程。";
ApplyDaqChannelEditorsToOptions();
if (_devOpts.Daq.ChannelCount is not (1 or 2 or 4 or 8))
return "启用通道数只能为 1、2、4 或 8。";
var enabled = _devOpts.Daq.Channels.Where(x => x.PhysicalChannel < _devOpts.Daq.ChannelCount).ToArray();
if (enabled.Length == 0)
return "没有可用的启用通道。";
var outputVoltage = enabled.Where(x =>
string.Equals(x.Role, nameof(DaqChannelRole.OutputVoltage), StringComparison.OrdinalIgnoreCase)).ToArray();
if (outputVoltage.Length != 1)
return $"必须配置且只能配置一个 OutputVoltage 角色,当前配置了 {outputVoltage.Length} 个。";
var voltageChannel = outputVoltage[0];
if (!voltageChannel.IsCalibrated)
return $"CH{voltageChannel.PhysicalChannel} OutputVoltage 尚未勾选已标定。";
if (!double.IsFinite(voltageChannel.Scale) || voltageChannel.Scale == 0)
return $"CH{voltageChannel.PhysicalChannel} Scale 无效。";
if (!double.IsFinite(voltageChannel.Offset))
return $"CH{voltageChannel.PhysicalChannel} Offset 无效。";
return "采集配置可用于曲线测试。";
}
}
public void NotifyDaqSettingsChanged()
{
ApplyDaqChannelEditorsToOptions();
RefreshDaqEditorEnabledStates();
OnPropertyChanged(nameof(DaqActualSampleRateHz));
OnPropertyChanged(nameof(DaqValidationMessage));
}
[RelayCommand]
private async Task DisconnectSspc()
{
await _sspc.DisconnectAsync();
ApplyDaqChannelEditorsToOptions();
await _settingsStore.SaveAsync();
OnPropertyChanged(nameof(SspcStatus));
}
private async Task Reconnect(string name, IDeviceConnection device)
{
try
{
await device.DisconnectAsync();
bool connected = await device.ConnectAsync();
ApplyDaqChannelEditorsToOptions();
await _settingsStore.SaveAsync();
if (connected && device.IsConnected)
{
_log.Add(UiLogLevel.Success, $"{name} 连接成功。");
}
else
{
_log.Add(UiLogLevel.Warning, $"{name} 连接未成功,设备保持未连接。");
}
}
catch (Exception ex)
{
_log.Add(UiLogLevel.Error, $"{name} 连接失败:{ex.Message}");
}
}
private void RaiseStatuses()
{
OnPropertyChanged(nameof(SspcStatus));
OnPropertyChanged(nameof(PowerStatus));
OnPropertyChanged(nameof(LoadStatus));
OnPropertyChanged(nameof(DaqStatus));
}
[RelayCommand]
private void BrowseProjects()
{
string? selected = SelectFolder("选择项目数据目录", _storeOpts.ProjectsRoot);
if (string.IsNullOrWhiteSpace(selected)) return;
_storeOpts.ProjectsRoot = selected;
OnPropertyChanged(nameof(Storage));
ApplyDaqChannelEditorsToOptions();
_settingsStore.ScheduleSave();
}
[RelayCommand]
private void BrowseReports()
{
string? selected = SelectFolder("选择报告保存目录", _reportOpts.SavePath);
if (string.IsNullOrWhiteSpace(selected)) return;
_reportOpts.SavePath = selected;
OnPropertyChanged(nameof(Report));
ApplyDaqChannelEditorsToOptions();
_settingsStore.ScheduleSave();
}
[RelayCommand]
private void OpenProjectsDirectory() => OpenDirectory(_storeOpts.ProjectsRoot);
[RelayCommand]
private void OpenReportsDirectory() => OpenDirectory(_reportOpts.SavePath);
private static string? SelectFolder(string description, string currentPath)
{
var dialog = new OpenFolderDialog
{
Title = description,
InitialDirectory = Directory.Exists(currentPath) ? currentPath : null
};
return dialog.ShowDialog() == true ? dialog.FolderName : null;
}
private static void OpenDirectory(string path)
{
if (string.IsNullOrWhiteSpace(path)) return;
Directory.CreateDirectory(path);
System.Diagnostics.Process.Start("explorer.exe", path);
}
private void NormalizeDaqChannels()
{
_devOpts.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(_devOpts.Daq.Channels);
}
private void LoadDaqChannelEditors()
{
DaqChannelEditors.Clear();
NormalizeDaqChannels();
foreach (var channel in _devOpts.Daq.Channels)
{
var editor = new DaqChannelEditor
{
PhysicalChannel = channel.PhysicalChannel,
Role = channel.Role,
Name = channel.Name,
Scale = channel.Scale,
Offset = channel.Offset,
IsCalibrated = channel.IsCalibrated,
IsEnabled = channel.PhysicalChannel < _devOpts.Daq.ChannelCount
};
editor.Bind(channel, OnDaqChannelEditorChanged);
DaqChannelEditors.Add(editor);
}
ApplyDaqChannelEditorsToOptions();
}
private void OnDaqChannelEditorChanged()
{
ApplyDaqChannelEditorsToOptions();
_settingsStore.ScheduleSave();
OnPropertyChanged(nameof(DaqValidationMessage));
}
private void RefreshDaqEditorEnabledStates()
{
foreach (var editor in DaqChannelEditors)
editor.IsEnabled = editor.PhysicalChannel < _devOpts.Daq.ChannelCount;
}
private void ApplyDaqChannelEditorsToOptions()
{
if (DaqChannelEditors.Count == 0)
{
NormalizeDaqChannels();
return;
}
_devOpts.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(
DaqChannelEditors.Select(x => new DaqChannelOptions
{
PhysicalChannel = x.PhysicalChannel,
Role = x.Role,
Name = x.Name,
Scale = x.Scale,
Offset = x.Offset,
IsCalibrated = x.IsCalibrated
}));
}
}
public sealed record TerminatorOption(string Name, string Value);
public sealed partial class DaqChannelEditor : ObservableObject
{
private DaqChannelOptions? _source;
private Action? _changed;
public int PhysicalChannel { get; init; }
public string ChannelName => $"CH{PhysicalChannel}";
[ObservableProperty] private string _role = nameof(DaqChannelRole.Unconfigured);
[ObservableProperty] private string _name = "";
[ObservableProperty] private double _scale = 1;
[ObservableProperty] private double _offset;
[ObservableProperty] private bool _isCalibrated;
[ObservableProperty] private bool _isEnabled;
public void Bind(DaqChannelOptions source, Action changed)
{
_source = source;
_changed = changed;
}
partial void OnRoleChanged(string value)
{
if (_source == null) return;
_source.Role = value;
_changed?.Invoke();
}
partial void OnNameChanged(string value)
{
if (_source == null) return;
_source.Name = value;
_changed?.Invoke();
}
partial void OnScaleChanged(double value)
{
if (_source == null) return;
_source.Scale = value;
_changed?.Invoke();
}
partial void OnOffsetChanged(double value)
{
if (_source == null) return;
_source.Offset = value;
_changed?.Invoke();
}
partial void OnIsCalibratedChanged(bool value)
{
if (_source == null) return;
_source.IsCalibrated = value;
_changed?.Invoke();
}
}