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; /// 系统设置页。 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 devOpts, IOptions storeOpts, IOptions 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 PortNames { get; } = new(); public ObservableCollection DaqDevices { get; } = new(); public ObservableCollection DaqChannelEditors { get; } = new(); public IReadOnlyList DaqChannelCounts { get; } = new[] { 1, 2, 4, 8 }; public IReadOnlyList DaqInputRanges { get; } = new[] { "PlusMinus5V", "PlusMinus1V" }; public IReadOnlyList DaqRoles { get; } = Enum.GetNames(); public IReadOnlyList LoadQueryModes { get; } = Enum.GetValues(); public IReadOnlyList 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(); } }