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

460 lines
16 KiB
C#
Raw Normal View History

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();
}
}