SSPC-Tester/SSPCTester.UI/ViewModels/ExperimentConsoleViewModel.cs
2026-07-12 12:07:41 +08:00

680 lines
24 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 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<PowerPreset> PowerPresets { get; } = new();
public ObservableCollection<LoadPreset> LoadPresets { get; } = new();
public ObservableCollection<ExperimentConsoleChannelRow> Channels { get; } = new();
public ExperimentConsoleViewModel(
IPowerSupply power,
ISspc sspc,
ILoad load,
IOptions<ConsoleOptions> 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(bool active)
{
if (active)
StartPolling();
else
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<CancellationToken, Task> 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<bool> 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<string> QueryErrorStateAsync(IPowerSupply power, CancellationToken ct)
{
if (!power.IsConnected) return "未连接";
return FormatErrorState(await power.QueryErrorAsync(ct).ConfigureAwait(false));
}
private static async Task<string> 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;
}
}