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

822 lines
31 KiB
C#
Raw Permalink Normal View History

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;
2026-07-08 00:25:45 +08:00
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;
2026-07-08 00:25:45 +08:00
private readonly ProjectStore _projectStore;
private Action? _closeAction;
2026-07-08 00:25:45 +08:00
private Func<bool>? _canRefreshProjects;
private Func<string, Task>? _projectRefreshAction;
public SettingsViewModel(
IOptions<DeviceOptions> devOpts,
IOptions<StorageOptions> storeOpts,
IOptions<ReportOptions> reportOpts,
ISspc sspc, IPowerSupply power, ILoad load, IDaq daq, ISafetyDio safetyDio, UiLogSink log,
2026-07-08 00:25:45 +08:00
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;
2026-07-08 00:25:45 +08:00
_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<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<int> Pci2312Channels { get; } = Enumerable.Range(0, 16).ToArray();
public IReadOnlyList<string> DaqInputRanges { get; } = new[] { "PlusMinus5V", "PlusMinus1V" };
public IReadOnlyList<int> DaqClockDividerOptions { get; } = new[] { 1, 2, 5, 10, 20, 50, 100, 1_000, 10_000, 100_000 };
public IReadOnlyList<DaqRoleOption> 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<LoadQueryMode> LoadQueryModes { get; } = Enum.GetValues<LoadQueryMode>();
public IReadOnlyList<string> ReportFormatOptions { get; } = new[] { "Pdf", "Word", "Both" };
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 => 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;
2026-07-08 00:25:45 +08:00
public void SetProjectRefreshAction(Func<string, Task> refreshAction, Func<bool>? 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]
2026-07-08 00:25:45 +08:00
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();
2026-07-08 00:25:45 +08:00
ApplyProjectsRootToStore();
OnPropertyChanged(nameof(HasUnsavedChanges));
OnPropertyChanged(nameof(DaqValidationMessage));
2026-07-08 00:25:45 +08:00
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);
2026-07-08 00:25:45 +08:00
[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();
}