460 lines
16 KiB
C#
460 lines
16 KiB
C#
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();
|
||
}
|
||
}
|