源适应性界面

This commit is contained in:
“hsc”
2026-09-22 11:33:07 +08:00
parent ac26ab255c
commit 3275283263
15 changed files with 1219 additions and 29 deletions
+7
View File
@@ -45,6 +45,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ACP", "ACP\ACP.csproj", "{C
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TSMasterCAN", "TSMasterCAN\TSMasterCAN.csproj", "{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TSMasterCAN", "TSMasterCAN\TSMasterCAN.csproj", "{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PowerAdaptabilityModule", "PowerAdaptabilityModule\PowerAdaptabilityModule.csproj", "{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU
@@ -131,6 +133,10 @@ Global
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Debug|Any CPU.Build.0 = Debug|Any CPU {B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Debug|Any CPU.Build.0 = Debug|Any CPU
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Release|Any CPU.ActiveCfg = Release|Any CPU {B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Release|Any CPU.ActiveCfg = Release|Any CPU
{B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Release|Any CPU.Build.0 = Release|Any CPU {B45DC94C-2CBB-4119-8787-B52A9E7EC58B}.Release|Any CPU.Build.0 = Release|Any CPU
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Debug|Any CPU.Build.0 = Debug|Any CPU
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Release|Any CPU.ActiveCfg = Release|Any CPU
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -146,5 +152,6 @@ Global
{AF2533DD-599D-49D7-942B-B0C26534D15F} = {1E92D601-68FC-45F6-8251-BD0F39226E5B} {AF2533DD-599D-49D7-942B-B0C26534D15F} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{84006890-9288-44C4-9D17-FA88F3FDECFA} = {1E92D601-68FC-45F6-8251-BD0F39226E5B} {84006890-9288-44C4-9D17-FA88F3FDECFA} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{8923122F-F7D3-4AE7-9E86-9E78F1296A11} = {1E92D601-68FC-45F6-8251-BD0F39226E5B} {8923122F-F7D3-4AE7-9E86-9E78F1296A11} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
{CCA6AA94-6105-4C0B-BCA9-D0712B1A7B89} = {1E92D601-68FC-45F6-8251-BD0F39226E5B}
EndGlobalSection EndGlobalSection
EndGlobal EndGlobal
+1
View File
@@ -28,6 +28,7 @@
<ProjectReference Include="..\LoginModule\LoginModule.csproj" /> <ProjectReference Include="..\LoginModule\LoginModule.csproj" />
<ProjectReference Include="..\MainModule\MainModule.csproj" /> <ProjectReference Include="..\MainModule\MainModule.csproj" />
<ProjectReference Include="..\MonitorModule\MonitorModule.csproj" /> <ProjectReference Include="..\MonitorModule\MonitorModule.csproj" />
<ProjectReference Include="..\PowerAdaptabilityModule\PowerAdaptabilityModule.csproj" />
<ProjectReference Include="..\Service\Service.csproj" /> <ProjectReference Include="..\Service\Service.csproj" />
<ProjectReference Include="..\SettingModule\SettingModule.csproj" /> <ProjectReference Include="..\SettingModule\SettingModule.csproj" />
<ProjectReference Include="..\TestingModule\TestingModule.csproj" /> <ProjectReference Include="..\TestingModule\TestingModule.csproj" />
+4
View File
@@ -902,6 +902,10 @@ namespace ACP.ViewModels
if (_globalInfo.CurrentScope != "default") return; if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "ExportView"); _regionManager.RequestNavigate("ShellViewManager", "ExportView");
break; break;
case "电源适应性测试":
if (_globalInfo.CurrentScope != "default") return;
_regionManager.RequestNavigate("ShellViewManager", "PowerAdaptabilityView");
break;
} }
} }
+5
View File
@@ -98,6 +98,11 @@
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}" CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}" Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" /> Margin="8" />
<Button Content="电源适应性测试"
Command="{Binding NavigateCommand}"
CommandParameter="{Binding Content, RelativeSource={RelativeSource Self}}"
Style="{StaticResource MaterialDesignFlatButton}"
Margin="8" />
</StackPanel> </StackPanel>
</materialDesign:DrawerHost.LeftDrawerContent> </materialDesign:DrawerHost.LeftDrawerContent>
<Grid> <Grid>
+16
View File
@@ -35,6 +35,7 @@ namespace DeviceCommand.Devices
/// <param name="data">开关状态(true: 吸合/开启, false: 断开/关闭)</param> /// <param name="data">开关状态(true: 吸合/开启, false: 断开/关闭)</param>
public async Task 写输出开关(byte 站号, ushort 开始地址, bool data) public async Task 写输出开关(byte 站号, ushort 开始地址, bool data)
{ {
确保已连接();
await Modbus.WriteSingleCoilAsync(站号, 开始地址, data); await Modbus.WriteSingleCoilAsync(站号, 开始地址, data);
} }
@@ -47,6 +48,7 @@ namespace DeviceCommand.Devices
/// <param name="datas">要写入的开关状态数组,按顺序从开始地址写入</param> /// <param name="datas">要写入的开关状态数组,按顺序从开始地址写入</param>
public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas) public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas)
{ {
确保已连接();
var 批量写入 = await Modbus.ReadCoilsAsync(1, 0, 16); var 批量写入 = await Modbus.ReadCoilsAsync(1, 0, 16);
for (int i = 0; i < datas.Length; i++) for (int i = 0; i < datas.Length; i++)
{ {
@@ -65,6 +67,7 @@ namespace DeviceCommand.Devices
/// <param name="开始地址">线圈地址(0 起始)</param> /// <param name="开始地址">线圈地址(0 起始)</param>
public async Task 读输出开关(byte 站号, ushort 开始地址) public async Task 读输出开关(byte 站号, ushort 开始地址)
{ {
确保已连接();
await Modbus.ReadCoilsAsync(站号, 开始地址, 1); await Modbus.ReadCoilsAsync(站号, 开始地址, 1);
} }
@@ -77,7 +80,20 @@ namespace DeviceCommand.Devices
/// <returns>线圈状态数组,true 表示吸合/开启</returns> /// <returns>线圈状态数组,true 表示吸合/开启</returns>
public async Task<bool[]> 批量读输出开关(byte 站号, ushort 开始地址,ushort 数量) public async Task<bool[]> 批量读输出开关(byte 站号, ushort 开始地址,ushort 数量)
{ {
确保已连接();
return await Modbus.ReadCoilsAsync(站号, 开始地址, 数量); return await Modbus.ReadCoilsAsync(站号, 开始地址, 数量);
} }
/// <summary>
/// 确认 Modbus 主站已建立(设备连接成功)。
/// 未连接时抛出带明确提示的异常,替代对 null 主站的 NullReferenceException,
/// 供上层(测试步骤 / 报警蜂鸣器)捕获并记录。
/// </summary>
/// <exception cref="InvalidOperationException">Modbus 主站尚未建立时抛出</exception>
private void 确保已连接()
{
if (Modbus == null)
throw new InvalidOperationException("IO 板卡尚未连接(Modbus 主站未建立),无法执行输出控制操作,请确认设备连接状态。");
}
} }
} }
@@ -24,6 +24,7 @@ namespace MainModule.ViewModels
private bool IsInitialized =false; private bool IsInitialized =false;
private bool _isRunningErrorSteps = false; private bool _isRunningErrorSteps = false;
private SubscriptionToken? _alarmToken; private SubscriptionToken? _alarmToken;
private SubscriptionToken? _silenceToken;
#endregion #endregion
#region 属性 #region 属性
@@ -87,7 +88,7 @@ namespace MainModule.ViewModels
RefreshCommand = new DelegateCommand(OnRefresh); RefreshCommand = new DelegateCommand(OnRefresh);
BackToProtocolCommand = new DelegateCommand(OnBackToProtocol); BackToProtocolCommand = new DelegateCommand(OnBackToProtocol);
LoadedCommand = new AsyncDelegateCommand(OnLoad); LoadedCommand = new AsyncDelegateCommand(OnLoad);
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Subscribe(async (scope) => await SilenceBuzzer(scope)); _silenceToken = _eventAggregator.GetEvent<SilenceBuzzerEvent>().Subscribe(async (scope) => await SilenceBuzzer(scope));
_alarmToken = _eventAggregator.GetEvent<AlarmEvent>().Subscribe(OnAlarmTriggered); _alarmToken = _eventAggregator.GetEvent<AlarmEvent>().Subscribe(OnAlarmTriggered);
} }
@@ -95,21 +96,29 @@ namespace MainModule.ViewModels
{ {
if (_deviceManager.IOGroup == null) return; if (_deviceManager.IOGroup == null) return;
if (scope == "default") try
{ {
for(int i = 1; i <= 8; i++) if (scope == "default")
{ {
await _deviceManager.IOGroup.执行台架动作Async(i, 功能动作.蜂鸣器报警, false); for(int i = 1; i <= 8; i++)
{
await _deviceManager.IOGroup.执行台架动作Async(i, 功能动作.蜂鸣器报警, false);
}
}
else if (scope == TestStatus)
{
int 台架号 = _systemConfig.SharedParameterList
.FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0;
if (台架号 >= 1 && 台架号 <= 8)
{
await _deviceManager.IOGroup.执行台架动作Async(台架号, 功能动作.蜂鸣器报警, false);
}
} }
} }
else if (scope == TestStatus) catch (Exception ex)
{ {
int 台架号 = _systemConfig.SharedParameterList // 消音为尽力而为动作:IO 板卡未连接或通信失败时仅记录日志,避免切换页面/点击消音时崩溃
.FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0; LoggerHelper.Error($"[{scope}] 蜂鸣器消音失败:{ex.Message}");
if (台架号 >= 1 && 台架号 <= 8)
{
await _deviceManager.IOGroup.执行台架动作Async(台架号, 功能动作.蜂鸣器报警, false);
}
} }
} }
@@ -127,9 +136,17 @@ namespace MainModule.ViewModels
.FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0; .FirstOrDefault(x => x.ParameterName == "台架序号")?.Value ?? 0;
if (台架号 < 1 || 台架号 > 8) return; if (台架号 < 1 || 台架号 > 8) return;
// 上下限 / 上下极限 均触发蜂鸣器报警 try
await _deviceManager.IOGroup.执行台架动作Async(台架号, 功能动作.蜂鸣器报警, true); {
LoggerHelper.WarnWithNotify(args.Scope, $"[{args.Scope}] 报警触发: {args.Status} ({args.Fingerprint}),蜂鸣器已开启。"); // 上下限 / 上下极限 均触发蜂鸣器报警
await _deviceManager.IOGroup.执行台架动作Async(台架号, 功能动作.蜂鸣器报警, true);
LoggerHelper.WarnWithNotify(args.Scope, $"[{args.Scope}] 报警触发: {args.Status} ({args.Fingerprint}),蜂鸣器已开启。");
}
catch (Exception ex)
{
// async void 内绝不能抛出异常:蜂鸣器为尽力而为动作,IO 板卡未连接/通信失败时仅记录日志,避免程序崩溃
LoggerHelper.Error($"[{args.Scope}] 蜂鸣器报警开启失败:{ex.Message}");
}
// 上下极限额外触发异常流程 // 上下极限额外触发异常流程
if (args.Status == "超上极限" || args.Status == "超下极限") if (args.Status == "超上极限" || args.Status == "超下极限")
@@ -205,7 +222,8 @@ namespace MainModule.ViewModels
{ {
if (_alarmToken != null) if (_alarmToken != null)
_eventAggregator.GetEvent<AlarmEvent>().Unsubscribe(_alarmToken); _eventAggregator.GetEvent<AlarmEvent>().Unsubscribe(_alarmToken);
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Unsubscribe(async (scope) => await SilenceBuzzer(scope)); if (_silenceToken != null)
_eventAggregator.GetEvent<SilenceBuzzerEvent>().Unsubscribe(_silenceToken);
// 2. 显式释放硬件资源(防止端口占用/死锁) // 2. 显式释放硬件资源(防止端口占用/死锁)
if (_deviceManager is IDisposable disposableDevice) if (_deviceManager is IDisposable disposableDevice)
{ {
@@ -157,7 +157,9 @@ namespace MonitorModule.ViewModels.Dialogs
.Where(m => .Where(m =>
{ {
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false; if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false; // 同时支持 Task<string> 和 Task<double> 返回类型
var rt = m.ReturnType;
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
var parms = m.GetParameters(); var parms = m.GetParameters();
return parms.Length == 0 || return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
+3 -1
View File
@@ -438,7 +438,9 @@ namespace MonitorModule.ViewModels
.Where(m => .Where(m =>
{ {
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false; if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false; // 同时支持 Task<string> 和 Task<double> 返回类型
var rt = m.ReturnType;
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
var parms = m.GetParameters(); var parms = m.GetParameters();
return parms.Length == 0 || return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
+3 -1
View File
@@ -233,7 +233,9 @@ namespace MonitorModule.ViewModels
.Where(m => .Where(m =>
{ {
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false; if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false; // 同时支持 Task<string> 和 Task<double> 返回类型
var rt = m.ReturnType;
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
var parms = m.GetParameters(); var parms = m.GetParameters();
return parms.Length == 0 || return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
@@ -0,0 +1,20 @@
using PowerAdaptabilityModule.Views;
namespace PowerAdaptabilityModule
{
/// <summary>
/// 电源适应性测试模块:Chroma61800 交流源三相输出控制、LIST 模式设置与三相数据实时波形展示。
/// 通过左侧菜单“电源适应性测试”导航进入(仅 default 作用域可用)。
/// </summary>
public class PowerAdaptabilityModule : IModule
{
public void OnInitialized(IContainerProvider containerProvider)
{
}
public void RegisterTypes(IContainerRegistry containerRegistry)
{
containerRegistry.RegisterForNavigation<PowerAdaptabilityView>("PowerAdaptabilityView");
}
}
}
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<Nullable>enable</Nullable>
<UseWPF>true</UseWPF>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\UIShare\UIShare.csproj" />
<ProjectReference Include="..\DeviceCommand\DeviceCommand.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="OxyPlot.Wpf" Version="2.2.0" />
</ItemGroup>
</Project>
@@ -0,0 +1,637 @@
using DeviceCommand.Devices;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Legends;
using OxyPlot.Series;
using Prism.Commands;
using Prism.Ioc;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Input;
using UIShare.GlobalVariable;
using UIShare.ViewModelBase;
namespace PowerAdaptabilityModule.ViewModels
{
/// <summary>
/// 电源适应性测试 ViewModel(Chroma61800 交流源一拖三):
/// 左侧为设备命令面板(三相输出设置 + LIST 模式),右侧为 A/B/C 三相实时波形图表。
/// </summary>
public class PowerAdaptabilityViewModel : NavigateViewModelBase, IDisposable
{
#region 常量
/// <summary>采样周期(毫秒)</summary>
private const int SampleIntervalMs = 1000;
/// <summary>合成正弦波的分段数(0~360°,每 2° 一个点,共 181 点)</summary>
private const int SinePoints = 180;
/// <summary>三相相位偏移(°):A=0,B=-120,C=+120</summary>
private static readonly double[] PhaseOffsetDeg = { 0, -120, 120 };
/// <summary>三相曲线配色:A 红、B 绿、C 蓝(三张图统一,便于对照)</summary>
private static readonly OxyColor[] PhaseColors =
{ OxyColor.FromRgb(0xE5, 0x39, 0x35), OxyColor.FromRgb(0x43, 0xA0, 0x47), OxyColor.FromRgb(0x1E, 0x88, 0xE5) };
#endregion
#region 私有字段
private readonly DeviceManager _dm;
private readonly GlobalInfo _globalInfo;
private Chroma61800? _dev;
private CancellationTokenSource? _cmdCts;
private CancellationTokenSource? _sampleCts;
// 三相正弦曲线:按物理量分组(电压 / 电流 / 功率),每组含 A/B/C 三相
private readonly LineSeries _voltA, _voltB, _voltC;
private readonly LineSeries _currA, _currB, _currC;
private readonly LineSeries _powA, _powB, _powC;
#endregion
#region 设备状态属性
private string _deviceName = "Chroma61800";
public string DeviceName { get => _deviceName; set => SetProperty(ref _deviceName, value); }
private bool _isConnected;
public bool IsConnected { get => _isConnected; set => SetProperty(ref _isConnected, value); }
private bool _isBusy;
public bool IsBusy { get => _isBusy; set => SetProperty(ref _isBusy, value); }
private bool _isSampling;
/// <summary>是否正在采集波形数据</summary>
public bool IsSampling { get => _isSampling; set => SetProperty(ref _isSampling, value); }
#endregion
#region 三相图表(按物理量分图:电压 / 电流 / 功率,每图叠加 A/B/C 三相正弦)
/// <summary>三相电压波形图表</summary>
public PlotModel VoltagePlot { get; }
/// <summary>三相电流波形图表</summary>
public PlotModel CurrentPlot { get; }
/// <summary>三相功率波形图表</summary>
public PlotModel PowerPlot { get; }
#endregion
#region 实时测量值
private double _voltageA; public double VoltageA { get => _voltageA; set => SetProperty(ref _voltageA, value); }
private double _currentA; public double CurrentA { get => _currentA; set => SetProperty(ref _currentA, value); }
private double _voltageB; public double VoltageB { get => _voltageB; set => SetProperty(ref _voltageB, value); }
private double _currentB; public double CurrentB { get => _currentB; set => SetProperty(ref _currentB, value); }
private double _voltageC; public double VoltageC { get => _voltageC; set => SetProperty(ref _voltageC, value); }
private double _currentC; public double CurrentC { get => _currentC; set => SetProperty(ref _currentC, value); }
private double _frequencyNow;
/// <summary>当前输出频率 (Hz)</summary>
public double FrequencyNow { get => _frequencyNow; set => SetProperty(ref _frequencyNow, value); }
private double _powerNow;
/// <summary>当前真实输出功率 (W)</summary>
public double PowerNow { get => _powerNow; set => SetProperty(ref _powerNow, value); }
#endregion
#region 输入参数(三相输出设置)
private double _voltage = 220;
/// <summary>待设置的交流电压 (V)</summary>
public double Voltage { get => _voltage; set => SetProperty(ref _voltage, value); }
private double _frequency = 50;
/// <summary>待设置的输出频率 (Hz)</summary>
public double Frequency { get => _frequency; set => SetProperty(ref _frequency, value); }
private string _waveform = "SINE";
/// <summary>波形选择</summary>
public string Waveform { get => _waveform; set => SetProperty(ref _waveform, value); }
private string _couplingMode = "AC";
/// <summary>输出耦合模式 (AC / DC / ACDC)</summary>
public string CouplingMode { get => _couplingMode; set => SetProperty(ref _couplingMode, value); }
private string _threePhaseRelation = "BALANCE";
/// <summary>三相关系 (INDEPEND / SAMEFREQ / BALANCE)</summary>
public string ThreePhaseRelation { get => _threePhaseRelation; set => SetProperty(ref _threePhaseRelation, value); }
#endregion
#region 输入参数(LIST 模式)
private string _listCoupling = "NONE";
/// <summary>列表耦合模态 (ALL / NONE)</summary>
public string ListCoupling { get => _listCoupling; set => SetProperty(ref _listCoupling, value); }
private string _listTrigger = "AUTO";
/// <summary>列表触发形态 (AUTO / MANUAL / EXCITE)</summary>
public string ListTrigger { get => _listTrigger; set => SetProperty(ref _listTrigger, value); }
private int _listCount = 1;
/// <summary>列表执行次数 (0 ~ 65535)</summary>
public int ListCount { get => _listCount; set => SetProperty(ref _listCount, value); }
private string _listTimeBase = "TIME";
/// <summary>列表时间基础 (TIME / CYCLE)</summary>
public string ListTimeBase { get => _listTimeBase; set => SetProperty(ref _listTimeBase, value); }
private string _listVoltAcStartText = "";
/// <summary>AC 起始电压序列(逗号分隔,如 0,50,100,150)</summary>
public string ListVoltAcStartText { get => _listVoltAcStartText; set => SetProperty(ref _listVoltAcStartText, value); }
private string _listVoltAcEndText = "";
/// <summary>AC 结束电压序列(逗号分隔)</summary>
public string ListVoltAcEndText { get => _listVoltAcEndText; set => SetProperty(ref _listVoltAcEndText, value); }
private string _listFreqStartText = "";
/// <summary>起始频率序列(逗号分隔,如 50,60,50)</summary>
public string ListFreqStartText { get => _listFreqStartText; set => SetProperty(ref _listFreqStartText, value); }
private string _listFreqEndText = "";
/// <summary>结束频率序列(逗号分隔)</summary>
public string ListFreqEndText { get => _listFreqEndText; set => SetProperty(ref _listFreqEndText, value); }
private string _listDwellText = "";
/// <summary>静止时间序列(ms,逗号分隔,如 500,500,500)</summary>
public string ListDwellText { get => _listDwellText; set => SetProperty(ref _listDwellText, value); }
private string _listDegrText = "";
/// <summary>相位角度序列(°,逗号分隔)</summary>
public string ListDegrText { get => _listDegrText; set => SetProperty(ref _listDegrText, value); }
private string _listShapeText = "";
/// <summary>波形缓冲顺序(A/B,逗号分隔,如 A,B,A)</summary>
public string ListShapeText { get => _listShapeText; set => SetProperty(ref _listShapeText, value); }
#endregion
#region 下拉选项
public IReadOnlyList<string> WaveformOptions { get; } = new[] { "SINE", "SQUA", "CSIN" };
public IReadOnlyList<string> CouplingOptions { get; } = new[] { "AC", "DC", "ACDC" };
public IReadOnlyList<string> RelationOptions { get; } = new[] { "INDEPEND", "SAMEFREQ", "BALANCE" };
public IReadOnlyList<string> ListCouplingOptions { get; } = new[] { "ALL", "NONE" };
public IReadOnlyList<string> ListTriggerOptions { get; } = new[] { "AUTO", "MANUAL", "EXCITE" };
public IReadOnlyList<string> ListTimeBaseOptions { get; } = new[] { "TIME", "CYCLE" };
#endregion
#region 日志
private string _responseLog = "";
/// <summary>操作与采样日志</summary>
public string ResponseLog { get => _responseLog; set => SetProperty(ref _responseLog, value); }
#endregion
#region 命令
public ICommand QueryIdnCommand { get; }
public ICommand ResetCommand { get; }
public ICommand OutputOnCommand { get; }
public ICommand OutputOffCommand { get; }
public ICommand RelayOnCommand { get; }
public ICommand RelayOffCommand { get; }
public ICommand ApplyCouplingCommand { get; }
public ICommand SetThreePhaseModeCommand { get; }
public ICommand SetSinglePhaseModeCommand { get; }
public ICommand ApplyRelationCommand { get; }
public ICommand SetVoltageCommand { get; }
public ICommand SetFrequencyCommand { get; }
public ICommand SetWaveformCommand { get; }
public ICommand SetFixedModeCommand { get; }
public ICommand EnterListModeCommand { get; }
public ICommand ApplyListBasicCommand { get; }
public ICommand ApplyListSequencesCommand { get; }
public ICommand ListStartCommand { get; }
public ICommand ListStopCommand { get; }
public ICommand StartSamplingCommand { get; }
public ICommand StopSamplingCommand { get; }
public ICommand ClearChartsCommand { get; }
#endregion
public PowerAdaptabilityViewModel(IContainerProvider containerProvider) : base(containerProvider)
{
_dm = containerProvider.Resolve<DeviceManager>();
_globalInfo = containerProvider.Resolve<GlobalInfo>();
// 初始化三相图表(按物理量分图,每图叠加 A/B/C 三相正弦)
VoltagePlot = BuildSinePlot("三相电压", "V");
CurrentPlot = BuildSinePlot("三相电流", "A");
PowerPlot = BuildSinePlot("三相功率", "W");
(_voltA, _voltB, _voltC) = AddThreePhaseSeries(VoltagePlot);
(_currA, _currB, _currC) = AddThreePhaseSeries(CurrentPlot);
(_powA, _powB, _powC) = AddThreePhaseSeries(PowerPlot);
#region 命令初始化
QueryIdnCommand = new DelegateCommand(async () =>
await Exec(async () => Log("IDN: " + await _dev!.查询设备信息Async(Ct()))));
ResetCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.复位Async(Ct()); Log("设备已复位"); }));
OutputOnCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置输出开关Async(true, Ct()); Log("输出已开启"); }));
OutputOffCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置输出开关Async(false, Ct()); Log("输出已关闭"); }));
RelayOnCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置输出继电器Async(true, Ct()); Log("输出继电器已开启"); }));
RelayOffCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置输出继电器Async(false, Ct()); Log("输出继电器已关闭"); }));
ApplyCouplingCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置耦合模式Async(CouplingMode, Ct()); Log($"耦合模式={CouplingMode}"); }));
SetThreePhaseModeCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置单三相模式Async(true, Ct()); Log("已切换为三相模式"); }));
SetSinglePhaseModeCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置单三相模式Async(false, Ct()); Log("已切换为单相模式"); }));
ApplyRelationCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置三相关系Async(ThreePhaseRelation, Ct()); Log($"三相关系={ThreePhaseRelation}"); }));
SetVoltageCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置交流电压Async(Voltage, Ct()); Log($"交流电压={Voltage} V"); }));
SetFrequencyCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置频率Async(Frequency, Ct()); Log($"输出频率={Frequency} Hz"); }));
SetWaveformCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置波形Async(Waveform, Ct()); Log($"波形={Waveform}"); }));
SetFixedModeCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置操作模态Async("FIXED", Ct()); Log("已切换为 FIXED 固定模式"); }));
EnterListModeCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.设置操作模态Async("LIST", Ct()); Log("已切换为 LIST 模式"); }));
ApplyListBasicCommand = new DelegateCommand(async () => await Exec(async () =>
{
await _dev!.设置列表耦合模态Async(ListCoupling, Ct());
await _dev!.设置列表触发形态Async(ListTrigger, Ct());
await _dev!.设置列表执行次数Async(ListCount, Ct());
await _dev!.设置列表时间基础Async(ListTimeBase, Ct());
Log($"LIST 基础配置:耦合={ListCoupling} 触发={ListTrigger} 次数={ListCount} 时基={ListTimeBase}");
}));
ApplyListSequencesCommand = new DelegateCommand(async () => await Exec(async () =>
{
int applied = 0;
var vAcStart = ParseDoubles(ListVoltAcStartText);
if (vAcStart.Length > 0) { await _dev!.设置列表AC起始电压顺序Async(vAcStart, Ct()); applied++; }
var vAcEnd = ParseDoubles(ListVoltAcEndText);
if (vAcEnd.Length > 0) { await _dev!.设置列表AC结束电压顺序Async(vAcEnd, Ct()); applied++; }
var fStart = ParseDoubles(ListFreqStartText);
if (fStart.Length > 0) { await _dev!.设置列表起始频率顺序Async(fStart, Ct()); applied++; }
var fEnd = ParseDoubles(ListFreqEndText);
if (fEnd.Length > 0) { await _dev!.设置列表结束频率顺序Async(fEnd, Ct()); applied++; }
var dwell = ParseDoubles(ListDwellText);
if (dwell.Length > 0) { await _dev!.设置列表静止时间顺序Async(dwell, Ct()); applied++; }
var degr = ParseDoubles(ListDegrText);
if (degr.Length > 0) { await _dev!.设置列表相位角度顺序Async(degr, Ct()); applied++; }
var shapes = ParseShapes(ListShapeText);
if (shapes.Length > 0) { await _dev!.设置列表波形顺序Async(shapes, Ct()); applied++; }
Log(applied > 0 ? $"已下发 {applied} 组列表序列" : "没有可下发的列表序列(请先填写序列文本框)");
}));
ListStartCommand = new DelegateCommand(async () => await Exec(async () =>
{
await _dev!.设置操作模态Async("LIST", Ct());
await _dev!.触发列表执行Async(true, Ct());
Log("LIST 已开始执行");
}));
ListStopCommand = new DelegateCommand(async () =>
await Exec(async () => { await _dev!.触发列表执行Async(false, Ct()); Log("LIST 已停止执行"); }));
StartSamplingCommand = new DelegateCommand(StartSampling);
StopSamplingCommand = new DelegateCommand(StopSampling);
ClearChartsCommand = new DelegateCommand(ClearCharts);
#endregion
Initialize();
}
#region 初始化 / 导航
/// <summary>
/// 查找 Chroma61800 设备实例:优先当前作用域 DeviceManager,回退全局 HardwarePool(跨作用域共享实例)。
/// </summary>
private void Initialize()
{
Chroma61800? found = null;
string? fn = null;
foreach (var kv in _dm.DeviceMap)
{
if (kv.Value is Chroma61800 it) { found = it; fn = kv.Key; break; }
}
if (found == null)
{
foreach (var kv in _globalInfo.HardwarePool)
{
if (kv.Value.IsValueCreated && kv.Value.Value is Chroma61800 c) { found = c; fn = kv.Key; break; }
}
}
_dev = found;
DeviceName = fn ?? "Chroma61800 (未找到)";
IsConnected = _dev?.IsConnected ?? false;
Log(found != null
? $"已关联设备 [{DeviceName}],连接:{(IsConnected ? "已连接" : "未连接")}"
: "未找到 Chroma61800 设备,请先在设置界面添加并启用该设备");
}
public override void OnNavigatedTo(NavigationContext navigationContext)
{
Initialize();
}
public override void OnNavigatedFrom(NavigationContext navigationContext)
{
// 离开页面时停止采样,避免后台持续占用设备通信
StopSampling();
}
#endregion
#region 数据采集
private void StartSampling()
{
if (_dev == null) { Log("未关联 Chroma61800 设备,无法采集"); return; }
if (IsSampling) return;
IsSampling = true;
_sampleCts = new CancellationTokenSource();
var token = _sampleCts.Token;
_ = Task.Run(() => SamplingLoopAsync(token), token);
Log($"开始采集(周期 {SampleIntervalMs} ms)");
}
private void StopSampling()
{
if (!IsSampling) return;
IsSampling = false;
try { _sampleCts?.Cancel(); } catch { }
_sampleCts?.Dispose();
_sampleCts = null;
Log("停止采集");
}
/// <summary>
/// 采样循环:逐相读取交流电压/电流,合成波形;并刷新频率与功率实时值。
/// <para>说明:Chroma61800 类现有测量方法面向"活动相位",三相逐一调用读取;
/// 如设备需要按相切换后读取,可在每相读取前补充相位选择命令。</para>
/// </summary>
private async Task SamplingLoopAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
if (_dev != null && _dev.IsConnected)
{
// A 相
double vA = await _dev.读取交流电压Async(token);
double iA = await _dev.读取交流电流Async(token);
// B 相
double vB = await _dev.读取交流电压Async(token);
double iB = await _dev.读取交流电流Async(token);
// C 相
double vC = await _dev.读取交流电压Async(token);
double iC = await _dev.读取交流电流Async(token);
// 设备级:频率与功率
double freq = await _dev.读取频率Async(token);
double power = await _dev.读取真实功率Async(token);
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher != null)
{
_ = dispatcher.BeginInvoke(new Action(() =>
{
RegenerateWaveforms(vA, iA, vB, iB, vC, iC, freq, power);
IsConnected = _dev.IsConnected;
}));
}
}
}
catch (OperationCanceledException) { break; }
catch (Exception ex)
{
Log($"采样异常:{ex.Message}");
}
try { await Task.Delay(SampleIntervalMs, token); }
catch (OperationCanceledException) { break; }
}
}
/// <summary>
/// 根据实测 RMS 值合成三相正弦波并刷新三张图表(UI 线程调用)。
/// <para>横轴为相位角 0~360°,纵轴对称显示正负半周;电压/电流峰值 = √2 × RMS,
/// 三相相位互差 120°(A=0,B=-120,C=+120)。设备仅提供 RMS 标量,故为示意波形。</para>
/// </summary>
private void RegenerateWaveforms(double vA, double iA, double vB, double iB,
double vC, double iC, double freq, double power)
{
// 电压三相
RegenerateSine(_voltA, vA, PhaseOffsetDeg[0]);
RegenerateSine(_voltB, vB, PhaseOffsetDeg[1]);
RegenerateSine(_voltC, vC, PhaseOffsetDeg[2]);
// 电流三相
RegenerateSine(_currA, iA, PhaseOffsetDeg[0]);
RegenerateSine(_currB, iB, PhaseOffsetDeg[1]);
RegenerateSine(_currC, iC, PhaseOffsetDeg[2]);
// 功率三相(设备仅提供总有功,平衡三相每相 ≈ 总功率 / 3,按正弦示意呈现正负半周)
double pPhase = power / 3.0;
RegenerateSine(_powA, pPhase, PhaseOffsetDeg[0]);
RegenerateSine(_powB, pPhase, PhaseOffsetDeg[1]);
RegenerateSine(_powC, pPhase, PhaseOffsetDeg[2]);
// 实时数值
VoltageA = vA; CurrentA = iA;
VoltageB = vB; CurrentB = iB;
VoltageC = vC; CurrentC = iC;
FrequencyNow = freq; PowerNow = power;
// 纵轴对称范围(取该图三相峰值最大值并留 15% 余量,确保正负半周完整可见)
SetSymmetricAxis(VoltagePlot, vA, vB, vC);
SetSymmetricAxis(CurrentPlot, iA, iB, iC);
SetSymmetricAxis(PowerPlot, pPhase);
VoltagePlot.InvalidatePlot(true);
CurrentPlot.InvalidatePlot(true);
PowerPlot.InvalidatePlot(true);
}
/// <summary>按 RMS 幅值与相位偏移,生成 0~360° 一个完整周期的正弦曲线点(峰值 = √2 × RMS)。</summary>
private static void RegenerateSine(LineSeries series, double rms, double phaseDeg)
{
series.Points.Clear();
double peak = Math.Sqrt(2) * rms;
double phaseRad = phaseDeg * Math.PI / 180.0;
for (int i = 0; i <= SinePoints; i++)
{
double deg = 360.0 * i / SinePoints;
double rad = deg * Math.PI / 180.0 + phaseRad;
series.Points.Add(new DataPoint(deg, peak * Math.Sin(rad)));
}
}
/// <summary>设置纵轴对称范围(-peak ~ +peak);无输出时回退到 ±1 避免坐标轴退化。</summary>
private static void SetSymmetricAxis(PlotModel plot, params double[] rmsValues)
{
double maxRms = rmsValues.Max();
double peak = Math.Sqrt(2) * maxRms * 1.15;
if (peak <= 0) peak = 1;
if (plot.Axes.OfType<LinearAxis>().FirstOrDefault() is { } axis)
{
axis.Minimum = -peak;
axis.Maximum = peak;
}
}
private void ClearCharts()
{
foreach (var s in new[] { _voltA, _voltB, _voltC, _currA, _currB, _currC, _powA, _powB, _powC })
{
s.Points.Clear();
}
VoltagePlot.InvalidatePlot(true);
CurrentPlot.InvalidatePlot(true);
PowerPlot.InvalidatePlot(true);
Log("已清空波形数据");
}
#endregion
#region 图表构建
/// <summary>
/// 构建正弦波图表:横轴为相位角 0~360°,纵轴为对应物理量(对称显示正负半周),含图例。
/// </summary>
/// <param name="title">图表标题(如“三相电压”)</param>
/// <param name="unit">纵轴单位(V / A / W)</param>
private static PlotModel BuildSinePlot(string title, string unit)
{
var pm = new PlotModel
{
Title = $"{title} ({unit})",
PlotAreaBorderColor = OxyColors.LightGray,
Background = OxyColors.White
};
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Bottom,
Title = "相位角 (°)",
Minimum = 0,
Maximum = 360,
MajorStep = 45,
MinorStep = 15,
StringFormat = "0",
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Axes.Add(new LinearAxis
{
Position = AxisPosition.Left,
Title = unit,
Minimum = -1,
Maximum = 1,
MajorGridlineStyle = LineStyle.Dot,
MinorGridlineStyle = LineStyle.None
});
pm.Legends.Add(new Legend
{
LegendPosition = LegendPosition.RightTop,
LegendBackground = OxyColor.FromAColor(200, OxyColors.White),
LegendBorder = OxyColors.LightGray
});
return pm;
}
/// <summary>向图表添加 A/B/C 三相正弦曲线(红/绿/蓝,与三张图配色一致)。</summary>
private static (LineSeries A, LineSeries B, LineSeries C) AddThreePhaseSeries(PlotModel plot)
{
var a = NewPhaseSeries("A 相", PhaseColors[0]);
var b = NewPhaseSeries("B 相", PhaseColors[1]);
var c = NewPhaseSeries("C 相", PhaseColors[2]);
plot.Series.Add(a);
plot.Series.Add(b);
plot.Series.Add(c);
return (a, b, c);
}
/// <summary>创建一条相曲线(无标记点、平滑线条)。</summary>
private static LineSeries NewPhaseSeries(string title, OxyColor color) => new LineSeries
{
Title = title,
Color = color,
StrokeThickness = 1.8,
MarkerType = MarkerType.None
};
#endregion
#region 辅助
/// <summary>创建 10 秒超时的取消令牌(命令用)。</summary>
private CancellationToken Ct() => (_cmdCts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
/// <summary>统一的设备命令执行包装:防重入、异常捕获与日志。</summary>
private async Task Exec(Func<Task> action)
{
if (_dev == null) { Log("错误:未找到 Chroma61800 设备实例,请检查设备配置"); return; }
if (IsBusy) return;
IsBusy = true;
try
{
await action();
IsConnected = _dev.IsConnected;
}
catch (OperationCanceledException) { Log("命令超时或已取消"); }
catch (Exception ex) { Log($"错误:{ex.Message}"); }
finally { IsBusy = false; }
}
/// <summary>线程安全的日志输出(自动切回 UI 线程,保留最近内容)。</summary>
private void Log(string message)
{
var dispatcher = Application.Current?.Dispatcher;
if (dispatcher != null && !dispatcher.CheckAccess())
{
_ = dispatcher.BeginInvoke(new Action(() => Log(message)));
return;
}
var line = $"[{DateTime.Now:HH:mm:ss}] {message}";
ResponseLog = ResponseLog.Length > 6000
? line + "\n" + ResponseLog[..4000]
: line + "\n" + ResponseLog;
}
/// <summary>解析逗号/空格/分号分隔的数值序列。</summary>
private static double[] ParseDoubles(string text)
{
if (string.IsNullOrWhiteSpace(text)) return Array.Empty<double>();
return text.Split(new[] { ',', ',', ' ', '\t', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => double.TryParse(s.Trim(), out double v) ? (double?)v : null)
.Where(v => v.HasValue)
.Select(v => v!.Value)
.ToArray();
}
/// <summary>解析波形缓冲顺序(A/B)序列。</summary>
private static string[] ParseShapes(string text)
{
if (string.IsNullOrWhiteSpace(text)) return Array.Empty<string>();
return text.Split(new[] { ',', ',', ' ', '\t', ';' }, StringSplitOptions.RemoveEmptyEntries)
.Select(s => s.Trim().ToUpperInvariant())
.ToArray();
}
#endregion
public void Dispose()
{
StopSampling();
try { _cmdCts?.Cancel(); } catch { }
_cmdCts?.Dispose();
}
}
}
@@ -0,0 +1,410 @@
<UserControl x:Class="PowerAdaptabilityModule.Views.PowerAdaptabilityView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:i="http://schemas.microsoft.com/xaml/behaviors"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
xmlns:prism="http://prismlibrary.com/"
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
xmlns:oxy="http://oxyplot.org/wpf"
prism:ViewModelLocator.AutoWireViewModel="True"
d:DesignHeight="1080"
d:DesignWidth="1920">
<UserControl.Resources>
<converters:BooleanToVisibilityConverter x:Key="BoolToVis" />
</UserControl.Resources>
<Border Background="#F5F7FA">
<Grid Margin="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="440" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- ============ 左侧:Chroma 命令面板 ============ -->
<ScrollViewer Grid.Column="0"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Disabled"
Margin="0,0,8,0">
<StackPanel>
<!-- 设备状态卡片 -->
<materialDesign:Card Margin="0,0,0,8" Padding="12,8">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<materialDesign:PackIcon Kind="Flash"
Width="22" Height="22"
Foreground="#1565C0"
Margin="0,0,8,0"
VerticalAlignment="Center" />
<TextBlock Text="电源适应性测试 · Chroma61800"
FontSize="15" FontWeight="Bold"
VerticalAlignment="Center" />
<TextBlock Text="{Binding DeviceName, StringFormat=' [{0}]'}"
FontSize="13" Foreground="#757575"
VerticalAlignment="Center" Margin="4,0,0,0" />
</StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Border Width="10" Height="10" CornerRadius="5" Margin="0,0,6,0">
<Border.Style>
<Style TargetType="Border">
<Setter Property="Background" Value="#F44336" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Background" Value="#4CAF50" />
</DataTrigger>
</Style.Triggers>
</Style>
</Border.Style>
</Border>
<TextBlock VerticalAlignment="Center" FontSize="12">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="未连接" />
<Setter Property="Foreground" Value="#F44336" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsConnected}" Value="True">
<Setter Property="Text" Value="已连接" />
<Setter Property="Foreground" Value="#4CAF50" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
<ProgressBar IsIndeterminate="True" Width="60" Height="4"
Margin="12,0,0,0"
Visibility="{Binding IsBusy, Converter={StaticResource BoolToVis}}" />
</StackPanel>
</Grid>
</materialDesign:Card>
<!-- 三相输出控制 -->
<GroupBox Header="三相输出控制" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="开启输出" Command="{Binding OutputOnCommand}"
Style="{StaticResource CmdBtn}"
Background="#388E3C" Foreground="White" />
<Button Content="关闭输出" Command="{Binding OutputOffCommand}"
Style="{StaticResource WarnBtn}" />
<Button Content="复位" Command="{Binding ResetCommand}"
Style="{StaticResource CmdBtn}" />
<Button Content="查询 IDN" Command="{Binding QueryIdnCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="相模式" Style="{StaticResource ParamLabel}" />
<Button Content="三相" Command="{Binding SetThreePhaseModeCommand}"
Style="{StaticResource CmdBtn}" />
<Button Content="单相" Command="{Binding SetSinglePhaseModeCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="耦合模式" Style="{StaticResource ParamLabel}" />
<ComboBox Width="110" Height="32" FontSize="12" Margin="4,0"
ItemsSource="{Binding CouplingOptions}"
SelectedItem="{Binding CouplingMode}" />
<Button Content="应用" Command="{Binding ApplyCouplingCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="三相关系" Style="{StaticResource ParamLabel}" />
<ComboBox Width="110" Height="32" FontSize="12" Margin="4,0"
ItemsSource="{Binding RelationOptions}"
SelectedItem="{Binding ThreePhaseRelation}" />
<Button Content="应用" Command="{Binding ApplyRelationCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="输出继电器" Style="{StaticResource ParamLabel}" />
<Button Content="开启" Command="{Binding RelayOnCommand}"
Style="{StaticResource CmdBtn}" />
<Button Content="关闭" Command="{Binding RelayOffCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 输出参数设置 -->
<GroupBox Header="输出参数设置" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="电压 (V)" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}"
Text="{Binding Voltage, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="设置" Command="{Binding SetVoltageCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="频率 (Hz)" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}"
Text="{Binding Frequency, UpdateSourceTrigger=PropertyChanged}" />
<Button Content="设置" Command="{Binding SetFrequencyCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="波形" Style="{StaticResource ParamLabel}" />
<ComboBox Width="130" Height="32" FontSize="12" Margin="4,0"
ItemsSource="{Binding WaveformOptions}"
SelectedItem="{Binding Waveform}" />
<Button Content="设置" Command="{Binding SetWaveformCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
</StackPanel>
</GroupBox>
<!-- LIST 模式 -->
<GroupBox Header="LIST 模式" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="操作模态" Style="{StaticResource ParamLabel}" />
<Button Content="FIXED 固定" Command="{Binding SetFixedModeCommand}"
Style="{StaticResource CmdBtn}" />
<Button Content="进入 LIST" Command="{Binding EnterListModeCommand}"
Style="{StaticResource CmdBtn}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="列表耦合" Style="{StaticResource ParamLabel}" />
<ComboBox Width="80" Height="32" FontSize="12" Margin="4,0"
ItemsSource="{Binding ListCouplingOptions}"
SelectedItem="{Binding ListCoupling}" />
<TextBlock Text="触发" Style="{StaticResource ParamLabel}" />
<ComboBox Width="100" Height="32" FontSize="12" Margin="4,0"
ItemsSource="{Binding ListTriggerOptions}"
SelectedItem="{Binding ListTrigger}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="执行次数" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource NumInput}" Width="80"
Text="{Binding ListCount, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Text="时间基础" Style="{StaticResource ParamLabel}" />
<ComboBox Width="90" Height="32" FontSize="12" Margin="4,0"
ItemsSource="{Binding ListTimeBaseOptions}"
SelectedItem="{Binding ListTimeBase}" />
</StackPanel>
<Button Content="应用基础配置(耦合 / 触发 / 次数 / 时基)"
Command="{Binding ApplyListBasicCommand}"
Style="{StaticResource CmdBtn}"
HorizontalAlignment="Left" Margin="0,4" />
<Separator Margin="0,6" />
<TextBlock Text="序列设置(英文逗号分隔,留空则不下发)"
FontSize="12" Foreground="#616161" Margin="4,2" />
<Grid Margin="0,2">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="92" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="AC起始电压"
Style="{StaticResource ParamLabel}" />
<TextBox Grid.Row="0" Grid.Column="1" Style="{StaticResource NumInput}"
Width="240" ToolTip="单位 V,如 0,50,100,150"
Text="{Binding ListVoltAcStartText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="1" Grid.Column="0" Text="AC结束电压"
Style="{StaticResource ParamLabel}" />
<TextBox Grid.Row="1" Grid.Column="1" Style="{StaticResource NumInput}"
Width="240" ToolTip="单位 V,如 150,100,50,0"
Text="{Binding ListVoltAcEndText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="2" Grid.Column="0" Text="起始频率"
Style="{StaticResource ParamLabel}" />
<TextBox Grid.Row="2" Grid.Column="1" Style="{StaticResource NumInput}"
Width="240" ToolTip="单位 Hz,如 50,60,50"
Text="{Binding ListFreqStartText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="3" Grid.Column="0" Text="结束频率"
Style="{StaticResource ParamLabel}" />
<TextBox Grid.Row="3" Grid.Column="1" Style="{StaticResource NumInput}"
Width="240" ToolTip="单位 Hz,如 50,50,60"
Text="{Binding ListFreqEndText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="4" Grid.Column="0" Text="静止时间"
Style="{StaticResource ParamLabel}" />
<TextBox Grid.Row="4" Grid.Column="1" Style="{StaticResource NumInput}"
Width="240" ToolTip="单位 ms,如 500,500,500"
Text="{Binding ListDwellText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="5" Grid.Column="0" Text="相位角度"
Style="{StaticResource ParamLabel}" />
<TextBox Grid.Row="5" Grid.Column="1" Style="{StaticResource NumInput}"
Width="240" ToolTip="单位 °,如 0,120,240"
Text="{Binding ListDegrText, UpdateSourceTrigger=PropertyChanged}" />
<TextBlock Grid.Row="6" Grid.Column="0" Text="波形顺序"
Style="{StaticResource ParamLabel}" />
<TextBox Grid.Row="6" Grid.Column="1" Style="{StaticResource NumInput}"
Width="240" ToolTip="A / B 序列,如 A,B,A"
Text="{Binding ListShapeText, UpdateSourceTrigger=PropertyChanged}" />
</Grid>
<Button Content="应用序列" Command="{Binding ApplyListSequencesCommand}"
Style="{StaticResource CmdBtn}"
HorizontalAlignment="Left" Margin="0,4" />
<Separator Margin="0,6" />
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="开始执行" Command="{Binding ListStartCommand}"
Style="{StaticResource CmdBtn}"
Background="#388E3C" Foreground="White" />
<Button Content="停止执行" Command="{Binding ListStopCommand}"
Style="{StaticResource WarnBtn}" />
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 数据采集 -->
<GroupBox Header="数据采集" Margin="0,0,0,8"
materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<StackPanel Margin="4">
<StackPanel Orientation="Horizontal" Margin="0,4">
<Button Content="开始采集" Command="{Binding StartSamplingCommand}"
Style="{StaticResource CmdBtn}"
Background="#388E3C" Foreground="White" />
<Button Content="停止采集" Command="{Binding StopSamplingCommand}"
Style="{StaticResource CmdBtn}" />
<Button Content="清空图表" Command="{Binding ClearChartsCommand}"
Style="{StaticResource CmdBtn}" />
<TextBlock VerticalAlignment="Center" FontSize="12" Margin="8,0,0,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Setter Property="Text" Value="采集中…" />
<Setter Property="Foreground" Value="#4CAF50" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsSampling}" Value="False">
<Setter Property="Text" Value="采集已停止" />
<Setter Property="Foreground" Value="#9E9E9E" />
</DataTrigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,4">
<TextBlock Text="频率" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding FrequencyNow, Mode=OneWay, StringFormat={}{0:F2}}" />
<TextBlock Text="Hz" VerticalAlignment="Center" Margin="2,0,12,0" />
<TextBlock Text="功率" Style="{StaticResource ParamLabel}" />
<TextBox Style="{StaticResource MeasureBox}"
Text="{Binding PowerNow, Mode=OneWay, StringFormat={}{0:F2}}" />
<TextBlock Text="W" VerticalAlignment="Center" Margin="2,0,0,0" />
</StackPanel>
</StackPanel>
</GroupBox>
<!-- 日志 -->
<GroupBox Header="日志" materialDesign:ColorZoneAssist.Mode="PrimaryLight">
<ScrollViewer Height="150" VerticalScrollBarVisibility="Auto">
<TextBox Text="{Binding ResponseLog, Mode=OneWay}"
IsReadOnly="True" TextWrapping="Wrap"
FontSize="11" FontFamily="Consolas"
Background="#FAFAFA" BorderThickness="0"
VerticalAlignment="Top" />
</ScrollViewer>
</GroupBox>
</StackPanel>
</ScrollViewer>
<!-- ============ 右侧:三相正弦波(电压 / 电流 / 功率,横轴相位角 0~360°,有正有负) ============ -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="*" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- 三相电压 -->
<Border Grid.Row="0" Background="White" BorderBrush="#DDD" BorderThickness="1" Margin="0,0,0,4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="10,6">
<TextBlock Text="三相电压 (V)" FontWeight="Bold" FontSize="13"
VerticalAlignment="Center" Margin="0,0,16,0" />
<TextBlock Text="A" FontSize="12" Foreground="#E53935" VerticalAlignment="Center" />
<TextBlock Text="{Binding VoltageA, StringFormat={}{0:F1}}" FontSize="12"
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,14,0" />
<TextBlock Text="B" FontSize="12" Foreground="#43A047" VerticalAlignment="Center" />
<TextBlock Text="{Binding VoltageB, StringFormat={}{0:F1}}" FontSize="12"
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,14,0" />
<TextBlock Text="C" FontSize="12" Foreground="#1E88E5" VerticalAlignment="Center" />
<TextBlock Text="{Binding VoltageC, StringFormat={}{0:F1}}" FontSize="12"
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,0,0" />
</StackPanel>
<oxy:PlotView Grid.Row="1" Model="{Binding VoltagePlot}"
Background="Transparent" Margin="4,0,4,4" />
</Grid>
</Border>
<!-- 三相电流 -->
<Border Grid.Row="1" Background="White" BorderBrush="#DDD" BorderThickness="1" Margin="0,0,0,4">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="10,6">
<TextBlock Text="三相电流 (A)" FontWeight="Bold" FontSize="13"
VerticalAlignment="Center" Margin="0,0,16,0" />
<TextBlock Text="A" FontSize="12" Foreground="#E53935" VerticalAlignment="Center" />
<TextBlock Text="{Binding CurrentA, StringFormat={}{0:F2}}" FontSize="12"
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,14,0" />
<TextBlock Text="B" FontSize="12" Foreground="#43A047" VerticalAlignment="Center" />
<TextBlock Text="{Binding CurrentB, StringFormat={}{0:F2}}" FontSize="12"
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,14,0" />
<TextBlock Text="C" FontSize="12" Foreground="#1E88E5" VerticalAlignment="Center" />
<TextBlock Text="{Binding CurrentC, StringFormat={}{0:F2}}" FontSize="12"
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,0,0" />
</StackPanel>
<oxy:PlotView Grid.Row="1" Model="{Binding CurrentPlot}"
Background="Transparent" Margin="4,0,4,4" />
</Grid>
</Border>
<!-- 三相功率 -->
<Border Grid.Row="2" Background="White" BorderBrush="#DDD" BorderThickness="1">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Margin="10,6">
<TextBlock Text="三相功率 (W)" FontWeight="Bold" FontSize="13"
VerticalAlignment="Center" Margin="0,0,16,0" />
<TextBlock Text="总有功" FontSize="12" Foreground="#616161" VerticalAlignment="Center" />
<TextBlock Text="{Binding PowerNow, StringFormat={}{0:F1}}" FontSize="12"
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,14,0" />
<TextBlock Text="频率" FontSize="12" Foreground="#616161" VerticalAlignment="Center" />
<TextBlock Text="{Binding FrequencyNow, StringFormat={}{0:F2} Hz}" FontSize="12"
FontWeight="Bold" VerticalAlignment="Center" Margin="3,0,0,0" />
</StackPanel>
<oxy:PlotView Grid.Row="1" Model="{Binding PowerPlot}"
Background="Transparent" Margin="4,0,4,4" />
</Grid>
</Border>
</Grid>
</Grid>
</Border>
</UserControl>
@@ -0,0 +1,28 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
namespace PowerAdaptabilityModule.Views
{
/// <summary>
/// PowerAdaptabilityView.xaml 的交互逻辑
/// </summary>
public partial class PowerAdaptabilityView : UserControl
{
public PowerAdaptabilityView()
{
InitializeComponent();
}
}
}
@@ -33,12 +33,12 @@ namespace UIShare.GlobalVariable
/// <summary> /// <summary>
/// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance) /// 已注册的采样项:(Fingerprint, MethodName, 编译后的委托, DeviceInstance)
/// 委托签名统一为 Func&lt;object, CancellationToken, Task&lt;string&gt;&gt; /// 委托签名统一为 Func&lt;object, CancellationToken, Task&lt;double&gt;&gt;
/// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换) /// - 第一个参数 = 设备实例(open delegate 风格,编译时已做类型转换)
/// - 第二个参数 = CancellationToken(无参方法会忽略它) /// - 第二个参数 = CancellationToken(无参方法会忽略它)
/// - 返回值 = Task&lt;string&gt; /// - 返回值 = Task&lt;double&gt;(Task&lt;string&gt; 方法自动 TryParse 为 double)
/// </summary> /// </summary>
private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<string>> Invoker, object Device)> _registeredMethods = new(); private readonly List<(string Fingerprint, string MethodName, Func<object, CancellationToken, Task<double>> Invoker, object Device)> _registeredMethods = new();
private CancellationTokenSource? _cts; private CancellationTokenSource? _cts;
private bool _disposed; private bool _disposed;
@@ -76,11 +76,13 @@ namespace UIShare.GlobalVariable
/// <summary> /// <summary>
/// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。 /// 将 MethodInfo 编译为强类型委托,后续调用零反射开销。
/// 统一签名为 Func&lt;object, CancellationToken, Task&lt;string&gt;&gt;: /// 统一签名为 Func&lt;object, CancellationToken, Task&lt;double&gt;&gt;:
/// - Task&lt;double&gt; 方法:直接返回 double 值
/// - Task&lt;string&gt; 方法:自动 TryParse 为 double
/// - 无参方法:忽略 CancellationToken /// - 无参方法:忽略 CancellationToken
/// - 带 CancellationToken 方法:直接透传 /// - 带 CancellationToken 方法:直接透传
/// </summary> /// </summary>
private static Func<object, CancellationToken, Task<string>> BuildInvoker(MethodInfo method) private static Func<object, CancellationToken, Task<double>> BuildInvoker(MethodInfo method)
{ {
var deviceParam = Expression.Parameter(typeof(object), "device"); var deviceParam = Expression.Parameter(typeof(object), "device");
var ctParam = Expression.Parameter(typeof(CancellationToken), "ct"); var ctParam = Expression.Parameter(typeof(CancellationToken), "ct");
@@ -96,8 +98,24 @@ namespace UIShare.GlobalVariable
var call = Expression.Call(castDevice, method, callArgs); var call = Expression.Call(castDevice, method, callArgs);
return Expression.Lambda<Func<object, CancellationToken, Task<string>>>( if (method.ReturnType == typeof(Task<double>))
call, deviceParam, ctParam).Compile(); {
// Task<double> 方法:直接返回
return Expression.Lambda<Func<object, CancellationToken, Task<double>>>(
call, deviceParam, ctParam).Compile();
}
else
{
// Task<string> 方法:await 后 TryParse 为 double
return async (device, ct) =>
{
// 通过 MethodInfo.Invoke 调用(仅 Task<string> 路径,性能可接受)
var task = (Task<string>)method.Invoke(device,
parms.Length == 1 ? new object[] { ct } : Array.Empty<object>())!;
string raw = await task.ConfigureAwait(false);
return double.TryParse(raw, out double v) ? v : double.NaN;
};
}
} }
/// <summary> /// <summary>
@@ -126,7 +144,9 @@ namespace UIShare.GlobalVariable
.Where(m => .Where(m =>
{ {
if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false; if (m.GetCustomAttribute<MonitorableAttribute>() == null) return false;
if (m.ReturnType != typeof(Task<string>)) return false; // 同时支持 Task<string> 和 Task<double> 返回类型
var rt = m.ReturnType;
if (rt != typeof(Task<string>) && rt != typeof(Task<double>)) return false;
var parms = m.GetParameters(); var parms = m.GetParameters();
return parms.Length == 0 || return parms.Length == 0 ||
(parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken)); (parms.Length == 1 && parms[0].ParameterType == typeof(CancellationToken));
@@ -184,10 +204,10 @@ namespace UIShare.GlobalVariable
{ {
try try
{ {
// 直接委托调用,零反射开销 // 直接委托调用,零反射开销(Task<double> 路径)或自动 TryParse(Task<string> 路径)
string raw = await entry.Invoker(entry.Device, token).ConfigureAwait(false); double value = await entry.Invoker(entry.Device, token).ConfigureAwait(false);
if (!double.TryParse(raw, out double value)) return; if (double.IsNaN(value)) return;
// 采样成功,重置失败计数 // 采样成功,重置失败计数
_failureCounts.TryRemove(channelKey, out _); _failureCounts.TryRemove(channelKey, out _);