using System.Collections.ObjectModel; using System.IO; using System.IO.Ports; using System.Text.Json; using System.Windows; 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.Logic.Storage; 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 ISafetyDio _safetyDio; private readonly DeviceOptions _runtimeDevices; private readonly StorageOptions _runtimeStorage; private readonly ReportOptions _runtimeReport; private readonly UserSettingsStore _settingsStore; private readonly ProjectStore _projectStore; private Action? _closeAction; private Func? _canRefreshProjects; private Func? _projectRefreshAction; public SettingsViewModel( IOptions devOpts, IOptions storeOpts, IOptions reportOpts, ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, ISafetyDio safetyDio, UiLogSink log, UserSettingsStore settingsStore, ProjectStore projectStore) { _runtimeDevices = devOpts.Value; _runtimeStorage = storeOpts.Value; _runtimeReport = reportOpts.Value; _sspc = sspc; _power = power; _load = load; _daq = daq; _safetyDio = safetyDio; _log = log; _settingsStore = settingsStore; _projectStore = projectStore; var persisted = _settingsStore.LoadSnapshot(); Devices = CloneDeviceOptions(persisted?.Devices ?? _runtimeDevices); Storage = CloneStorageOptions(persisted?.Storage ?? _runtimeStorage); Report = CloneReportOptions(persisted?.Report ?? _runtimeReport); Devices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(Devices.Daq.Channels); LoadDaqChannelEditors(); RefreshPorts(); _sspc.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(SspcStatus)); _power.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(PowerStatus)); _load.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(LoadStatus)); _daq.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(DaqStatus)); _safetyDio.ConnectionChanged += (_, _) => OnPropertyChanged(nameof(Pci2312Status)); _ = RefreshDaqDevices(); } public DeviceOptions Devices { get; } public StorageOptions Storage { get; } public ReportOptions Report { get; } 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 Pci2312Channels { get; } = Enumerable.Range(0, 16).ToArray(); public IReadOnlyList DaqInputRanges { get; } = new[] { "PlusMinus5V", "PlusMinus1V" }; public IReadOnlyList DaqClockDividerOptions { get; } = new[] { 1, 2, 5, 10, 20, 50, 100, 1_000, 10_000, 100_000 }; public IReadOnlyList DaqRoleOptions { get; } = new[] { new DaqRoleOption(nameof(DaqChannelRole.Unconfigured), "未配置"), new DaqRoleOption(nameof(DaqChannelRole.OutputVoltage), "输出电压"), new DaqRoleOption(nameof(DaqChannelRole.OutputCurrent), "输出电流"), new DaqRoleOption(nameof(DaqChannelRole.InputVoltage), "输入电压"), new DaqRoleOption(nameof(DaqChannelRole.InputCurrent), "输入电流"), new DaqRoleOption(nameof(DaqChannelRole.Auxiliary), "辅助") }; public IReadOnlyList LoadQueryModes { get; } = Enum.GetValues(); public IReadOnlyList ReportFormatOptions { get; } = new[] { "Pdf", "Word", "Both" }; 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 => Devices.Daq.LogicalDeviceId; set { if (Devices.Daq.LogicalDeviceId == value) return; Devices.Daq.LogicalDeviceId = value; OnPropertyChanged(); NotifyDraftChanged(); } } public string SspcStatus => _sspc.IsConnected ? "已连接" : "未连接"; public string PowerStatus => _power.IsConnected ? "已连接" : "未连接"; public string LoadStatus => _load.IsConnected ? "已连接" : "未连接"; public string DaqStatus => _daq.IsConnected ? "已连接" : "未连接"; public string Pci2312Status => _safetyDio.IsConnected ? "已连接" : "未连接"; public double DaqActualSampleRateHz => Devices.Daq.ActualSampleRateHz; public bool HasUnsavedChanges => !SettingsFingerprint(Devices, Storage, Report) .Equals(SettingsFingerprint(_runtimeDevices, _runtimeStorage, _runtimeReport), StringComparison.Ordinal); public string DaqValidationMessage { get { ApplyDaqChannelEditorsToDraft(); if (Devices.Daq.ChannelCount is not (1 or 2 or 4 or 8)) return "启用通道数只能为 1、2、4 或 8。"; if (Devices.Daq.ClockDivider < 1) return "PCIe8586 时钟分频必须大于 0。"; if (Devices.Daq.PreTriggerMs <= 0 || Devices.Daq.PostTriggerMs <= 0) return "前置和后置采集窗口必须大于 0。"; var enabled = Devices.Daq.Channels.Where(x => x.PhysicalChannel < Devices.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.Length} 个。"; var voltageChannel = outputVoltage[0]; if (!voltageChannel.IsCalibrated) return $"CH{voltageChannel.PhysicalChannel} 输出电压尚未勾选已标定。"; if (!double.IsFinite(voltageChannel.Scale) || voltageChannel.Scale == 0) return $"CH{voltageChannel.PhysicalChannel} Scale 无效。"; if (!double.IsFinite(voltageChannel.Offset)) return $"CH{voltageChannel.PhysicalChannel} Offset 无效。"; return HasUnsavedChanges ? "采集配置有效。未保存,测试仍会使用上一次保存的配置。" : "采集配置有效。"; } } public void SetCloseAction(Action closeAction) => _closeAction = closeAction; public void SetProjectRefreshAction(Func refreshAction, Func? canRefresh = null) { _projectRefreshAction = refreshAction; _canRefreshProjects = canRefresh; } public void NotifyDraftChanged() { ApplyDaqChannelEditorsToDraft(); RefreshDaqEditorEnabledStates(); OnPropertyChanged(nameof(DaqActualSampleRateHz)); OnPropertyChanged(nameof(DaqValidationMessage)); OnPropertyChanged(nameof(HasUnsavedChanges)); } [RelayCommand] private void Close() => _closeAction?.Invoke(); [RelayCommand] private async Task SaveSettings() => await SaveSettingsCore(logSaved: true); private async Task SaveSettingsCore(bool logSaved) { ApplyDaqChannelEditorsToDraft(); Devices.Daq.Channels = DaqChannelOptionsNormalizer.Normalize(Devices.Daq.Channels); await _settingsStore.SaveAsync(Devices, Storage, Report); var persisted = _settingsStore.LoadSnapshot(); if (persisted is not null) { ApplyToRuntime(persisted.Devices, _runtimeDevices); ApplyToRuntime(persisted.Storage, _runtimeStorage); ApplyToRuntime(persisted.Report, _runtimeReport); ApplyToRuntime(persisted.Devices, Devices); ApplyToRuntime(persisted.Storage, Storage); ApplyToRuntime(persisted.Report, Report); } else { ApplyToRuntime(Devices, _runtimeDevices); ApplyToRuntime(Storage, _runtimeStorage); ApplyToRuntime(Report, _runtimeReport); } LoadDaqChannelEditors(); ApplyProjectsRootToStore(); OnPropertyChanged(nameof(HasUnsavedChanges)); OnPropertyChanged(nameof(DaqValidationMessage)); if (logSaved) _log.Add(UiLogLevel.Success, "设置已保存。重新连接请单独点击“重新连接”。"); } private void ApplyProjectsRootToStore() { _projectStore.ProjectsRoot = _runtimeStorage.ProjectsRoot; if (!string.IsNullOrWhiteSpace(_projectStore.ProjectsRoot)) Directory.CreateDirectory(_projectStore.ProjectsRoot); } [RelayCommand] private void RefreshPorts() { string? selected = Devices.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 ReconnectAll() { if (!EnsureSavedBeforeDeviceAction()) return; await Reconnect("SSPC", _sspc).ConfigureAwait(false); await Reconnect("电源", _power).ConfigureAwait(false); await Reconnect("负载", _load).ConfigureAwait(false); await TestDaqSavedConfig().ConfigureAwait(false); await Reconnect("PCI2312", _safetyDio).ConfigureAwait(false); RaiseStatuses(); } [RelayCommand] private async Task TestSspc() { if (!EnsureSavedBeforeDeviceAction()) return; await Reconnect("SSPC", _sspc).ConfigureAwait(false); OnPropertyChanged(nameof(SspcStatus)); } [RelayCommand] private async Task TestPower() { if (!EnsureSavedBeforeDeviceAction()) return; await Reconnect("电源", _power).ConfigureAwait(false); if (_power.IsConnected) { try { var id = await _power.QueryIdentityAsync().ConfigureAwait(false); _log.Add(UiLogLevel.Success, $"UDP5080 设备标识:{id}"); } catch (Exception ex) { _log.Add(UiLogLevel.Warning, $"UDP5080 已连接,但读取设备标识失败:{ex.Message}"); } } OnPropertyChanged(nameof(PowerStatus)); } [RelayCommand] private async Task TestLoad() { if (!EnsureSavedBeforeDeviceAction()) return; await Reconnect("负载", _load).ConfigureAwait(false); if (_load.IsConnected) { try { var id = await _load.QueryIdentityAsync().ConfigureAwait(false); var reading = await _load.ReadPowerAsync().ConfigureAwait(false); _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() { if (!EnsureSavedBeforeDeviceAction()) return; await TestDaqSavedConfig().ConfigureAwait(false); } [RelayCommand] private async Task TestPci2312() { if (!EnsureSavedBeforeDeviceAction()) return; await Reconnect("PCI2312", _safetyDio).ConfigureAwait(false); if (_safetyDio.IsConnected) { try { bool input = await _safetyDio.ReadInputAsync(_runtimeDevices.Pci2312.EmergencyInputChannel).ConfigureAwait(false); _log.Add(UiLogLevel.Success, $"PCI2312 连接成功,DI{_runtimeDevices.Pci2312.EmergencyInputChannel}={(input ? 1 : 0)}"); } catch (Exception ex) { _log.Add(UiLogLevel.Warning, $"PCI2312 已连接,但读取输入失败:{ex.Message}"); } } OnPropertyChanged(nameof(Pci2312Status)); } [RelayCommand] private async Task RefreshDaqDevices() { try { var devices = await _daq.EnumerateDevicesAsync().ConfigureAwait(false); await RunOnUiAsync(() => { DaqDevices.Clear(); foreach (var device in devices) DaqDevices.Add(device); if (devices.Count > 0) { if (!devices.Any(x => x.LogicalId == SelectedDaqLogicalId)) SelectedDaqLogicalId = devices[0].LogicalId; OnPropertyChanged(nameof(SelectedDaqLogicalId)); } else { DaqDevices.Add(new DaqDeviceDescriptor( SelectedDaqLogicalId, null, "当前配置的 PCIe8586", Devices.Daq.UseMock)); } }).ConfigureAwait(false); _log.Add(devices.Count > 0 ? UiLogLevel.Success : UiLogLevel.Warning, devices.Count > 0 ? $"发现 {devices.Count} 张 PCIe8586。" : "未发现 PCIe8586。"); } catch (Exception ex) { await RunOnUiAsync(() => { if (!DaqDevices.Any(x => x.LogicalId == SelectedDaqLogicalId)) DaqDevices.Add(new DaqDeviceDescriptor( SelectedDaqLogicalId, null, "当前配置的 PCIe8586", Devices.Daq.UseMock)); }).ConfigureAwait(false); _log.Add(UiLogLevel.Error, $"枚举 PCIe8586 失败:{ex.Message}"); } } [RelayCommand] private async Task DisconnectSspc() { await _sspc.DisconnectAsync().ConfigureAwait(false); OnPropertyChanged(nameof(SspcStatus)); } [RelayCommand] private async Task DisconnectDaq() { await _daq.DisconnectAsync().ConfigureAwait(false); OnPropertyChanged(nameof(DaqStatus)); } [RelayCommand] private async Task DisconnectPci2312() { await _safetyDio.DisconnectAsync().ConfigureAwait(false); OnPropertyChanged(nameof(Pci2312Status)); } [RelayCommand] private void BrowseProjects() { string? selected = SelectFolder("选择项目数据目录", Storage.ProjectsRoot); if (string.IsNullOrWhiteSpace(selected)) return; Storage.ProjectsRoot = selected; OnPropertyChanged(nameof(Storage)); NotifyDraftChanged(); } [RelayCommand] private void BrowseReports() { string? selected = SelectFolder("选择报告保存目录", Report.SavePath); if (string.IsNullOrWhiteSpace(selected)) return; Report.SavePath = selected; OnPropertyChanged(nameof(Report)); NotifyDraftChanged(); } [RelayCommand] private void OpenProjectsDirectory() => OpenDirectory(Storage.ProjectsRoot); [RelayCommand] private async Task RefreshProjects() { NotifyDraftChanged(); if (string.IsNullOrWhiteSpace(Storage.ProjectsRoot)) { _log.Add(UiLogLevel.Error, "项目数据路径为空,无法刷新工程。"); return; } if (_canRefreshProjects?.Invoke() == false) return; if (_projectRefreshAction == null) { _log.Add(UiLogLevel.Warning, "刷新工程功能尚未初始化。"); return; } try { await SaveSettingsCore(logSaved: false); await _projectRefreshAction(_projectStore.ProjectsRoot); await RunOnUiAsync(() => { OnPropertyChanged(nameof(HasUnsavedChanges)); OnPropertyChanged(nameof(DaqValidationMessage)); }); _log.Add(UiLogLevel.Success, "工程列表已刷新。"); } catch (Exception ex) { _log.Add(UiLogLevel.Error, $"刷新工程失败:{ex.Message}"); } } [RelayCommand] private void OpenReportsDirectory() => OpenDirectory(Report.SavePath); private bool EnsureSavedBeforeDeviceAction() { NotifyDraftChanged(); if (!HasUnsavedChanges) return true; _log.Add(UiLogLevel.Warning, "当前设置尚未保存。请先点击“保存设置”,再执行连接或测试。"); return false; } private async Task TestDaqSavedConfig() { try { await _daq.DisconnectAsync().ConfigureAwait(false); } catch { } await RefreshDaqDevices().ConfigureAwait(false); try { await _daq.ConnectAsync().ConfigureAwait(false); var channels = Enumerable.Range(0, _runtimeDevices.Daq.ChannelCount).ToArray(); var readings = await _daq.ReadInstantAsync(channels).ConfigureAwait(false); string preview = string.Join(", ", channels.Zip(readings, (ch, value) => $"CH{ch}={value:F4}")); _log.Add(UiLogLevel.Success, $"PCIe8586 连接并采集成功:{preview}"); } catch (Exception ex) { _log.Add(UiLogLevel.Error, $"PCIe8586 连接失败:{ex.Message}"); } await RunOnUiAsync(() => OnPropertyChanged(nameof(DaqStatus))).ConfigureAwait(false); } private static Task RunOnUiAsync(Action action) { var dispatcher = Application.Current?.Dispatcher; if (dispatcher is null || dispatcher.CheckAccess()) { action(); return Task.CompletedTask; } return dispatcher.InvokeAsync(action).Task; } private async Task Reconnect(string name, IDeviceConnection device) { try { await device.DisconnectAsync().ConfigureAwait(false); bool connected = await device.ConnectAsync().ConfigureAwait(false); _log.Add(connected && device.IsConnected ? UiLogLevel.Success : UiLogLevel.Warning, connected && device.IsConnected ? $"{name} 连接成功。" : $"{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)); OnPropertyChanged(nameof(Pci2312Status)); } private void LoadDaqChannelEditors() { DaqChannelEditors.Clear(); foreach (var channel in Devices.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 < Devices.Daq.ChannelCount }; editor.SetChangeHandler(OnDaqChannelEditorChanged); DaqChannelEditors.Add(editor); } ApplyDaqChannelEditorsToDraft(); } private void OnDaqChannelEditorChanged() { ApplyDaqChannelEditorsToDraft(); OnPropertyChanged(nameof(DaqValidationMessage)); OnPropertyChanged(nameof(HasUnsavedChanges)); } private void RefreshDaqEditorEnabledStates() { foreach (var editor in DaqChannelEditors) editor.IsEnabled = editor.PhysicalChannel < Devices.Daq.ChannelCount; } private void ApplyDaqChannelEditorsToDraft() { if (DaqChannelEditors.Count == 0) return; Devices.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 })); } 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 static DeviceOptions CloneDeviceOptions(DeviceOptions source) => new() { MockMode = source.MockMode, Sspc = CloneSspcOptions(source.Sspc), PowerSupply = ClonePowerSupplyOptions(source.PowerSupply), Load = CloneLoadOptions(source.Load), Daq = CloneDaqOptions(source.Daq), Pci2312 = ClonePci2312Options(source.Pci2312) }; private static SspcOptions CloneSspcOptions(SspcOptions source) => new() { UseMock = source.UseMock, AutoConnect = source.AutoConnect, PortName = source.PortName, BaudRate = source.BaudRate, DataBits = source.DataBits, Parity = source.Parity, StopBits = source.StopBits, ReadTimeoutMs = source.ReadTimeoutMs, Retry = source.Retry, DeviceProfilePath = source.DeviceProfilePath, CloseAllRelaysOnDisconnect = source.CloseAllRelaysOnDisconnect, LogRawFrames = source.LogRawFrames, ModuleAddresses = source.ModuleAddresses.ToArray(), ChannelsPerModule = source.ChannelsPerModule }; private static PowerSupplyOptions ClonePowerSupplyOptions(PowerSupplyOptions source) => new() { UseMock = source.UseMock, AutoConnect = source.AutoConnect, PortName = source.PortName, BaudRate = source.BaudRate, Host = source.Host, Port = source.Port, TimeoutSeconds = source.TimeoutSeconds, RequirePingBeforeConnect = source.RequirePingBeforeConnect, Terminator = source.Terminator, IdnCommand = source.IdnCommand, VoltageCommand = source.VoltageCommand, CurrentCommand = source.CurrentCommand, SetVoltageCommand = source.SetVoltageCommand, SetCurrentLimitCommand = source.SetCurrentLimitCommand, ErrorCommand = source.ErrorCommand }; private static LoadOptions CloneLoadOptions(LoadOptions source) => new() { UseMock = source.UseMock, AutoConnect = source.AutoConnect, PortName = source.PortName, BaudRate = source.BaudRate, Host = source.Host, Port = source.Port, TimeoutSeconds = source.TimeoutSeconds, Terminator = source.Terminator, Channel = source.Channel, UseFetch = source.UseFetch, QueryMode = source.QueryMode, SetModeCommand = source.SetModeCommand, SetValueCommand = source.SetValueCommand, ResetCommand = source.ResetCommand, ErrorCommand = source.ErrorCommand }; private static DaqOptions CloneDaqOptions(DaqOptions source) => new() { UseMock = source.UseMock, AutoConnect = source.AutoConnect, LogicalDeviceId = source.LogicalDeviceId, ChannelCount = source.ChannelCount, InputRange = source.InputRange, ClockDivider = source.ClockDivider, PreTriggerMs = source.PreTriggerMs, PostTriggerMs = source.PostTriggerMs, Channels = DaqChannelOptionsNormalizer.Normalize(source.Channels.Select(CloneDaqChannelOptions)) }; private static Pci2312Options ClonePci2312Options(Pci2312Options source) => new() { Enabled = source.Enabled, UseMock = source.UseMock, AutoConnect = source.AutoConnect, DeviceId = source.DeviceId, EmergencyInputChannel = source.EmergencyInputChannel, EmergencyInputActiveHigh = source.EmergencyInputActiveHigh, AlarmOutputChannel = source.AlarmOutputChannel, AlarmOutputActiveHigh = source.AlarmOutputActiveHigh, PollIntervalMs = source.PollIntervalMs }; private static DaqChannelOptions CloneDaqChannelOptions(DaqChannelOptions source) => new() { PhysicalChannel = source.PhysicalChannel, Role = source.Role, Name = source.Name, Scale = source.Scale, Offset = source.Offset, IsCalibrated = source.IsCalibrated }; private static StorageOptions CloneStorageOptions(StorageOptions source) => new() { ProjectsRoot = source.ProjectsRoot }; private static ReportOptions CloneReportOptions(ReportOptions source) => new() { SavePath = source.SavePath, Format = source.Format }; private static void ApplyToRuntime(DeviceOptions source, DeviceOptions target) { target.MockMode = source.MockMode; ApplyToRuntime(source.Sspc, target.Sspc); ApplyToRuntime(source.PowerSupply, target.PowerSupply); ApplyToRuntime(source.Load, target.Load); ApplyToRuntime(source.Daq, target.Daq); ApplyToRuntime(source.Pci2312, target.Pci2312); } private static void ApplyToRuntime(SspcOptions source, SspcOptions target) { target.UseMock = source.UseMock; target.AutoConnect = source.AutoConnect; target.PortName = source.PortName; target.BaudRate = source.BaudRate; target.DataBits = source.DataBits; target.Parity = source.Parity; target.StopBits = source.StopBits; target.ReadTimeoutMs = source.ReadTimeoutMs; target.Retry = source.Retry; target.DeviceProfilePath = source.DeviceProfilePath; target.CloseAllRelaysOnDisconnect = source.CloseAllRelaysOnDisconnect; target.LogRawFrames = source.LogRawFrames; target.ModuleAddresses = source.ModuleAddresses.ToArray(); target.ChannelsPerModule = source.ChannelsPerModule; } private static void ApplyToRuntime(PowerSupplyOptions source, PowerSupplyOptions target) { target.UseMock = source.UseMock; target.AutoConnect = source.AutoConnect; target.PortName = source.PortName; target.BaudRate = source.BaudRate; target.Host = source.Host; target.Port = source.Port; target.TimeoutSeconds = source.TimeoutSeconds; target.RequirePingBeforeConnect = source.RequirePingBeforeConnect; target.Terminator = source.Terminator; target.IdnCommand = source.IdnCommand; target.VoltageCommand = source.VoltageCommand; target.CurrentCommand = source.CurrentCommand; target.SetVoltageCommand = source.SetVoltageCommand; target.SetCurrentLimitCommand = source.SetCurrentLimitCommand; target.ErrorCommand = source.ErrorCommand; } private static void ApplyToRuntime(LoadOptions source, LoadOptions target) { target.UseMock = source.UseMock; target.AutoConnect = source.AutoConnect; target.PortName = source.PortName; target.BaudRate = source.BaudRate; target.Host = source.Host; target.Port = source.Port; target.TimeoutSeconds = source.TimeoutSeconds; target.Terminator = source.Terminator; target.Channel = source.Channel; target.UseFetch = source.UseFetch; target.QueryMode = source.QueryMode; target.SetModeCommand = source.SetModeCommand; target.SetValueCommand = source.SetValueCommand; target.ResetCommand = source.ResetCommand; target.ErrorCommand = source.ErrorCommand; } private static void ApplyToRuntime(DaqOptions source, DaqOptions target) { target.UseMock = source.UseMock; target.AutoConnect = source.AutoConnect; target.LogicalDeviceId = source.LogicalDeviceId; target.ChannelCount = source.ChannelCount; target.InputRange = source.InputRange; target.ClockDivider = source.ClockDivider; target.PreTriggerMs = source.PreTriggerMs; target.PostTriggerMs = source.PostTriggerMs; target.Channels = DaqChannelOptionsNormalizer.Normalize(source.Channels.Select(CloneDaqChannelOptions)); } private static void ApplyToRuntime(Pci2312Options source, Pci2312Options target) { target.Enabled = source.Enabled; target.UseMock = source.UseMock; target.AutoConnect = source.AutoConnect; target.DeviceId = source.DeviceId; target.EmergencyInputChannel = source.EmergencyInputChannel; target.EmergencyInputActiveHigh = source.EmergencyInputActiveHigh; target.AlarmOutputChannel = source.AlarmOutputChannel; target.AlarmOutputActiveHigh = source.AlarmOutputActiveHigh; target.PollIntervalMs = source.PollIntervalMs; } private static void ApplyToRuntime(StorageOptions source, StorageOptions target) { target.ProjectsRoot = source.ProjectsRoot; } private static void ApplyToRuntime(ReportOptions source, ReportOptions target) { target.SavePath = source.SavePath; target.Format = source.Format; } private static string SettingsFingerprint(DeviceOptions devices, StorageOptions storage, ReportOptions report) { var model = new { Devices = devices, Storage = storage, Report = report }; return JsonSerializer.Serialize(model); } } public sealed record TerminatorOption(string Name, string Value); public sealed record DaqRoleOption(string Value, string Name); public sealed partial class DaqChannelEditor : ObservableObject { 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 SetChangeHandler(Action changed) => _changed = changed; partial void OnRoleChanged(string value) => _changed?.Invoke(); partial void OnNameChanged(string value) => _changed?.Invoke(); partial void OnScaleChanged(double value) => _changed?.Invoke(); partial void OnOffsetChanged(double value) => _changed?.Invoke(); partial void OnIsCalibratedChanged(bool value) => _changed?.Invoke(); }