using System.Collections.ObjectModel; using System.Windows; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.Extensions.Options; using SSPCTester.Devices.Interfaces; using SSPCTester.UI.Services; namespace SSPCTester.UI.ViewModels; public partial class ExperimentConsoleViewModel : ObservableObject { private readonly IPowerSupply _power; private readonly ISspc _sspc; private readonly ILoad _load; private readonly ConsoleOptions _options; private readonly UserSettingsStore _settingsStore; private readonly UiLogSink _log; private readonly object _pollLock = new(); private readonly HashSet _activeHosts = new(ReferenceEqualityComparer.Instance); private CancellationTokenSource? _pollCts; private Task? _pollTask; [ObservableProperty] private bool _busy; [ObservableProperty] private double _powerVoltageSetpoint = 3.0; [ObservableProperty] private double _powerCurrentLimit = 1.0; [ObservableProperty] private PowerPreset? _selectedPowerPreset; [ObservableProperty] private double _powerVoltage = double.NaN; [ObservableProperty] private double _powerCurrent = double.NaN; [ObservableProperty] private double _powerWatts = double.NaN; [ObservableProperty] private bool? _isPowerOutputOn; [ObservableProperty] private string _powerOutputStateText = "未知"; [ObservableProperty] private string _powerModeText = "未知"; [ObservableProperty] private string _powerProtectionStateText = "未连接"; [ObservableProperty] private double _loadCurrentSetpoint = 1.0; [ObservableProperty] private LoadPreset? _selectedLoadPreset; [ObservableProperty] private double _loadVoltage = double.NaN; [ObservableProperty] private double _loadCurrent = double.NaN; [ObservableProperty] private double _loadWatts = double.NaN; [ObservableProperty] private bool? _isLoadInputOn; [ObservableProperty] private string _loadInputStateText = "未知"; [ObservableProperty] private string _loadProtectionStateText = "未连接"; public ObservableCollection PowerPresets { get; } = new(); public ObservableCollection LoadPresets { get; } = new(); public ObservableCollection Channels { get; } = new(); public ExperimentConsoleViewModel( IPowerSupply power, ISspc sspc, ILoad load, IOptions options, UserSettingsStore settingsStore, UiLogSink log) { _power = power; _sspc = sspc; _load = load; _options = options.Value; _settingsStore = settingsStore; _log = log; foreach (var preset in _options.PowerPresets) PowerPresets.Add(Clone(preset)); foreach (var preset in _options.LoadPresets) LoadPresets.Add(Clone(preset)); for (int i = 1; i <= _sspc.ChannelCount; i++) Channels.Add(new ExperimentConsoleChannelRow(i)); SelectedPowerPreset = PowerPresets.FirstOrDefault(); SelectedLoadPreset = LoadPresets.FirstOrDefault(); _power.ConnectionChanged += (_, _) => RaiseConnectionStatus(); _sspc.ConnectionChanged += (_, _) => RaiseConnectionStatus(); _load.ConnectionChanged += (_, _) => RaiseConnectionStatus(); } public string PowerConnectionStatus => _power.IsConnected ? "已连接" : "未连接"; public string RelayConnectionStatus => _sspc.IsConnected ? "已连接" : "未连接"; public string LoadConnectionStatus => _load.IsConnected ? "已连接" : "未连接"; public bool IsPowerConnected => _power.IsConnected; public bool IsRelayConnected => _sspc.IsConnected; public bool IsLoadConnected => _load.IsConnected; public bool CanTurnPowerOutputOn => IsPowerOutputOn != true; public bool CanTurnPowerOutputOff => IsPowerOutputOn != false; public bool CanTurnLoadInputOn => IsLoadInputOn != true; public bool CanTurnLoadInputOff => IsLoadInputOn != false; partial void OnIsPowerOutputOnChanged(bool? value) { OnPropertyChanged(nameof(CanTurnPowerOutputOn)); OnPropertyChanged(nameof(CanTurnPowerOutputOff)); } partial void OnIsLoadInputOnChanged(bool? value) { OnPropertyChanged(nameof(CanTurnLoadInputOn)); OnPropertyChanged(nameof(CanTurnLoadInputOff)); } partial void OnSelectedPowerPresetChanged(PowerPreset? value) { DeletePowerPresetCommand.NotifyCanExecuteChanged(); if (value == null) return; PowerVoltageSetpoint = value.Voltage; PowerCurrentLimit = value.CurrentLimit; } partial void OnSelectedLoadPresetChanged(LoadPreset? value) { DeleteLoadPresetCommand.NotifyCanExecuteChanged(); if (value == null) return; LoadCurrentSetpoint = value.Current; } public void SetActive(object host, bool active) { ArgumentNullException.ThrowIfNull(host); lock (_pollLock) { if (active) { _activeHosts.Add(host); StartPolling(); } else { _activeHosts.Remove(host); if (_activeHosts.Count == 0) StopPolling(); } } } [RelayCommand] private async Task ApplyPower() { if (!ValidateNonNegative(PowerVoltageSetpoint, "电源电压设定") || !ValidateNonNegative(PowerCurrentLimit, "电源电流限值")) return; await RunDeviceActionAsync("应用电源设置", async ct => { if (!await EnsureConnectedAsync(_power, "电源", ct)) return; await _power.SetVoltageAsync(PowerVoltageSetpoint, ct); await _power.SetCurrentLimitAsync(PowerCurrentLimit, ct); _log.Add(UiLogLevel.Success, $"电源设置已应用:V={PowerVoltageSetpoint:F3}V,限流={PowerCurrentLimit:F3}A。"); await RefreshPowerAsync(ct); }); } [RelayCommand] private async Task SavePowerPreset() { if (!ValidateNonNegative(PowerVoltageSetpoint, "电源电压设定") || !ValidateNonNegative(PowerCurrentLimit, "电源电流限值")) return; string name = $"{PowerVoltageSetpoint:F3}V / {PowerCurrentLimit:F3}A"; var existing = PowerPresets.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); if (existing == null) { existing = new PowerPreset { Name = name }; PowerPresets.Add(existing); } existing.Voltage = PowerVoltageSetpoint; existing.CurrentLimit = PowerCurrentLimit; SelectedPowerPreset = existing; await PersistPresetsAsync(); _log.Add(UiLogLevel.Success, $"电源预设已保存:{existing.Name}。"); } private bool CanDeletePowerPreset() => SelectedPowerPreset != null; [RelayCommand(CanExecute = nameof(CanDeletePowerPreset))] private async Task DeletePowerPreset() { var preset = SelectedPowerPreset; if (preset == null) return; if (!Confirm($"确认删除电源预设:{preset.Name}?")) return; int index = PowerPresets.IndexOf(preset); PowerPresets.Remove(preset); SelectedPowerPreset = PowerPresets.Count == 0 ? null : PowerPresets[Math.Clamp(index, 0, PowerPresets.Count - 1)]; await PersistPresetsAsync(); _log.Add(UiLogLevel.Warning, $"电源预设已删除:{preset.Name}。"); } [RelayCommand] private async Task PowerOutputOn() => await SetPowerOutputAsync(true); [RelayCommand] private async Task PowerOutputOff() => await SetPowerOutputAsync(false); [RelayCommand] private async Task PowerSafeReset() { if (!Confirm("确认执行电源安全复位?")) return; await RunDeviceActionAsync("电源安全复位", async ct => { if (!await EnsureConnectedAsync(_power, "电源", ct)) return; await _power.OutputAsync(false, ct); await _power.SetVoltageAsync(0, ct); await _power.SetCurrentLimitAsync(0, ct); PowerVoltageSetpoint = 0; PowerCurrentLimit = 0; SelectedPowerPreset = null; _log.Add(UiLogLevel.Warning, "电源已安全复位:输出关闭,电压和电流限值设为 0。"); await RefreshPowerAsync(ct); }); } [RelayCommand] private async Task ApplyLoad() { if (!ValidateNonNegative(LoadCurrentSetpoint, "负载电流")) return; await RunDeviceActionAsync("应用负载设置", async ct => { if (!await EnsureConnectedAsync(_load, "负载", ct)) return; await _load.SetModeAsync(LoadMode.CC, ct); await _load.SetValueAsync(LoadCurrentSetpoint, ct); _log.Add(UiLogLevel.Success, $"负载设置已应用:CC {LoadCurrentSetpoint:F3}A。"); await RefreshLoadAsync(ct); }); } [RelayCommand] private async Task SaveLoadPreset() { if (!ValidateNonNegative(LoadCurrentSetpoint, "负载电流")) return; string name = $"{LoadCurrentSetpoint:F3}A"; var existing = LoadPresets.FirstOrDefault(x => string.Equals(x.Name, name, StringComparison.OrdinalIgnoreCase)); if (existing == null) { existing = new LoadPreset { Name = name }; LoadPresets.Add(existing); } existing.Current = LoadCurrentSetpoint; SelectedLoadPreset = existing; await PersistPresetsAsync(); _log.Add(UiLogLevel.Success, $"负载预设已保存:{existing.Name}。"); } private bool CanDeleteLoadPreset() => SelectedLoadPreset != null; [RelayCommand(CanExecute = nameof(CanDeleteLoadPreset))] private async Task DeleteLoadPreset() { var preset = SelectedLoadPreset; if (preset == null) return; if (!Confirm($"确认删除负载预设:{preset.Name}?")) return; int index = LoadPresets.IndexOf(preset); LoadPresets.Remove(preset); SelectedLoadPreset = LoadPresets.Count == 0 ? null : LoadPresets[Math.Clamp(index, 0, LoadPresets.Count - 1)]; await PersistPresetsAsync(); _log.Add(UiLogLevel.Warning, $"负载预设已删除:{preset.Name}。"); } [RelayCommand] private async Task LoadInputOn() => await SetLoadInputAsync(true); [RelayCommand] private async Task LoadInputOff() => await SetLoadInputAsync(false); [RelayCommand] private async Task LoadSafeReset() { if (!Confirm("确认执行负载安全复位?")) return; await RunDeviceActionAsync("负载安全复位", async ct => { if (!await EnsureConnectedAsync(_load, "负载", ct)) return; await _load.InputAsync(false, ct); await _load.ResetAsync(ct); await _load.SetModeAsync(LoadMode.CC, ct); await _load.SetValueAsync(0, ct); await _load.InputAsync(false, ct); LoadCurrentSetpoint = 0; SelectedLoadPreset = null; _log.Add(UiLogLevel.Warning, "负载已安全复位:输入关闭,CC 电流设为 0。"); await RefreshLoadAsync(ct); }); } [RelayCommand] private async Task RelayAllOn() { if (!Confirm("确认全开 24 路通道?")) return; await RunDeviceActionAsync("继电器全开", async ct => { if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return; foreach (var row in Channels) await _sspc.TurnOnAsync(row.Channel, ct); _log.Add(UiLogLevel.Warning, "继电器 24 路通道已全开。"); await RefreshRelayAsync(ct); }); } [RelayCommand] private async Task RelayAllOff() { if (!Confirm("确认全关 24 路通道?")) return; await RunDeviceActionAsync("继电器全关", async ct => { if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return; foreach (var row in Channels) await _sspc.TurnOffAsync(row.Channel, ct); _log.Add(UiLogLevel.Warning, "继电器 24 路通道已全关。"); await RefreshRelayAsync(ct); }); } [RelayCommand] private async Task ReadRelayAll() { await RunDeviceActionAsync("读取继电器", async ct => { if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return; await RefreshRelayAsync(ct); }); } [RelayCommand] private async Task TurnRelayOn(ExperimentConsoleChannelRow? row) { if (row == null) return; await RunDeviceActionAsync($"CH{row.Channel:00} 开", async ct => { if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return; await _sspc.TurnOnAsync(row.Channel, ct); await RefreshRelayChannelAsync(row, ct); }); } [RelayCommand] private async Task TurnRelayOff(ExperimentConsoleChannelRow? row) { if (row == null) return; await RunDeviceActionAsync($"CH{row.Channel:00} 关", async ct => { if (!await EnsureConnectedAsync(_sspc, "继电器", ct)) return; await _sspc.TurnOffAsync(row.Channel, ct); await RefreshRelayChannelAsync(row, ct); }); } private async Task SetPowerOutputAsync(bool on) { await RunDeviceActionAsync(on ? "电源输出开" : "电源输出关", async ct => { if (!await EnsureConnectedAsync(_power, "电源", ct)) return; await _power.OutputAsync(on, ct); _log.Add(UiLogLevel.Success, on ? "电源输出已开启。" : "电源输出已关闭。"); await RefreshPowerAsync(ct); }); } private async Task SetLoadInputAsync(bool on) { await RunDeviceActionAsync(on ? "负载输入开" : "负载输入关", async ct => { if (!await EnsureConnectedAsync(_load, "负载", ct)) return; await _load.InputAsync(on, ct); _log.Add(UiLogLevel.Success, on ? "负载输入已开启。" : "负载输入已关闭。"); await RefreshLoadAsync(ct); }); } private void StartPolling() { lock (_pollLock) { if (_pollTask is { IsCompleted: false }) return; _pollCts = new CancellationTokenSource(); _pollTask = PollAsync(_pollCts.Token); } } private void StopPolling() { Task? taskToObserve; CancellationTokenSource? ctsToDispose; lock (_pollLock) { taskToObserve = _pollTask; ctsToDispose = _pollCts; _pollTask = null; _pollCts = null; ctsToDispose?.Cancel(); } if (taskToObserve != null) _ = taskToObserve.ContinueWith(_ => ctsToDispose?.Dispose(), TaskScheduler.Default); else ctsToDispose?.Dispose(); } private async Task PollAsync(CancellationToken ct) { try { while (!ct.IsCancellationRequested) { await RefreshAllConnectedAsync(ct).ConfigureAwait(false); await Task.Delay(TimeSpan.FromSeconds(1), ct).ConfigureAwait(false); } } catch (OperationCanceledException) when (ct.IsCancellationRequested) { } } private async Task RefreshAllConnectedAsync(CancellationToken ct) { if (_power.IsConnected) await RefreshPowerAsync(ct).ConfigureAwait(false); else await RunOnUiAsync(() => { IsPowerOutputOn = null; PowerOutputStateText = "未连接"; PowerProtectionStateText = "未连接"; }).ConfigureAwait(false); if (_load.IsConnected) await RefreshLoadAsync(ct).ConfigureAwait(false); else await RunOnUiAsync(() => { IsLoadInputOn = null; LoadInputStateText = "未连接"; LoadProtectionStateText = "未连接"; }).ConfigureAwait(false); if (_sspc.IsConnected) await RefreshRelayAsync(ct).ConfigureAwait(false); } private async Task RefreshPowerAsync(CancellationToken ct) { try { var (volts, amps) = await _power.ReadAsync(ct).ConfigureAwait(false); bool outputOn = await _power.GetOutputStateAsync(ct).ConfigureAwait(false); string errorState = await QueryErrorStateAsync(_power, ct).ConfigureAwait(false); await RunOnUiAsync(() => { PowerVoltage = volts; PowerCurrent = Math.Abs(amps); PowerWatts = Math.Abs(volts * amps); PowerModeText = InferPowerMode(Math.Abs(amps)); IsPowerOutputOn = outputOn; PowerOutputStateText = outputOn ? "ON" : "OFF"; PowerProtectionStateText = errorState; }).ConfigureAwait(false); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { } catch (Exception ex) { await RunOnUiAsync(() => { IsPowerOutputOn = null; PowerOutputStateText = _power.IsConnected ? "未知" : "未连接"; PowerProtectionStateText = _power.IsConnected ? "通讯异常" : "未连接"; }).ConfigureAwait(false); _log.Add(UiLogLevel.Error, $"电源读数刷新失败:{ex.Message}"); } } private async Task RefreshLoadAsync(CancellationToken ct) { try { var reading = await _load.ReadPowerAsync(ct).ConfigureAwait(false); bool inputOn = await _load.GetInputStateAsync(ct).ConfigureAwait(false); string errorState = await QueryErrorStateAsync(_load, ct).ConfigureAwait(false); await RunOnUiAsync(() => { LoadVoltage = reading.Volts; LoadCurrent = Math.Abs(reading.Amps); LoadWatts = Math.Abs(reading.Watts); IsLoadInputOn = inputOn; LoadInputStateText = inputOn ? "ON" : "OFF"; LoadProtectionStateText = errorState; }).ConfigureAwait(false); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { } catch (Exception ex) { await RunOnUiAsync(() => { IsLoadInputOn = null; LoadInputStateText = _load.IsConnected ? "未知" : "未连接"; LoadProtectionStateText = _load.IsConnected ? "通讯异常" : "未连接"; }).ConfigureAwait(false); _log.Add(UiLogLevel.Error, $"负载读数刷新失败:{ex.Message}"); } } private async Task RefreshRelayAsync(CancellationToken ct) { try { var measurements = await _sspc.ReadMeasurementsAsync(ct).ConfigureAwait(false); await RunOnUiAsync(() => { foreach (var measurement in measurements) { var row = Channels.FirstOrDefault(x => x.Channel == measurement.PhysicalSlot); row?.Apply(measurement.Voltage, Math.Abs(measurement.Current)); } }).ConfigureAwait(false); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { } catch (Exception ex) { _log.Add(UiLogLevel.Error, $"继电器读数刷新失败:{ex.Message}"); } } private async Task RefreshRelayChannelAsync(ExperimentConsoleChannelRow row, CancellationToken ct) { try { var measurement = await _sspc.ReadMeasurementAsync(row.Channel, ct).ConfigureAwait(false); await RunOnUiAsync(() => row.Apply(measurement.Voltage, Math.Abs(measurement.Current))).ConfigureAwait(false); } catch (OperationCanceledException) when (ct.IsCancellationRequested) { } catch (Exception ex) { _log.Add(UiLogLevel.Error, $"CH{row.Channel:00} 读数刷新失败:{ex.Message}"); } } private async Task RunDeviceActionAsync(string actionName, Func action) { if (Busy) return; Busy = true; using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10)); try { await action(cts.Token).ConfigureAwait(false); } catch (OperationCanceledException) { _log.Add(UiLogLevel.Error, $"{actionName} 超时或已取消。"); } catch (Exception ex) { _log.Add(UiLogLevel.Error, $"{actionName} 失败:{ex.Message}"); } finally { await RunOnUiAsync(() => { Busy = false; RaiseConnectionStatus(); }).ConfigureAwait(false); } } private async Task EnsureConnectedAsync(IDeviceConnection device, string name, CancellationToken ct) { if (device.IsConnected) return true; bool connected = await device.ConnectAsync(ct).ConfigureAwait(false); await RunOnUiAsync(RaiseConnectionStatus).ConfigureAwait(false); if (connected && device.IsConnected) return true; _log.Add(UiLogLevel.Warning, $"{name} 未连接,操作未执行。"); return false; } private string InferPowerMode(double amps) { if (!IsValidNonNegative(PowerCurrentLimit) || PowerCurrentLimit <= 0 || double.IsNaN(amps)) return "未知"; return amps >= PowerCurrentLimit * 0.95 ? "CC" : "CV"; } private static async Task QueryErrorStateAsync(IPowerSupply power, CancellationToken ct) { if (!power.IsConnected) return "未连接"; return FormatErrorState(await power.QueryErrorAsync(ct).ConfigureAwait(false)); } private static async Task QueryErrorStateAsync(ILoad load, CancellationToken ct) { if (!load.IsConnected) return "未连接"; return FormatErrorState(await load.QueryErrorAsync(ct).ConfigureAwait(false)); } private static string FormatErrorState(string? response) { string text = (response ?? "").Trim(); if (string.IsNullOrWhiteSpace(text)) return "正常"; string lower = text.ToLowerInvariant(); string firstPart = text.Split(',', 2)[0].Trim().Trim('"'); bool zeroCode = int.TryParse(firstPart.TrimStart('+'), out int code) && code == 0; if (zeroCode || lower.Contains("no error") || lower.Contains("noerror")) return "正常"; return text.Length > 64 ? $"异常:{text[..64]}..." : $"异常:{text}"; } private bool ValidateNonNegative(double value, string name) { if (IsValidNonNegative(value)) return true; _log.Add(UiLogLevel.Error, $"{name} 必须是有限非负数。"); return false; } private static bool IsValidNonNegative(double value) => double.IsFinite(value) && value >= 0; private async Task PersistPresetsAsync() { _options.PowerPresets = PowerPresets.Select(Clone).ToList(); _options.LoadPresets = LoadPresets.Select(Clone).ToList(); await _settingsStore.SaveAsync().ConfigureAwait(false); } private static PowerPreset Clone(PowerPreset source) => new() { Name = source.Name, Voltage = source.Voltage, CurrentLimit = source.CurrentLimit }; private static LoadPreset Clone(LoadPreset source) => new() { Name = source.Name, Current = source.Current }; private static bool Confirm(string message) => MessageBox.Show(message, "确认操作", MessageBoxButton.YesNo, MessageBoxImage.Warning) == MessageBoxResult.Yes; private void RaiseConnectionStatus() { OnPropertyChanged(nameof(PowerConnectionStatus)); OnPropertyChanged(nameof(RelayConnectionStatus)); OnPropertyChanged(nameof(LoadConnectionStatus)); OnPropertyChanged(nameof(IsPowerConnected)); OnPropertyChanged(nameof(IsRelayConnected)); OnPropertyChanged(nameof(IsLoadConnected)); } private static Task RunOnUiAsync(Action action) { var dispatcher = Application.Current?.Dispatcher; if (dispatcher == null || dispatcher.CheckAccess()) { action(); return Task.CompletedTask; } return dispatcher.InvokeAsync(action).Task; } } public partial class ExperimentConsoleChannelRow : ObservableObject { public ExperimentConsoleChannelRow(int channel) { Channel = channel; } public int Channel { get; } public string ChannelText => $"CH{Channel:00}"; [ObservableProperty] private double _voltage = double.NaN; [ObservableProperty] private double _current = double.NaN; public void Apply(double voltage, double current) { Voltage = voltage; Current = current; } }