2026-07-12 12:07:41 +08:00
|
|
|
|
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();
|
2026-07-13 03:51:13 +08:00
|
|
|
|
private readonly HashSet<object> _activeHosts = new(ReferenceEqualityComparer.Instance);
|
2026-07-12 12:07:41 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-13 03:51:13 +08:00
|
|
|
|
public void SetActive(object host, bool active)
|
2026-07-12 12:07:41 +08:00
|
|
|
|
{
|
2026-07-13 03:51:13 +08:00
|
|
|
|
ArgumentNullException.ThrowIfNull(host);
|
|
|
|
|
|
|
|
|
|
|
|
lock (_pollLock)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (active)
|
|
|
|
|
|
{
|
|
|
|
|
|
_activeHosts.Add(host);
|
|
|
|
|
|
StartPolling();
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
_activeHosts.Remove(host);
|
|
|
|
|
|
if (_activeHosts.Count == 0)
|
|
|
|
|
|
StopPolling();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-07-12 12:07:41 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
[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;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|