Compare commits
25
Commits
master
..
b1a160d159
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b1a160d159 | ||
|
|
3d30d33f7b | ||
|
|
1355753e41 | ||
|
|
dfbc5797bf | ||
|
|
7043a66a28 | ||
|
|
8d8df56544 | ||
|
|
8b53d8e921 | ||
|
|
31255fadeb | ||
|
|
c4fc2d80fd | ||
|
|
4acb4d8d13 | ||
|
|
7a56eb98d2 | ||
|
|
61d7293afb | ||
|
|
47b4d3837b | ||
|
|
6a59552bc7 | ||
|
|
38f594e4aa | ||
|
|
fb6e5b3cb1 | ||
|
|
2bb8313f99 | ||
|
|
92eab2f626 | ||
|
|
8d941d92f0 | ||
|
|
e392527592 | ||
|
|
30f5f6f8d6 | ||
|
|
b43090e56e | ||
|
|
a9355087ad | ||
|
|
8daa1df8e9 | ||
|
|
7776e6c7ab |
+59
-2
@@ -1,4 +1,5 @@
|
|||||||
using Common;
|
using Common;
|
||||||
|
using Command;
|
||||||
using ADP.ViewModels;
|
using ADP.ViewModels;
|
||||||
using ADP.ViewModels.Dialogs;
|
using ADP.ViewModels.Dialogs;
|
||||||
using ADP.Views;
|
using ADP.Views;
|
||||||
@@ -12,6 +13,8 @@ using Service.Interface;
|
|||||||
using System.Configuration;
|
using System.Configuration;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
using UIShare.PubEvent;
|
using UIShare.PubEvent;
|
||||||
using static System.Runtime.InteropServices.JavaScript.JSType;
|
using static System.Runtime.InteropServices.JavaScript.JSType;
|
||||||
@@ -23,6 +26,7 @@ using DeviceCommand.Base;
|
|||||||
using AutoMapper;
|
using AutoMapper;
|
||||||
using Microsoft.Extensions.Logging.Abstractions;
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
using ADP.Profiles;
|
using ADP.Profiles;
|
||||||
|
using Prism.Dialogs;
|
||||||
using 加密狗;
|
using 加密狗;
|
||||||
|
|
||||||
namespace ADP
|
namespace ADP
|
||||||
@@ -60,6 +64,56 @@ namespace ADP
|
|||||||
}
|
}
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
|
// 向命令库 CommandDialog 注入弹窗能力(命令库是纯 .NET 类库,不引用 WPF,这里通过委托实现依赖倒置)
|
||||||
|
var dialogService = Container.Resolve<IDialogService>();
|
||||||
|
CommandDialog.弹窗处理器 = (弹窗类型, 弹窗详细, 是否阻塞, 自动关闭秒数, ct) =>
|
||||||
|
{
|
||||||
|
var tcs = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||||
|
CancellationTokenRegistration registration = default;
|
||||||
|
if (是否阻塞)
|
||||||
|
{
|
||||||
|
registration = ct.Register(() => tcs.TrySetCanceled(ct));
|
||||||
|
}
|
||||||
|
// 命令可能在后台线程执行,统一切回 UI 线程弹窗
|
||||||
|
Application.Current.Dispatcher.Invoke(() =>
|
||||||
|
{
|
||||||
|
var param = new DialogParameters
|
||||||
|
{
|
||||||
|
{ "Title", 弹窗类型 switch
|
||||||
|
{
|
||||||
|
CommandDialog.DialogType.Warning => "警告",
|
||||||
|
CommandDialog.DialogType.Error => "错误",
|
||||||
|
_ => "信息提示"
|
||||||
|
} },
|
||||||
|
{ "Message", 弹窗详细 ?? string.Empty },
|
||||||
|
{ "Icon", 弹窗类型 switch
|
||||||
|
{
|
||||||
|
CommandDialog.DialogType.Warning => "warn",
|
||||||
|
CommandDialog.DialogType.Error => "error",
|
||||||
|
_ => "info"
|
||||||
|
} },
|
||||||
|
{ "ShowOk", true },
|
||||||
|
{ "AutoCloseSeconds", (double)自动关闭秒数 }
|
||||||
|
};
|
||||||
|
if (是否阻塞)
|
||||||
|
{
|
||||||
|
// 阻塞:用户手动关闭或自动关闭后才继续执行步骤
|
||||||
|
dialogService.ShowDialog("MessageBox", param, _ =>
|
||||||
|
{
|
||||||
|
registration.Dispose();
|
||||||
|
tcs.TrySetResult(true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 非阻塞:弹出后步骤立即继续(不等关闭回调)
|
||||||
|
dialogService.Show("MessageBox", param, _ => { });
|
||||||
|
tcs.TrySetResult(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return tcs.Task;
|
||||||
|
};
|
||||||
|
|
||||||
// 配置全局日志分发器:按 CurrentScope 路由到对应 LogArea
|
// 配置全局日志分发器:按 CurrentScope 路由到对应 LogArea
|
||||||
var globalInfo = Container.Resolve<GlobalInfo>();
|
var globalInfo = Container.Resolve<GlobalInfo>();
|
||||||
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
|
LoggerHelper.Progress = new ScopeLogDispatcher(globalInfo);
|
||||||
@@ -67,6 +121,8 @@ namespace ADP
|
|||||||
//初始化数据库
|
//初始化数据库
|
||||||
DatabaseConfig.SetTenant(10001);
|
DatabaseConfig.SetTenant(10001);
|
||||||
DatabaseConfig.InitSqlite();
|
DatabaseConfig.InitSqlite();
|
||||||
|
// 启动时检查数据库文件是否过期,过期则归档(重命名加时间戳),后续会自动创建新库
|
||||||
|
DatabaseConfig.TryArchiveOldDatabase();
|
||||||
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
DatabaseConfig.CreateDatabaseAndCheckConnection(createDatabase: true, checkConnection: true);
|
||||||
SqlSugarContext.InitDatabase();
|
SqlSugarContext.InitDatabase();
|
||||||
//显示登录窗口
|
//显示登录窗口
|
||||||
@@ -84,8 +140,8 @@ namespace ADP
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
string strUserSn = "ACA89B2C1194650D4E93D97BE189FFCC";
|
string strUserSn = "ACC77E6F627236F3D02FA8EA1BE35794";
|
||||||
string strApiKey = "C6D01BCD442347822876F8232245DE9422643C167B8C61CE4DF6049990E7395588675A8FC6AA4BF7AD353D669C993236BEA83F77551753885A47AF34185CC3A7";
|
string strApiKey = "9185203127F9DE4FB4BC2F073F6CC583D9BED1B6713EF00A0C06625EDD0FAE862164280F5B2AADC090C4878E96DDFE05DDE3952F9B6F1FA483A340FB34A5D83B";
|
||||||
|
|
||||||
Byte[] buffer = new Byte[256];
|
Byte[] buffer = new Byte[256];
|
||||||
buffer = System.Text.Encoding.Default.GetBytes(strUserSn);
|
buffer = System.Text.Encoding.Default.GetBytes(strUserSn);
|
||||||
@@ -152,6 +208,7 @@ namespace ADP
|
|||||||
//注册服务
|
//注册服务
|
||||||
containerRegistry.Register<IMonitorValueService, MonitorValueService>();
|
containerRegistry.Register<IMonitorValueService, MonitorValueService>();
|
||||||
containerRegistry.Register<ITestReportService, TestReportService>();
|
containerRegistry.Register<ITestReportService, TestReportService>();
|
||||||
|
containerRegistry.Register<ITestCheckRecordService, TestCheckRecordService>();
|
||||||
}
|
}
|
||||||
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
|
//指定模块加载方式(需要手动将模块生成的dll放入Modules文件夹中)
|
||||||
protected override IModuleCatalog CreateModuleCatalog()
|
protected override IModuleCatalog CreateModuleCatalog()
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
using UIShare.PubEvent;
|
using UIShare.PubEvent;
|
||||||
using UIShare.ViewModelBase;
|
using UIShare.ViewModelBase;
|
||||||
|
using System;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using System.Windows.Media;
|
using System.Windows.Media;
|
||||||
|
using System.Windows.Threading;
|
||||||
|
|
||||||
namespace ADP.ViewModels.Dialogs
|
namespace ADP.ViewModels.Dialogs
|
||||||
{
|
{
|
||||||
@@ -69,6 +71,11 @@ namespace ADP.ViewModels.Dialogs
|
|||||||
|
|
||||||
public DialogCloseListener RequestClose { get; set; }
|
public DialogCloseListener RequestClose { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 自动关闭定时器:自动关闭秒数大于 0 时启动,到时自动关闭弹窗(同时解除阻塞等待)
|
||||||
|
/// </summary>
|
||||||
|
private DispatcherTimer _autoCloseTimer;
|
||||||
|
|
||||||
public MessageBoxViewModel(IContainerProvider containerProvider):base(containerProvider)
|
public MessageBoxViewModel(IContainerProvider containerProvider):base(containerProvider)
|
||||||
{
|
{
|
||||||
YesCommand = new DelegateCommand(OnYes);
|
YesCommand = new DelegateCommand(OnYes);
|
||||||
@@ -93,6 +100,7 @@ namespace ADP.ViewModels.Dialogs
|
|||||||
|
|
||||||
public override void OnDialogClosed()
|
public override void OnDialogClosed()
|
||||||
{
|
{
|
||||||
|
_autoCloseTimer?.Stop();
|
||||||
_eventAggregator.GetEvent<OverlayEvent>().Publish(false);
|
_eventAggregator.GetEvent<OverlayEvent>().Publish(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +125,21 @@ namespace ADP.ViewModels.Dialogs
|
|||||||
ShowNo = parameters.GetValue<bool>("ShowNo");
|
ShowNo = parameters.GetValue<bool>("ShowNo");
|
||||||
ShowOk = parameters.GetValue<bool>("ShowOk");
|
ShowOk = parameters.GetValue<bool>("ShowOk");
|
||||||
ShowCancel = parameters.GetValue<bool>("ShowCancel");
|
ShowCancel = parameters.GetValue<bool>("ShowCancel");
|
||||||
|
|
||||||
|
// 自动关闭:秒数大于 0 时启动定时器,到时自动关闭(供命令库 弹窗 命令的自动关闭参数使用)
|
||||||
|
if (parameters.TryGetValue("AutoCloseSeconds", out double autoCloseSeconds) && autoCloseSeconds > 0)
|
||||||
|
{
|
||||||
|
_autoCloseTimer = new DispatcherTimer
|
||||||
|
{
|
||||||
|
Interval = TimeSpan.FromSeconds(autoCloseSeconds)
|
||||||
|
};
|
||||||
|
_autoCloseTimer.Tick += (s, e) =>
|
||||||
|
{
|
||||||
|
_autoCloseTimer.Stop();
|
||||||
|
RequestClose.Invoke(new DialogResult(ButtonResult.OK));
|
||||||
|
};
|
||||||
|
_autoCloseTimer.Start();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -154,6 +154,24 @@ namespace ADP.ViewModels
|
|||||||
public ICommand MonitorValueSettingCommand { get; set; }
|
public ICommand MonitorValueSettingCommand { get; set; }
|
||||||
public ICommand GetFileStringCommand { get; set; }
|
public ICommand GetFileStringCommand { get; set; }
|
||||||
public ICommand SilenceBuzzerCommand { get; set; }
|
public ICommand SilenceBuzzerCommand { get; set; }
|
||||||
|
public ICommand GoBackCommand { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 子程序导航
|
||||||
|
|
||||||
|
public bool CanGoBack =>
|
||||||
|
(CurrentContext?.MainNav.CanGoBack ?? false) ||
|
||||||
|
(CurrentContext?.ErrorNav.CanGoBack ?? false);
|
||||||
|
|
||||||
|
private void OnGoBack()
|
||||||
|
{
|
||||||
|
var ctx = CurrentContext;
|
||||||
|
if (ctx == null) return;
|
||||||
|
// 工具栏按钮同时重置两个 Tab 的导航
|
||||||
|
ctx.MainNav.Reset(ctx.Program, "主程序");
|
||||||
|
ctx.ErrorNav.Reset(ctx.Program, "错误程序");
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public ShellViewModel(IContainerProvider containerProvider)
|
public ShellViewModel(IContainerProvider containerProvider)
|
||||||
@@ -188,8 +206,9 @@ namespace ADP.ViewModels
|
|||||||
MonitorValueSettingCommand = new DelegateCommand(MonitorValueSetting);
|
MonitorValueSettingCommand = new DelegateCommand(MonitorValueSetting);
|
||||||
GetFileStringCommand = new DelegateCommand(GetFileString);
|
GetFileStringCommand = new DelegateCommand(GetFileString);
|
||||||
SilenceBuzzerCommand = new AsyncDelegateCommand(SilenceBuzzer);
|
SilenceBuzzerCommand = new AsyncDelegateCommand(SilenceBuzzer);
|
||||||
|
GoBackCommand = new DelegateCommand(OnGoBack);
|
||||||
|
|
||||||
_globalInfo.ContextDic.Add("default", new ScopedContext());
|
_globalInfo.ContextDic.TryAdd("default", new ScopedContext());
|
||||||
|
|
||||||
_eventAggregator.GetEvent<LoginSuccessEvent>().Subscribe(() =>
|
_eventAggregator.GetEvent<LoginSuccessEvent>().Subscribe(() =>
|
||||||
{
|
{
|
||||||
@@ -457,6 +476,9 @@ namespace ADP.ViewModels
|
|||||||
|
|
||||||
LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 复位命令");
|
LoggerHelper.InfoWithNotify(runningScope, $"{_globalInfo.UserName} 执行工位 [{runningScope}] 复位命令");
|
||||||
|
|
||||||
|
// 重置导航栈,回到根程序界面
|
||||||
|
targetContext.ResetNavigation();
|
||||||
|
|
||||||
_executionTasks.TryGetValue(runningScope, out var currentTask);
|
_executionTasks.TryGetValue(runningScope, out var currentTask);
|
||||||
_errorExecutionTasks.TryGetValue(runningScope, out var errorTask);
|
_errorExecutionTasks.TryGetValue(runningScope, out var errorTask);
|
||||||
|
|
||||||
@@ -465,9 +487,22 @@ namespace ADP.ViewModels
|
|||||||
targetRunner.stepCTS.Cancel();
|
targetRunner.stepCTS.Cancel();
|
||||||
targetRunner.errorStepCTS.Cancel();
|
targetRunner.errorStepCTS.Cancel();
|
||||||
|
|
||||||
await Task.Delay(200);
|
// 💡 等待旧任务真正结束,而不是只等 200ms
|
||||||
|
if (currentTask != null)
|
||||||
|
{
|
||||||
|
try { await currentTask; } catch { /* 取消导致的异常忽略 */ }
|
||||||
|
}
|
||||||
|
if (errorTask != null)
|
||||||
|
{
|
||||||
|
try { await errorTask; } catch { /* 取消导致的异常忽略 */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// 💡 彻底复位:替换 CTS、清理任务字典、停止计时器、归零状态
|
||||||
|
targetRunner.stepCTS = new CancellationTokenSource();
|
||||||
|
targetRunner.errorStepCTS = new CancellationTokenSource();
|
||||||
|
_executionTasks.TryRemove(runningScope, out _);
|
||||||
|
_errorExecutionTasks.TryRemove(runningScope, out _);
|
||||||
|
|
||||||
// 💡 彻底复位:停止当前工位的计时器,并且完全归零(Reset)
|
|
||||||
targetContext.SW.Reset();
|
targetContext.SW.Reset();
|
||||||
targetContext.RunningTime = TimeSpan.Zero;
|
targetContext.RunningTime = TimeSpan.Zero;
|
||||||
|
|
||||||
@@ -573,6 +608,7 @@ namespace ADP.ViewModels
|
|||||||
targetContext.Program.Parameters.Clear();
|
targetContext.Program.Parameters.Clear();
|
||||||
targetContext.Program.StepCollection.Clear();
|
targetContext.Program.StepCollection.Clear();
|
||||||
targetContext.Program.ErrorStepCollection.Clear();
|
targetContext.Program.ErrorStepCollection.Clear();
|
||||||
|
targetContext.ResetNavigation();
|
||||||
foreach (var item in CurrentConfig.ParameterList)
|
foreach (var item in CurrentConfig.ParameterList)
|
||||||
{
|
{
|
||||||
var param = CurrentConfig.SharedParameterList.FirstOrDefault(x => x.ParameterName == item.Name);
|
var param = CurrentConfig.SharedParameterList.FirstOrDefault(x => x.ParameterName == item.Name);
|
||||||
|
|||||||
@@ -226,6 +226,16 @@ Command="{Binding GetFileStringCommand}">
|
|||||||
Foreground="Black" />
|
Foreground="Black" />
|
||||||
</MenuItem.Icon>
|
</MenuItem.Icon>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
|
<Separator/>
|
||||||
|
<!-- 返回上一级程序 -->
|
||||||
|
<MenuItem Header="返回上一级程序"
|
||||||
|
Foreground="Black"
|
||||||
|
Command="{Binding GoBackCommand}">
|
||||||
|
<MenuItem.Icon>
|
||||||
|
<materialDesign:PackIcon Kind="ArrowLeftBold"
|
||||||
|
Foreground="Black" />
|
||||||
|
</MenuItem.Icon>
|
||||||
|
</MenuItem>
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
<MenuItem Header="切换九宫格"
|
<MenuItem Header="切换九宫格"
|
||||||
FontSize="13"
|
FontSize="13"
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+1303
File diff suppressed because it is too large
Load Diff
@@ -1,164 +0,0 @@
|
|||||||
using DeviceCommand.Base;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BenchMovementModule.HardwareDrive
|
|
||||||
{
|
|
||||||
public abstract class GantryControlBase
|
|
||||||
{
|
|
||||||
protected readonly IModbusDevice _device;
|
|
||||||
protected readonly byte _slaveAddress;
|
|
||||||
|
|
||||||
// 默认 PLC Modbus 映射地址
|
|
||||||
protected const ushort ADDR_HM_X_ENABLE = 0xC100; // HM0: X轴使能
|
|
||||||
protected const ushort ADDR_HM_Y_ENABLE = 0xC101; // HM1: Y轴使能
|
|
||||||
protected const ushort ADDR_HM_Z_ENABLE = 0xC102; // HM2: Z轴使能
|
|
||||||
|
|
||||||
protected const ushort ADDR_M_X_START = 30; // M30: X轴指定位置启动
|
|
||||||
protected const ushort ADDR_M_Y_START = 31; // M31: Y轴指定位置启动
|
|
||||||
protected const ushort ADDR_M_Z_START = 35; // M35: Z轴指定位置启动
|
|
||||||
|
|
||||||
protected const ushort ADDR_M_ALL_STOP = 40; // M40: 全停
|
|
||||||
protected const ushort ADDR_M_X_STOP = 41; // M41: X轴停止
|
|
||||||
protected const ushort ADDR_M_Y_STOP = 42; // M42: Y轴停止
|
|
||||||
protected const ushort ADDR_M_Z_STOP = 43; // M43: Z轴停止
|
|
||||||
|
|
||||||
protected const ushort ADDR_M_HOME = 100; // M100: 回原点触发
|
|
||||||
|
|
||||||
protected const ushort ADDR_HD_X_SPEED = 0xA166; // HD230: X轴速度 (32位)
|
|
||||||
protected const ushort ADDR_HD_Y_SPEED = 0xA17A; // HD250: Y轴速度 (32位)
|
|
||||||
protected const ushort ADDR_HD_Z_SPEED = 0xA170; // HD240: Z轴速度 (32位)
|
|
||||||
|
|
||||||
protected const ushort ADDR_HD_X_TARGET = 0xA10C; // HD140: X轴指定位置 (32位)
|
|
||||||
protected const ushort ADDR_HD_Y_TARGET = 0xA120; // HD160: Y轴指定位置 (32位)
|
|
||||||
protected const ushort ADDR_HD_Z_TARGET = 0xA116; // HD150: Z轴指定位置 (32位)
|
|
||||||
|
|
||||||
public bool IsConnected => _device?.IsConnected ?? false;
|
|
||||||
|
|
||||||
protected GantryControlBase(IModbusDevice device, byte slaveAddress = 1)
|
|
||||||
{
|
|
||||||
_device = device ?? throw new ArgumentNullException(nameof(device));
|
|
||||||
_slaveAddress = slaveAddress;
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<bool> ConnectAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
return await _device.ConnectAsync(ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Close()
|
|
||||||
{
|
|
||||||
_device.Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 轴使能控制
|
|
||||||
/// </summary>
|
|
||||||
public async Task SetAxisEnableAsync(int axis, bool enable, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ushort address = axis switch
|
|
||||||
{
|
|
||||||
1 => ADDR_HM_X_ENABLE, // X
|
|
||||||
2 => ADDR_HM_Y_ENABLE, // Y
|
|
||||||
3 => ADDR_HM_Z_ENABLE, // Z
|
|
||||||
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)")
|
|
||||||
};
|
|
||||||
|
|
||||||
await _device.WriteSingleCoilAsync(_slaveAddress, address, enable, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 左右移动(X轴)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="position">目标绝对位置(脉冲数/单位值)</param>
|
|
||||||
/// <param name="speed">运行速度</param>
|
|
||||||
public async Task MoveLeftRightAsync(int position, int speed, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
// 1. 写入速度与目标位置(32位数据,写入连续的2个保持寄存器)
|
|
||||||
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_SPEED, Int32ToUshorts(speed), ct);
|
|
||||||
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_TARGET, Int32ToUshorts(position), ct);
|
|
||||||
|
|
||||||
// 2. 边缘触发启动信号 M30
|
|
||||||
await TriggerCoilAsync(ADDR_M_X_START, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 上下移动(Z轴)
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="position">目标绝对位置</param>
|
|
||||||
/// <param name="speed">运行速度</param>
|
|
||||||
public async Task MoveUpDownAsync(int position, int speed, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
// 1. 写入速度与目标位置(32位数据)
|
|
||||||
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_SPEED, Int32ToUshorts(speed), ct);
|
|
||||||
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_TARGET, Int32ToUshorts(position), ct);
|
|
||||||
|
|
||||||
// 2. 边缘触发启动信号 M35
|
|
||||||
await TriggerCoilAsync(ADDR_M_Z_START, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 前后移动(Y轴 - 预留拓展)
|
|
||||||
/// </summary>
|
|
||||||
public async Task MoveFrontBackAsync(int position, int speed, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_SPEED, Int32ToUshorts(speed), ct);
|
|
||||||
await _device.WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_TARGET, Int32ToUshorts(position), ct);
|
|
||||||
await TriggerCoilAsync(ADDR_M_Y_START, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 全局回零
|
|
||||||
/// </summary>
|
|
||||||
public async Task HomeAsync(CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
await TriggerCoilAsync(ADDR_M_HOME, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 轴停止
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="axis">1:X轴, 2:Y轴, 3:Z轴, 0:全停</param>
|
|
||||||
public async Task StopAsync(int axis = 0, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
ushort address = axis switch
|
|
||||||
{
|
|
||||||
0 => ADDR_M_ALL_STOP,
|
|
||||||
1 => ADDR_M_X_STOP,
|
|
||||||
2 => ADDR_M_Y_STOP,
|
|
||||||
3 => ADDR_M_Z_STOP,
|
|
||||||
_ => ADDR_M_ALL_STOP
|
|
||||||
};
|
|
||||||
|
|
||||||
await TriggerCoilAsync(address, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region 内部工具方法
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 触发一个点动信号(置 1 后,延时 100ms 自动置 0)
|
|
||||||
/// </summary>
|
|
||||||
protected async Task TriggerCoilAsync(ushort address, CancellationToken ct = default)
|
|
||||||
{
|
|
||||||
await _device.WriteSingleCoilAsync(_slaveAddress, address, true, ct);
|
|
||||||
await Task.Delay(100, ct); // 给予 PLC 扫描周期足够的响应时间
|
|
||||||
await _device.WriteSingleCoilAsync(_slaveAddress, address, false, ct);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将 32 位整型数据转换为 Modbus 的 2 个 16 位无符号整数(高低字转换)
|
|
||||||
/// </summary>
|
|
||||||
protected ushort[] Int32ToUshorts(int value)
|
|
||||||
{
|
|
||||||
// 本处采用低字在前(CDAB),如果PLC高字在前(ABCD),可将返回数组顺序颠倒
|
|
||||||
ushort lowWord = (ushort)(value & 0xFFFF);
|
|
||||||
ushort highWord = (ushort)((value >> 16) & 0xFFFF);
|
|
||||||
return new ushort[] { lowWord, highWord };
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
using DeviceCommand.Base;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.IO.Ports;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BenchMovementModule.HardwareDrive
|
|
||||||
{
|
|
||||||
public class GantryControlRtu : GantryControlBase
|
|
||||||
{
|
|
||||||
// 暴露出底层的 ModbusRtu 实例以便修改串口配置
|
|
||||||
public ModbusRtu RtuDevice => (ModbusRtu)_device;
|
|
||||||
|
|
||||||
public GantryControlRtu(string portName, int baudRate = 9600, byte slaveAddress = 1)
|
|
||||||
: base(new ModbusRtu(), slaveAddress)
|
|
||||||
{
|
|
||||||
RtuDevice.ConfigureDevice(portName, baudRate);
|
|
||||||
}
|
|
||||||
|
|
||||||
public GantryControlRtu(string portName, int baudRate, int dataBits, StopBits stopBits, Parity parity, byte slaveAddress = 1)
|
|
||||||
: base(new ModbusRtu(), slaveAddress)
|
|
||||||
{
|
|
||||||
RtuDevice.ConfigureDevice(portName, baudRate, dataBits, stopBits, parity);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
using DeviceCommand.Base;
|
|
||||||
using Model.Models;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BenchMovementModule.HardwareDrive
|
|
||||||
{
|
|
||||||
public class GantryControlTcp : GantryControlBase
|
|
||||||
{
|
|
||||||
// 暴露出底层的 ModbusTcp 实例以便修改网络配置
|
|
||||||
public ModbusTcp TcpDevice => (ModbusTcp)_device;
|
|
||||||
|
|
||||||
public GantryControlTcp(TcpConfig config, byte slaveAddress = 1)
|
|
||||||
: base(new ModbusTcp(config), slaveAddress)
|
|
||||||
{
|
|
||||||
}
|
|
||||||
|
|
||||||
public GantryControlTcp(string ipAddress, int port = 502, byte slaveAddress = 1)
|
|
||||||
: base(new ModbusTcp(), slaveAddress)
|
|
||||||
{
|
|
||||||
TcpDevice.ConfigureDevice(ipAddress, port);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -221,8 +221,8 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetAxisTargetPositionAsync(1, TargetX);
|
await _gantry!.设置轴目标位置(1, TargetX);
|
||||||
await _gantry.StartSingleAxisAsync(1);
|
await _gantry.启动单轴(1);
|
||||||
LoggerHelper.Info($"[BenchMovement] X轴移动到 {TargetX}");
|
LoggerHelper.Info($"[BenchMovement] X轴移动到 {TargetX}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -237,8 +237,8 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetAxisTargetPositionAsync(2, TargetY);
|
await _gantry!.设置轴目标位置(2, TargetY);
|
||||||
await _gantry.StartSingleAxisAsync(2);
|
await _gantry.启动单轴(2);
|
||||||
LoggerHelper.Info($"[BenchMovement] Y轴移动到 {TargetY}");
|
LoggerHelper.Info($"[BenchMovement] Y轴移动到 {TargetY}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -253,8 +253,8 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetAxisTargetPositionAsync(3, TargetZ);
|
await _gantry!.设置轴目标位置(3, TargetZ);
|
||||||
await _gantry.StartSingleAxisAsync(3);
|
await _gantry.启动单轴(3);
|
||||||
LoggerHelper.Info($"[BenchMovement] Z轴移动到 {TargetZ}");
|
LoggerHelper.Info($"[BenchMovement] Z轴移动到 {TargetZ}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -269,10 +269,10 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetAxisTargetPositionAsync(1, TargetX);
|
await _gantry!.设置轴目标位置(1, TargetX);
|
||||||
await _gantry.SetAxisTargetPositionAsync(2, TargetY);
|
await _gantry.设置轴目标位置(2, TargetY);
|
||||||
await _gantry.SetAxisTargetPositionAsync(3, TargetZ);
|
await _gantry.设置轴目标位置(3, TargetZ);
|
||||||
await _gantry.StartAllAxesAsync();
|
await _gantry.启动全部轴();
|
||||||
LoggerHelper.Info($"[BenchMovement] 三轴同时移动: X={TargetX}, Y={TargetY}, Z={TargetZ}");
|
LoggerHelper.Info($"[BenchMovement] 三轴同时移动: X={TargetX}, Y={TargetY}, Z={TargetZ}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -287,7 +287,7 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.HomeAsync();
|
await _gantry!.回零(15);
|
||||||
LoggerHelper.Info("[BenchMovement] 回原点已触发");
|
LoggerHelper.Info("[BenchMovement] 回原点已触发");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -302,7 +302,7 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetEStopStateAsync(true);
|
await _gantry!.设置急停状态(true);
|
||||||
LoggerHelper.Info("[BenchMovement] 急停已触发");
|
LoggerHelper.Info("[BenchMovement] 急停已触发");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -316,9 +316,9 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetStopStateAsync(1, true);
|
await _gantry!.设置轴停止状态(1, true);
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
await _gantry.SetStopStateAsync(1, false);
|
await _gantry.设置轴停止状态(1, false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -331,9 +331,9 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetStopStateAsync(2, true);
|
await _gantry!.设置轴停止状态(2, true);
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
await _gantry.SetStopStateAsync(2, false);
|
await _gantry.设置轴停止状态(2, false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -346,9 +346,9 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetStopStateAsync(3, true);
|
await _gantry!.设置轴停止状态(3, true);
|
||||||
await Task.Delay(100);
|
await Task.Delay(100);
|
||||||
await _gantry.SetStopStateAsync(3, false);
|
await _gantry.设置轴停止状态(3, false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -361,10 +361,10 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetAxisSpeedAsync(1, SpeedX);
|
await _gantry!.设置轴速度(1, SpeedX);
|
||||||
await _gantry.SetAxisSpeedAsync(2, SpeedY);
|
await _gantry.设置轴速度(2, SpeedY);
|
||||||
await _gantry.SetAxisSpeedAsync(3, SpeedZ);
|
await _gantry.设置轴速度(3, SpeedZ);
|
||||||
await _gantry.SetHomePositionSpeedAsync(HomeSpeed);
|
await _gantry.设置回零速度(HomeSpeed);
|
||||||
LoggerHelper.Info($"[BenchMovement] 速度设定完成: X={SpeedX}, Y={SpeedY}, Z={SpeedZ}, 回零={HomeSpeed}");
|
LoggerHelper.Info($"[BenchMovement] 速度设定完成: X={SpeedX}, Y={SpeedY}, Z={SpeedZ}, 回零={HomeSpeed}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -379,7 +379,7 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (!EnsureConnected()) return;
|
if (!EnsureConnected()) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await _gantry!.SetHomePositionAsync(HomeX, HomeY, HomeZ);
|
await _gantry!.设置原点位置(HomeX, HomeY, HomeZ);
|
||||||
LoggerHelper.Info($"[BenchMovement] 零点设定完成: X={HomeX}, Y={HomeY}, Z={HomeZ}");
|
LoggerHelper.Info($"[BenchMovement] 零点设定完成: X={HomeX}, Y={HomeY}, Z={HomeZ}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -419,16 +419,16 @@ namespace BenchMovementModule.ViewModels
|
|||||||
if (_gantry == null || !_gantry.IsConnected) return;
|
if (_gantry == null || !_gantry.IsConnected) return;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var (x, y, z) = await _gantry.GetHomePositionAsync();
|
var (x, y, z) = await _gantry.查询原点位置();
|
||||||
HomeX = x; HomeY = y; HomeZ = z;
|
HomeX = x; HomeY = y; HomeZ = z;
|
||||||
|
|
||||||
SpeedX = await _gantry.GetAxisSpeedAsync(1);
|
SpeedX = await _gantry.查询轴速度(1);
|
||||||
SpeedY = await _gantry.GetAxisSpeedAsync(2);
|
SpeedY = await _gantry.查询轴速度(2);
|
||||||
SpeedZ = await _gantry.GetAxisSpeedAsync(3);
|
SpeedZ = await _gantry.查询轴速度(3);
|
||||||
HomeSpeed = await _gantry.GetHomePositionSpeedAsync();
|
HomeSpeed = await _gantry.查询回零速度();
|
||||||
|
|
||||||
// 读取 6 轴绝对位置
|
// 读取 6 轴绝对位置
|
||||||
var positions = await _gantry.GetAllAbsolutePositionsAsync();
|
var positions = await _gantry.查询全部轴绝对位置();
|
||||||
AbsPos1 = positions.Axis1; AbsPos2 = positions.Axis2; AbsPos3 = positions.Axis3;
|
AbsPos1 = positions.Axis1; AbsPos2 = positions.Axis2; AbsPos3 = positions.Axis3;
|
||||||
AbsPos4 = positions.Axis4; AbsPos5 = positions.Axis5; AbsPos6 = positions.Axis6;
|
AbsPos4 = positions.Axis4; AbsPos5 = positions.Axis5; AbsPos6 = positions.Axis6;
|
||||||
}
|
}
|
||||||
@@ -444,7 +444,7 @@ namespace BenchMovementModule.ViewModels
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
// 批量读取 6 轴绝对位置(一次性通信,高效)
|
// 批量读取 6 轴绝对位置(一次性通信,高效)
|
||||||
var pos = await _gantry.GetAllAbsolutePositionsAsync();
|
var pos = await _gantry.查询全部轴绝对位置();
|
||||||
AbsPos1 = pos.Axis1; AbsPos2 = pos.Axis2; AbsPos3 = pos.Axis3;
|
AbsPos1 = pos.Axis1; AbsPos2 = pos.Axis2; AbsPos3 = pos.Axis3;
|
||||||
AbsPos4 = pos.Axis4; AbsPos5 = pos.Axis5; AbsPos6 = pos.Axis6;
|
AbsPos4 = pos.Axis4; AbsPos5 = pos.Axis5; AbsPos6 = pos.Axis6;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,6 +347,7 @@ namespace CANModule.ViewModels
|
|||||||
DeleteDcAutoLoadCommand = new DelegateCommand(DeleteDcAutoLoad);
|
DeleteDcAutoLoadCommand = new DelegateCommand(DeleteDcAutoLoad);
|
||||||
SendCustomMessageCommand = new DelegateCommand(SendCustomMessage);
|
SendCustomMessageCommand = new DelegateCommand(SendCustomMessage);
|
||||||
_canfd.OnDbcMessageDecoded += OnDbcMessageDecoded;
|
_canfd.OnDbcMessageDecoded += OnDbcMessageDecoded;
|
||||||
|
_canfd.OnRawMessageReceived += OnRawMessageReceived;
|
||||||
LoadCommand = new DelegateCommand(Load);
|
LoadCommand = new DelegateCommand(Load);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +369,7 @@ namespace CANModule.ViewModels
|
|||||||
if (_canfd != null)
|
if (_canfd != null)
|
||||||
{
|
{
|
||||||
_canfd.OnDbcMessageDecoded -= OnDbcMessageDecoded;
|
_canfd.OnDbcMessageDecoded -= OnDbcMessageDecoded;
|
||||||
|
_canfd.OnRawMessageReceived -= OnRawMessageReceived;
|
||||||
}
|
}
|
||||||
_refreshCts?.Cancel();
|
_refreshCts?.Cancel();
|
||||||
}
|
}
|
||||||
@@ -627,6 +629,36 @@ namespace CANModule.ViewModels
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnRawMessageReceived(uint channel, uint canId, byte[] data, ulong timestamp, bool isFD, byte dlcLength)
|
||||||
|
{
|
||||||
|
// 原始报文回调:不依赖 DBC,自定义报文也能收到
|
||||||
|
Application.Current.Dispatcher.BeginInvoke(() =>
|
||||||
|
{
|
||||||
|
// 提取实际 CAN ID(去掉扩展帧标志位)
|
||||||
|
int actualId = (int)(canId & 0x1FFFFFFF);
|
||||||
|
// 原始字节转为 [0x00, 0x01, ...] 格式,供监视表格直接显示
|
||||||
|
string hexData = 十六进制字符串转换(data);
|
||||||
|
var find = CanMessageList.FirstOrDefault(s => s.报文ID == actualId && s.通道 == channel);
|
||||||
|
if (find != null)
|
||||||
|
{
|
||||||
|
find.时间戳 = (ulong)DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||||
|
find.长度 = dlcLength;
|
||||||
|
find.数据 = hexData;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CanMessageList.Add(new CanMessageShowVM
|
||||||
|
{
|
||||||
|
通道 = (byte)channel,
|
||||||
|
报文ID = actualId,
|
||||||
|
时间戳 = (ulong)DateTimeOffset.Now.ToUnixTimeMilliseconds(),
|
||||||
|
长度 = dlcLength,
|
||||||
|
数据 = hexData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private string 十六进制字符串转换(System.Span<byte> s)
|
private string 十六进制字符串转换(System.Span<byte> s)
|
||||||
{
|
{
|
||||||
if (s == null || s.Length == 0)
|
if (s == null || s.Length == 0)
|
||||||
|
|||||||
@@ -79,16 +79,17 @@
|
|||||||
<!-- 监视选项 -->
|
<!-- 监视选项 -->
|
||||||
<TreeViewItem Header="监视">
|
<TreeViewItem Header="监视">
|
||||||
<DataGrid x:Name="表格"
|
<DataGrid x:Name="表格"
|
||||||
Width="550"
|
Width="Auto"
|
||||||
Height="350"
|
Height="350"
|
||||||
VirtualizingPanel.IsVirtualizing="True"
|
VirtualizingPanel.IsVirtualizing="True"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
AutoGenerateColumns="False"
|
AutoGenerateColumns="False"
|
||||||
ItemsSource="{Binding CanMessageList, Mode=OneWay}">
|
ItemsSource="{Binding CanMessageList, Mode=OneWay}">
|
||||||
<DataGrid.Columns>
|
<DataGrid.Columns>
|
||||||
<DataGridTextColumn Header="通道" Binding="{Binding 通道}" Width="90" />
|
<DataGridTextColumn Header="通道" Binding="{Binding 通道}" Width="60" />
|
||||||
<DataGridTextColumn Header="报文ID" Binding="{Binding 报文ID, Converter={StaticResource HexConverter}}" Width="150" />
|
<DataGridTextColumn Header="报文ID" Binding="{Binding 报文ID, Converter={StaticResource HexConverter}}" Width="110" />
|
||||||
<DataGridTextColumn Header="长度" Binding="{Binding 长度}" Width="90" />
|
<DataGridTextColumn Header="长度" Binding="{Binding 长度}" Width="50" />
|
||||||
|
<DataGridTextColumn Header="数据" Binding="{Binding 数据}" Width="Auto" />
|
||||||
<DataGridTextColumn Header="时间戳" Binding="{Binding 时间戳}" Width="Auto" />
|
<DataGridTextColumn Header="时间戳" Binding="{Binding 时间戳}" Width="Auto" />
|
||||||
</DataGrid.Columns>
|
</DataGrid.Columns>
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
using Common.Attributes;
|
||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Command
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 对话框命令:在测试流程中弹出提示窗口。
|
||||||
|
/// 命令库为纯 .NET 类库,不直接引用 WPF 程序集;
|
||||||
|
/// 实际的弹窗能力由宿主程序(ADP 外壳)在启动时注入到 <see cref="弹窗处理器"/> 委托中实现(依赖倒置)。
|
||||||
|
/// </summary>
|
||||||
|
[ADPCommand]
|
||||||
|
public static class CommandDialog
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 弹窗类型
|
||||||
|
/// </summary>
|
||||||
|
public enum DialogType
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 信息提示
|
||||||
|
/// </summary>
|
||||||
|
Info,
|
||||||
|
/// <summary>
|
||||||
|
/// 警告提示
|
||||||
|
/// </summary>
|
||||||
|
Warning,
|
||||||
|
/// <summary>
|
||||||
|
/// 错误提示
|
||||||
|
/// </summary>
|
||||||
|
Error
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 弹窗处理委托(参数依次为:弹窗类型、弹窗详细、是否阻塞、自动关闭秒数、取消令牌)。
|
||||||
|
/// 由宿主程序启动时注入实现;未注入时弹窗命令降级为仅输出日志,不会抛异常。
|
||||||
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
|
public static Func<DialogType, string, bool, float, CancellationToken, Task> 弹窗处理器;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 弹窗:在界面上显示一个提示对话框。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="弹窗类型">弹窗样式:Info 信息 / Warning 警告 / Error 错误</param>
|
||||||
|
/// <param name="弹窗详细">弹窗中显示的详细内容</param>
|
||||||
|
/// <param name="是否阻塞">true = 步骤暂停,等待用户关闭(或自动关闭)后才继续;false = 弹出后步骤立即继续</param>
|
||||||
|
/// <param name="自动关闭秒数">大于 0 时,弹窗在指定秒数后自动关闭;小于等于 0 时不自动关闭,需用户手动关闭</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
public static async Task 弹窗(DialogType 弹窗类型, string 弹窗详细, bool 是否阻塞, float 自动关闭秒数, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var handler = 弹窗处理器;
|
||||||
|
if (handler == null)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"[弹窗命令] 宿主未注入弹窗处理器,跳过弹窗:{弹窗详细}");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await handler(弹窗类型, 弹窗详细, 是否阻塞, 自动关闭秒数, ct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,12 +4,12 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="ncalc" Version="1.3.8" />
|
<PackageReference Include="ncalc" Version="1.3.8" />
|
||||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.5-beta1" />
|
<PackageReference Include="Newtonsoft.Json" Version="13.0.5-beta1" />
|
||||||
<PackageReference Include="System.IO.Ports" Version="11.0.0-preview.4.26230.115" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
@@ -23,7 +24,7 @@ namespace Common.Tools
|
|||||||
{
|
{
|
||||||
foreach (var kvp in processedVariables)
|
foreach (var kvp in processedVariables)
|
||||||
{
|
{
|
||||||
expr.Parameters[kvp.Key] = kvp.Value;
|
expr.Parameters[kvp.Key] = NormalizeValue(kvp.Value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -95,6 +96,29 @@ namespace Common.Tools
|
|||||||
return (processedExpression, newVariables);
|
return (processedExpression, newVariables);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 归一化变量取值:输入变量的值常以字符串形式保存(如 "10"),
|
||||||
|
/// 若直接参与比较,NCalc 会按字符串逐位比较("10" > "5" 为假),
|
||||||
|
/// 导致大于/小于判断失真。此处将可解析为数字/布尔的字符串转换为对应类型,
|
||||||
|
/// 使比较按数值语义进行;无法解析的字符串保持原样。
|
||||||
|
/// </summary>
|
||||||
|
private static object? NormalizeValue(object? value)
|
||||||
|
{
|
||||||
|
if (value is string s)
|
||||||
|
{
|
||||||
|
if (double.TryParse(s, NumberStyles.Float, CultureInfo.InvariantCulture, out var d)
|
||||||
|
|| double.TryParse(s, out d))
|
||||||
|
{
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
if (bool.TryParse(s, out var b))
|
||||||
|
{
|
||||||
|
return b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查字符串是否包含中文字符
|
// 检查字符串是否包含中文字符
|
||||||
private static bool ContainsChinese(string text)
|
private static bool ContainsChinese(string text)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ namespace DeviceCommand.Base
|
|||||||
{
|
{
|
||||||
public string IPAddress { get; private set; } = "127.0.0.1";
|
public string IPAddress { get; private set; } = "127.0.0.1";
|
||||||
public int Port { get; private set; } = 502;
|
public int Port { get; private set; } = 502;
|
||||||
public int SendTimeout { get; private set; } = 3000;
|
public int SendTimeout { get; private set; } = 10000;
|
||||||
public int ReceiveTimeout { get; private set; } = 3000;
|
public int ReceiveTimeout { get; private set; } = 10000;
|
||||||
|
|
||||||
private TcpClient _tcpClient;
|
private TcpClient _tcpClient;
|
||||||
public IModbusMaster Modbus { get; private set; }
|
public IModbusMaster Modbus { get; private set; }
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.ComponentModel;
|
||||||
|
|
||||||
namespace DeviceCommand.Devices
|
namespace DeviceCommand.Devices
|
||||||
{
|
{
|
||||||
@@ -18,6 +19,14 @@ namespace DeviceCommand.Devices
|
|||||||
// 向后兼容:TcpDevice 即自身(继承自 ModbusTcp)
|
// 向后兼容:TcpDevice 即自身(继承自 ModbusTcp)
|
||||||
public ModbusTcp TcpDevice => this;
|
public ModbusTcp TcpDevice => this;
|
||||||
|
|
||||||
|
// =================================================================
|
||||||
|
// ------------------ 软件位置跟踪(绝对位置寄存器会被清零)----------
|
||||||
|
// =================================================================
|
||||||
|
/// <summary>当前位置相对于机械零点的位置(每次移动后累加偏移)</summary>
|
||||||
|
private int _originX, _originY, _originZ;
|
||||||
|
/// <summary>是否已完成回零初始化</summary>
|
||||||
|
private bool _isHomed;
|
||||||
|
|
||||||
public GantryControlTcp(TcpConfig config) : base(config)
|
public GantryControlTcp(TcpConfig config) : base(config)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -26,7 +35,7 @@ namespace DeviceCommand.Devices
|
|||||||
// =================================================================
|
// =================================================================
|
||||||
// ------------------ Coil / Bit 状态与控制 (位元件) ------------------
|
// ------------------ Coil / Bit 状态与控制 (位元件) ------------------
|
||||||
// =================================================================
|
// =================================================================
|
||||||
private const ushort ADDR_M_ESTOP = 20496; // 0x5010: 紧停
|
private const ushort ADDR_M_ESTOP = 40; // 0x5010: 紧停
|
||||||
private const ushort ADDR_M_X_STOP = 41; // 0x29: X轴停止
|
private const ushort ADDR_M_X_STOP = 41; // 0x29: X轴停止
|
||||||
private const ushort ADDR_M_Y_STOP = 42; // 0x2A: Y轴停止
|
private const ushort ADDR_M_Y_STOP = 42; // 0x2A: Y轴停止
|
||||||
private const ushort ADDR_M_Z_STOP = 43; // 0x2B: Z轴停止
|
private const ushort ADDR_M_Z_STOP = 43; // 0x2B: Z轴停止
|
||||||
@@ -84,10 +93,12 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 轴使能控制
|
/// 轴使能控制
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="axis">轴编号 (1 - 6)</param>
|
/// <param name="轴编号">轴编号 (1 - 6)</param>
|
||||||
public async Task SetAxisEnableAsync(int axis, bool enable, CancellationToken ct = default)
|
/// <param name="使能">true: 使能轴, false: 禁用轴</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 设置轴使能(int 轴编号, bool 使能, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
ushort address = axis switch
|
ushort address = 轴编号 switch
|
||||||
{
|
{
|
||||||
1 => ADDR_HM_AXIS1_ENABLE,
|
1 => ADDR_HM_AXIS1_ENABLE,
|
||||||
2 => ADDR_HM_AXIS2_ENABLE,
|
2 => ADDR_HM_AXIS2_ENABLE,
|
||||||
@@ -95,60 +106,64 @@ namespace DeviceCommand.Devices
|
|||||||
4 => ADDR_HM_AXIS4_ENABLE,
|
4 => ADDR_HM_AXIS4_ENABLE,
|
||||||
5 => ADDR_HM_AXIS5_ENABLE,
|
5 => ADDR_HM_AXIS5_ENABLE,
|
||||||
6 => ADDR_HM_AXIS6_ENABLE,
|
6 => ADDR_HM_AXIS6_ENABLE,
|
||||||
_ => throw new ArgumentException("轴通道错误,仅支持 1 - 6", nameof(axis))
|
_ => throw new ArgumentException("轴通道错误,仅支持 1 - 6", nameof(轴编号))
|
||||||
};
|
};
|
||||||
|
|
||||||
await WriteSingleCoilAsync(_slaveAddress, address, enable, ct);
|
await WriteSingleCoilAsync(_slaveAddress, address, 使能, 取消令牌);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取指定轴的使能状态
|
/// 读取指定轴的使能状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="axis">1: X轴, 2: Y轴, 3: Z轴</param>
|
/// <param name="轴编号">1: X轴, 2: Y轴, 3: Z轴</param>
|
||||||
public async Task<bool> GetAxisEnableAsync(int axis, CancellationToken ct = default)
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
/// <returns>true: 已使能, false: 未使能</returns>
|
||||||
|
public async Task<bool> 查询轴使能(int 轴编号, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
ushort address = axis switch
|
ushort address = 轴编号 switch
|
||||||
{
|
{
|
||||||
1 => ADDR_HM_AXIS1_ENABLE,
|
1 => ADDR_HM_AXIS1_ENABLE,
|
||||||
2 => ADDR_HM_AXIS2_ENABLE,
|
2 => ADDR_HM_AXIS2_ENABLE,
|
||||||
3 => ADDR_HM_AXIS3_ENABLE,
|
3 => ADDR_HM_AXIS3_ENABLE,
|
||||||
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
|
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(轴编号))
|
||||||
};
|
};
|
||||||
|
|
||||||
bool[] coils = await ReadCoilsAsync(_slaveAddress, address, 1, ct);
|
bool[] coils = await ReadCoilsAsync(_slaveAddress, address, 1, 取消令牌);
|
||||||
return coils != null && coils.Length > 0 && coils[0];
|
return coils != null && coils.Length > 0 && coils[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 同时设置 X/Y/Z 轴的原点位置(带各自独立的范围检查)
|
/// 同时设置 X/Y/Z 轴的原点位置(带各自独立的范围检查)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="xPos">X轴原点位置 (范围: 0 ~ 50000)</param>
|
/// <param name="X轴原点">X轴原点位置 (范围: 0 ~ 50000)</param>
|
||||||
/// <param name="yPos">Y轴原点位置 (范围: 0 ~ 50000)</param>
|
/// <param name="Y轴原点">Y轴原点位置 (范围: 0 ~ 50000)</param>
|
||||||
/// <param name="zPos">Z轴原点位置 (范围: 0 ~ 20000)</param>
|
/// <param name="Z轴原点">Z轴原点位置 (范围: 0 ~ 40000)</param>
|
||||||
public async Task SetHomePositionAsync(int xPos, int yPos, int zPos, CancellationToken ct = default)
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 设置原点位置(int X轴原点, int Y轴原点, int Z轴原点, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
if (xPos < 0 || xPos > 50000)
|
if (X轴原点 < 0 || X轴原点 > 50000)
|
||||||
throw new ArgumentOutOfRangeException(nameof(xPos), $"X轴原点设置值 [{xPos}] 超出合法范围 (0 - 50000)");
|
throw new ArgumentOutOfRangeException(nameof(X轴原点), $"X轴原点设置值 [{X轴原点}] 超出合法范围 (0 - 50000)");
|
||||||
|
|
||||||
if (yPos < 0 || yPos > 50000)
|
if (Y轴原点 < 0 || Y轴原点 > 50000)
|
||||||
throw new ArgumentOutOfRangeException(nameof(yPos), $"Y轴原点设置值 [{yPos}] 超出合法范围 (0 - 50000)");
|
throw new ArgumentOutOfRangeException(nameof(Y轴原点), $"Y轴原点设置值 [{Y轴原点}] 超出合法范围 (0 - 50000)");
|
||||||
|
|
||||||
if (zPos < 0 || zPos > 20000)
|
if (Z轴原点 < 0 || Z轴原点 > 40000)
|
||||||
throw new ArgumentOutOfRangeException(nameof(zPos), $"Z轴原点设置值 [{zPos}] 超出合法范围 (0 - 20000)");
|
throw new ArgumentOutOfRangeException(nameof(Z轴原点), $"Z轴原点设置值 [{Z轴原点}] 超出合法范围 (0 - 40000)");
|
||||||
|
|
||||||
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_HOME_POS, Int32ToUshorts(xPos), ct);
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_HOME_POS, Int32ToUshorts(X轴原点), 取消令牌);
|
||||||
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_HOME_POS, Int32ToUshorts(yPos), ct);
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_HOME_POS, Int32ToUshorts(Y轴原点), 取消令牌);
|
||||||
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_HOME_POS, Int32ToUshorts(zPos), ct);
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_HOME_POS, Int32ToUshorts(Z轴原点), 取消令牌);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 批量查询当前 X、Y、Z 三轴设定的原点位置值
|
/// 批量查询当前 X、Y、Z 三轴设定的原点位置值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
/// <returns>返回包含三轴原点位置的元组 (X, Y, Z)</returns>
|
/// <returns>返回包含三轴原点位置的元组 (X, Y, Z)</returns>
|
||||||
public async Task<(int X, int Y, int Z)> GetHomePositionAsync(CancellationToken ct = default)
|
public async Task<(int X, int Y, int Z)> 查询原点位置(CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
// 41218(Y) - 41198(X) = 20,加Y轴自身的 2 个寄存器,共批量读取 22 个连续寄存器
|
// 41218(Y) - 41198(X) = 20,加Y轴自身的 2 个寄存器,共批量读取 22 个连续寄存器
|
||||||
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_HD_X_HOME_POS, 22, ct);
|
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_HD_X_HOME_POS, 22, 取消令牌);
|
||||||
|
|
||||||
if (registers == null || registers.Length < 22)
|
if (registers == null || registers.Length < 22)
|
||||||
throw new InvalidOperationException("从 PLC 读取三轴原点位置失败,返回数据长度不足。");
|
throw new InvalidOperationException("从 PLC 读取三轴原点位置失败,返回数据长度不足。");
|
||||||
@@ -163,20 +178,24 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置回原点定位速度(带 0 - 100000 范围检查)
|
/// 设置回原点定位速度(带 0 - 100000 范围检查)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task SetHomePositionSpeedAsync(int speed, CancellationToken ct = default)
|
/// <param name="速度">回原点定位速度 (范围: 0 - 100000)</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 设置回零速度(int 速度, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
if (speed < 0 || speed > 100000)
|
if (速度 < 0 || 速度 > 100000)
|
||||||
throw new ArgumentOutOfRangeException(nameof(speed), $"回原点定位速度值 [{speed}] 超出合法范围 (0 - 100000)");
|
throw new ArgumentOutOfRangeException(nameof(速度), $"回原点定位速度值 [{速度}] 超出合法范围 (0 - 100000)");
|
||||||
|
|
||||||
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_HOME_POS_SPEED, Int32ToUshorts(speed), ct);
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_HOME_POS_SPEED, Int32ToUshorts(速度), 取消令牌);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询当前设定的回原点定位速度
|
/// 查询当前设定的回原点定位速度
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<int> GetHomePositionSpeedAsync(CancellationToken ct = default)
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
/// <returns>当前回原点定位速度值</returns>
|
||||||
|
public async Task<int> 查询回零速度(CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_HD_HOME_POS_SPEED, 2, ct);
|
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_HD_HOME_POS_SPEED, 2, 取消令牌);
|
||||||
|
|
||||||
if (registers == null || registers.Length < 2)
|
if (registers == null || registers.Length < 2)
|
||||||
throw new InvalidOperationException("从 PLC 读取回原点定位速度失败,返回数据长度不足。");
|
throw new InvalidOperationException("从 PLC 读取回原点定位速度失败,返回数据长度不足。");
|
||||||
@@ -187,39 +206,44 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定轴的运行速度(带 0 - 100000 范围检查)
|
/// 设置指定轴的运行速度(带 0 - 100000 范围检查)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="axis">1: X轴, 2: Y轴, 3: Z轴</param>
|
/// <param name="轴编号">1: X轴, 2: Y轴, 3: Z轴</param>
|
||||||
public async Task SetAxisSpeedAsync(int axis, int speed, CancellationToken ct = default)
|
/// <param name="速度">运行速度 (范围: 0 - 100000)</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 设置轴速度(int 轴编号, int 速度, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
if (speed < 0 || speed > 100000)
|
if (速度 < 0 || 速度 > 100000)
|
||||||
throw new ArgumentOutOfRangeException(nameof(speed), $"轴速度设置值 [{speed}] 超出合法范围 (0 - 100000)");
|
throw new ArgumentOutOfRangeException(nameof(速度), $"轴速度设置值 [{速度}] 超出合法范围 (0 - 100000)");
|
||||||
|
|
||||||
ushort address = axis switch
|
ushort address = 轴编号 switch
|
||||||
{
|
{
|
||||||
1 => ADDR_HD_X_SPEED,
|
1 => ADDR_HD_X_SPEED,
|
||||||
2 => ADDR_HD_Y_SPEED,
|
2 => ADDR_HD_Y_SPEED,
|
||||||
3 => ADDR_HD_Z_SPEED,
|
3 => ADDR_HD_Z_SPEED,
|
||||||
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
|
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(轴编号))
|
||||||
};
|
};
|
||||||
|
|
||||||
await WriteMultipleRegistersAsync(_slaveAddress, address, Int32ToUshorts(speed), ct);
|
await WriteMultipleRegistersAsync(_slaveAddress, address, Int32ToUshorts(速度), 取消令牌);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取指定轴当前设定的运行速度
|
/// 读取指定轴当前设定的运行速度
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<int> GetAxisSpeedAsync(int axis, CancellationToken ct = default)
|
/// <param name="轴编号">1: X轴, 2: Y轴, 3: Z轴</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
/// <returns>当前设定的运行速度值</returns>
|
||||||
|
public async Task<int> 查询轴速度(int 轴编号, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
ushort address = axis switch
|
ushort address = 轴编号 switch
|
||||||
{
|
{
|
||||||
1 => ADDR_HD_X_SPEED,
|
1 => ADDR_HD_X_SPEED,
|
||||||
2 => ADDR_HD_Y_SPEED,
|
2 => ADDR_HD_Y_SPEED,
|
||||||
3 => ADDR_HD_Z_SPEED,
|
3 => ADDR_HD_Z_SPEED,
|
||||||
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
|
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(轴编号))
|
||||||
};
|
};
|
||||||
|
|
||||||
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, ct);
|
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, 取消令牌);
|
||||||
if (registers == null || registers.Length < 2)
|
if (registers == null || registers.Length < 2)
|
||||||
throw new InvalidOperationException($"从 PLC 读取轴 {axis} 速度失败。");
|
throw new InvalidOperationException($"从 PLC 读取轴 {轴编号} 速度失败。");
|
||||||
|
|
||||||
return UshortsToInt32(registers);
|
return UshortsToInt32(registers);
|
||||||
}
|
}
|
||||||
@@ -227,15 +251,18 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 写入指定轴的目标位置(动态安全校验:目标位置不能超过 [轴最大物理极限 - 当前轴原点设置值])
|
/// 写入指定轴的目标位置(动态安全校验:目标位置不能超过 [轴最大物理极限 - 当前轴原点设置值])
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task SetAxisTargetPositionAsync(int axis, int targetPos, CancellationToken ct = default)
|
/// <param name="轴编号">1: X轴, 2: Y轴, 3: Z轴</param>
|
||||||
|
/// <param name="目标位置">目标位置</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 设置轴目标位置(int 轴编号, int 目标位置, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
var (xHome, yHome, zHome) = await GetHomePositionAsync(ct);
|
var (xHome, yHome, zHome) = await 查询原点位置(取消令牌);
|
||||||
|
|
||||||
ushort targetAddress;
|
ushort targetAddress;
|
||||||
int maxLimit;
|
int maxLimit;
|
||||||
int currentHomePos;
|
int currentHomePos;
|
||||||
|
|
||||||
switch (axis)
|
switch (轴编号)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
targetAddress = ADDR_HD_X_TARGET;
|
targetAddress = ADDR_HD_X_TARGET;
|
||||||
@@ -249,43 +276,46 @@ namespace DeviceCommand.Devices
|
|||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
targetAddress = ADDR_HD_Z_TARGET;
|
targetAddress = ADDR_HD_Z_TARGET;
|
||||||
maxLimit = 20000;
|
maxLimit = 40000;
|
||||||
currentHomePos = zHome;
|
currentHomePos = zHome;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis));
|
throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(轴编号));
|
||||||
}
|
}
|
||||||
|
|
||||||
int allowedMaxLimit = maxLimit - currentHomePos;
|
int allowedMaxLimit = maxLimit - currentHomePos;
|
||||||
|
|
||||||
if (targetPos < 0 || targetPos > allowedMaxLimit)
|
if (目标位置 < 0 || 目标位置 > allowedMaxLimit)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException(
|
throw new ArgumentOutOfRangeException(
|
||||||
nameof(targetPos),
|
nameof(目标位置),
|
||||||
$"轴 {axis} 目标位置 [{targetPos}] 不合法!当前原点为 [{currentHomePos}],允许的有效写入范围为: 0 - {allowedMaxLimit}"
|
$"轴 {轴编号} 目标位置 [{目标位置}] 不合法!当前原点为 [{currentHomePos}],允许的有效写入范围为: 0 - {allowedMaxLimit}"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
await WriteMultipleRegistersAsync(_slaveAddress, targetAddress, Int32ToUshorts(targetPos), ct);
|
await WriteMultipleRegistersAsync(_slaveAddress, targetAddress, Int32ToUshorts(目标位置), 取消令牌);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取指定轴当前设定的目标指定位置
|
/// 读取指定轴当前设定的目标指定位置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<int> GetAxisTargetPositionAsync(int axis, CancellationToken ct = default)
|
/// <param name="轴编号">1: X轴, 2: Y轴, 3: Z轴</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
/// <returns>当前设定的目标位置值</returns>
|
||||||
|
public async Task<int> 查询轴目标位置(int 轴编号, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
ushort address = axis switch
|
ushort address = 轴编号 switch
|
||||||
{
|
{
|
||||||
1 => ADDR_HD_X_TARGET,
|
1 => ADDR_HD_X_TARGET,
|
||||||
2 => ADDR_HD_Y_TARGET,
|
2 => ADDR_HD_Y_TARGET,
|
||||||
3 => ADDR_HD_Z_TARGET,
|
3 => ADDR_HD_Z_TARGET,
|
||||||
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
|
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(轴编号))
|
||||||
};
|
};
|
||||||
|
|
||||||
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, ct);
|
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, 取消令牌);
|
||||||
|
|
||||||
if (registers == null || registers.Length < 2)
|
if (registers == null || registers.Length < 2)
|
||||||
throw new InvalidOperationException($"从 PLC 读取轴 {axis} 目标位置失败。");
|
throw new InvalidOperationException($"从 PLC 读取轴 {轴编号} 目标位置失败。");
|
||||||
|
|
||||||
return UshortsToInt32(registers);
|
return UshortsToInt32(registers);
|
||||||
}
|
}
|
||||||
@@ -293,53 +323,101 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置系统紧停锁定状态 (true: 锁定激活, false: 解除锁定)
|
/// 设置系统紧停锁定状态 (true: 锁定激活, false: 解除锁定)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task SetEStopStateAsync(bool state, CancellationToken ct = default)
|
/// <param name="状态">true: 锁定激活, false: 解除锁定</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 设置急停状态(bool 状态, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_ESTOP, state, ct);
|
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_ESTOP, 状态, 取消令牌);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定轴的停止控制状态 (true: 持续输出停止信号, false: 释放停止信号)
|
/// 设置指定轴的停止控制状态 (true: 持续输出停止信号, false: 释放停止信号)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task SetStopStateAsync(int axis, bool state, CancellationToken ct = default)
|
/// <param name="轴编号">1: X轴, 2: Y轴, 3: Z轴</param>
|
||||||
|
/// <param name="状态">true: 持续输出停止信号, false: 释放停止信号</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 设置轴停止状态(int 轴编号, bool 状态, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
ushort address = axis switch
|
ushort address = 轴编号 switch
|
||||||
{
|
{
|
||||||
1 => ADDR_M_X_STOP,
|
1 => ADDR_M_X_STOP,
|
||||||
2 => ADDR_M_Y_STOP,
|
2 => ADDR_M_Y_STOP,
|
||||||
3 => ADDR_M_Z_STOP,
|
3 => ADDR_M_Z_STOP,
|
||||||
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis))
|
_ => throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(轴编号))
|
||||||
};
|
};
|
||||||
|
|
||||||
await WriteSingleCoilAsync(_slaveAddress, address, state, ct);
|
await WriteSingleCoilAsync(_slaveAddress, address, 状态, 取消令牌);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 全局回零(安全校验版:先检查回原点速度是否为 0,为0则拒绝触发)
|
/// 全局回零(安全校验版:先检查回原点速度是否为 0,为0则拒绝触发)。
|
||||||
|
/// 回零完成后读取绝对位置寄存器保存为初始值,重置累计偏移。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task HomeAsync(CancellationToken ct = default)
|
/// <param name="回零点等待时间">回零点等待时间</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 回零(int 回零点等待时间, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
int currentSpeed = await GetHomePositionSpeedAsync(ct);
|
int currentSpeed = await 查询回零速度(取消令牌);
|
||||||
|
|
||||||
if (currentSpeed == 0)
|
if (currentSpeed == 0)
|
||||||
throw new InvalidOperationException("无法触发回原点!当前回原点定位速度为 0,请先设置合法的回零速度。");
|
throw new InvalidOperationException("无法触发回原点!当前回原点定位速度为 0,请先设置合法的回零速度。");
|
||||||
|
|
||||||
await TriggerCoilAsync(ADDR_M_HOME_TRIGGER, ct);
|
// 1. 触发回零
|
||||||
|
await TriggerCoilAsync(ADDR_M_HOME_TRIGGER, 取消令牌);
|
||||||
|
|
||||||
|
// 2. 等待到达机械零点(绝对位置 = 0)
|
||||||
|
await 等待到达零点(回零点等待时间, 取消令牌);
|
||||||
|
await Task.Delay(500, 取消令牌);
|
||||||
|
|
||||||
|
// 3. 清零绝对位置寄存器
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS1_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS2_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS3_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
|
||||||
|
// 4. 设置当前位置为机械零点 (0, 0, 0)
|
||||||
|
// 设置目标位置
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_TARGET, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_TARGET, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_TARGET, Int32ToUshorts(0), 取消令牌);
|
||||||
|
|
||||||
|
// 5. 读取配置的待定位置(原点位置)
|
||||||
|
var (xHome, yHome, zHome) = await 查询原点位置(取消令牌);
|
||||||
|
|
||||||
|
// 6. 移动到待定位置
|
||||||
|
if (xHome != 0 || yHome != 0 || zHome != 0)
|
||||||
|
{
|
||||||
|
|
||||||
|
_originX = xHome;
|
||||||
|
_originY = yHome;
|
||||||
|
_originZ = zHome;
|
||||||
|
|
||||||
|
// 启动三轴联动
|
||||||
|
await 启动全部轴(取消令牌);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8. 更新当前位置为待定位置
|
||||||
|
_originX = xHome;
|
||||||
|
_originY = yHome;
|
||||||
|
_originZ = zHome;
|
||||||
|
_isHomed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单轴指定位置启动(含速度判定、使能自动补齐、动作目标位置独立范围校验)
|
/// 单轴指定位置启动(含速度判定、使能自动补齐、动作目标位置独立范围校验)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="axis">1: X轴, 2: Y轴, 3: Z轴</param>
|
/// <param name="轴编号">1: X轴, 2: Y轴, 3: Z轴</param>
|
||||||
public async Task StartSingleAxisAsync(int axis, CancellationToken ct = default)
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 启动单轴(int 轴编号, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
ushort startAddress;
|
ushort startAddress;
|
||||||
int maxLimit;
|
int maxLimit;
|
||||||
int currentHomePos;
|
int currentHomePos;
|
||||||
|
|
||||||
// 1. 获取基础参数与校验边界
|
// 1. 获取基础参数与校验边界
|
||||||
var (xHome, yHome, zHome) = await GetHomePositionAsync(ct);
|
var (xHome, yHome, zHome) = await 查询原点位置(取消令牌);
|
||||||
switch (axis)
|
switch (轴编号)
|
||||||
{
|
{
|
||||||
case 1:
|
case 1:
|
||||||
startAddress = ADDR_M_X_START;
|
startAddress = ADDR_M_X_START;
|
||||||
@@ -353,53 +431,66 @@ namespace DeviceCommand.Devices
|
|||||||
break;
|
break;
|
||||||
case 3:
|
case 3:
|
||||||
startAddress = ADDR_M_Z_START;
|
startAddress = ADDR_M_Z_START;
|
||||||
maxLimit = 20000;
|
maxLimit = 40000;
|
||||||
currentHomePos = zHome;
|
currentHomePos = zHome;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(axis));
|
throw new ArgumentException("轴通道错误,仅支持 1(X), 2(Y), 3(Z)", nameof(轴编号));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 检查动作轴的速度
|
// 2. 检查动作轴的速度
|
||||||
int currentSpeed = await GetAxisSpeedAsync(axis, ct);
|
int currentSpeed = await 查询轴速度(轴编号, 取消令牌);
|
||||||
if (currentSpeed == 0)
|
if (currentSpeed == 0)
|
||||||
throw new InvalidOperationException($"轴 {axis} 无法启动!当前设定速度为 0。");
|
throw new InvalidOperationException($"轴 {轴编号} 无法启动!当前设定速度为 0。");
|
||||||
|
|
||||||
// 3. 检查动作轴使能,若为 false 则自动补写 true 激活驱动器
|
// 3. 批量检查 1~6 号轴使能状态,若任意轴未使能则全部补写 true
|
||||||
bool isEnabled = await GetAxisEnableAsync(axis, ct);
|
bool[] allEnableStates = await ReadCoilsAsync(_slaveAddress, ADDR_HM_AXIS1_ENABLE, 6, 取消令牌);
|
||||||
if (!isEnabled)
|
if (allEnableStates == null || allEnableStates.Length < 6)
|
||||||
|
throw new InvalidOperationException("从 PLC 读取 1~6 号轴使能状态失败。");
|
||||||
|
|
||||||
|
bool hasModifiedEnable = false;
|
||||||
|
ushort[] enableAddresses = { ADDR_HM_AXIS1_ENABLE, ADDR_HM_AXIS2_ENABLE, ADDR_HM_AXIS3_ENABLE, ADDR_HM_AXIS4_ENABLE, ADDR_HM_AXIS5_ENABLE, ADDR_HM_AXIS6_ENABLE };
|
||||||
|
|
||||||
|
for (int i = 0; i < 6; i++)
|
||||||
{
|
{
|
||||||
await SetAxisEnableAsync(axis, true, ct);
|
if (!allEnableStates[i])
|
||||||
await Task.Delay(100, ct); // 等待硬件继电器响应与伺服驱动就绪
|
{
|
||||||
|
await WriteSingleCoilAsync(_slaveAddress, enableAddresses[i], true, 取消令牌);
|
||||||
|
hasModifiedEnable = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasModifiedEnable)
|
||||||
|
await Task.Delay(100, 取消令牌); // 等待硬件继电器响应与伺服驱动就绪
|
||||||
|
|
||||||
// 4. 精准独立校验当前需要动作轴的目标定位是否越界
|
// 4. 精准独立校验当前需要动作轴的目标定位是否越界
|
||||||
int currentTarget = await GetAxisTargetPositionAsync(axis, ct);
|
int currentTarget = await 查询轴目标位置(轴编号, 取消令牌);
|
||||||
int allowedMaxLimit = maxLimit - currentHomePos;
|
int allowedMaxLimit = maxLimit - currentHomePos;
|
||||||
if (currentTarget < 0 || currentTarget > allowedMaxLimit)
|
if (currentTarget < 0 || currentTarget > allowedMaxLimit)
|
||||||
{
|
{
|
||||||
throw new ArgumentOutOfRangeException($"轴 {axis} 无法启动!当前指定目标位置 [{currentTarget}] 超过了允许的最大安全极限值 [{allowedMaxLimit}]。");
|
throw new ArgumentOutOfRangeException($"轴 {轴编号} 无法启动!当前指定目标位置 [{currentTarget}] 超过了允许的最大安全极限值 [{allowedMaxLimit}]。");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. 校验完成,边缘触发点动启动
|
// 5. 校验完成,边缘触发点动启动
|
||||||
await TriggerCoilAsync(startAddress, ct);
|
await TriggerCoilAsync(startAddress, 取消令牌);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 三轴同时启动(全开:严格校验全部运行轴速度、批量检测与补正1~6号轴使能、联动校验三轴目标范围极限)
|
/// 三轴同时启动(全开:严格校验全部运行轴速度、批量检测与补正1~6号轴使能、联动校验三轴目标范围极限)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task StartAllAxesAsync(CancellationToken ct = default)
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 启动全部轴(CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
// 1. 一次性读取三轴速度,严禁任何一轴速度为 0
|
// 1. 一次性读取三轴速度,严禁任何一轴速度为 0
|
||||||
int xSpeed = await GetAxisSpeedAsync(1, ct);
|
int xSpeed = await 查询轴速度(1, 取消令牌);
|
||||||
int ySpeed = await GetAxisSpeedAsync(2, ct);
|
int ySpeed = await 查询轴速度(2, 取消令牌);
|
||||||
int zSpeed = await GetAxisSpeedAsync(3, ct);
|
int zSpeed = await 查询轴速度(3, 取消令牌);
|
||||||
|
|
||||||
if (xSpeed == 0 || ySpeed == 0 || zSpeed == 0)
|
if (xSpeed == 0 || ySpeed == 0 || zSpeed == 0)
|
||||||
throw new InvalidOperationException($"无法全开启动!存在运行速度为 0 的轴。当前速度 -> X:{xSpeed}, Y:{ySpeed}, Z:{zSpeed}");
|
throw new InvalidOperationException($"无法全开启动!存在运行速度为 0 的轴。当前速度 -> X:{xSpeed}, Y:{ySpeed}, Z:{zSpeed}");
|
||||||
|
|
||||||
// 2. 批量读取 1 ~ 6 号轴连续的使能状态线圈,极大地优化网络性能
|
// 2. 批量读取 1 ~ 6 号轴连续的使能状态线圈,极大地优化网络性能
|
||||||
bool[] enableStates = await ReadCoilsAsync(_slaveAddress, ADDR_HM_AXIS1_ENABLE, 6, ct);
|
bool[] enableStates = await ReadCoilsAsync(_slaveAddress, ADDR_HM_AXIS1_ENABLE, 6, 取消令牌);
|
||||||
if (enableStates == null || enableStates.Length < 6)
|
if (enableStates == null || enableStates.Length < 6)
|
||||||
throw new InvalidOperationException("从 PLC 读取 1~6 号轴使能状态失败。");
|
throw new InvalidOperationException("从 PLC 读取 1~6 号轴使能状态失败。");
|
||||||
|
|
||||||
@@ -410,49 +501,94 @@ namespace DeviceCommand.Devices
|
|||||||
{
|
{
|
||||||
if (!enableStates[i])
|
if (!enableStates[i])
|
||||||
{
|
{
|
||||||
await WriteSingleCoilAsync(_slaveAddress, enableAddresses[i], true, ct);
|
await WriteSingleCoilAsync(_slaveAddress, enableAddresses[i], true, 取消令牌);
|
||||||
hasModifiedEnable = true;
|
hasModifiedEnable = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果执行过使能补齐补齐写入,在循环外进行统一延时,保证PLC时序安全
|
// 如果执行过使能补齐补齐写入,在循环外进行统一延时,保证PLC时序安全
|
||||||
if (hasModifiedEnable)
|
if (hasModifiedEnable)
|
||||||
await Task.Delay(100, ct);
|
await Task.Delay(100, 取消令牌);
|
||||||
|
|
||||||
// 3. 获取三轴原点及当前设定的目标指定位置
|
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_X_START, true, 取消令牌);
|
||||||
var (xHome, yHome, zHome) = await GetHomePositionAsync(ct);
|
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_Y_START, true, 取消令牌);
|
||||||
|
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_Z_START, true, 取消令牌);
|
||||||
|
|
||||||
int xTarget = await GetAxisTargetPositionAsync(1, ct);
|
await Task.Delay(100, 取消令牌); // 保持高电平状态以适配 PLC 扫描周期
|
||||||
int yTarget = await GetAxisTargetPositionAsync(2, ct);
|
|
||||||
int zTarget = await GetAxisTargetPositionAsync(3, ct);
|
|
||||||
|
|
||||||
// X轴安全范围联动校验
|
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_X_START, false, 取消令牌);
|
||||||
int xMaxAllowed = 50000 - xHome;
|
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_Y_START, false, 取消令牌);
|
||||||
if (xTarget < 0 || xTarget > xMaxAllowed)
|
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_Z_START, false, 取消令牌);
|
||||||
throw new ArgumentOutOfRangeException(nameof(xTarget), $"X轴目标位置 [{xTarget}] 越界,当前原点下最大范围 0-{xMaxAllowed}");
|
|
||||||
|
|
||||||
// Y轴安全范围联动校验
|
|
||||||
int yMaxAllowed = 50000 - yHome;
|
|
||||||
if (yTarget < 0 || yTarget > yMaxAllowed)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(yTarget), $"Y轴目标位置 [{yTarget}] 越界,当前原点下最大范围 0-{yMaxAllowed}");
|
|
||||||
|
|
||||||
// Z轴安全范围联动校验
|
|
||||||
int zMaxAllowed = 20000 - zHome;
|
|
||||||
if (zTarget < 0 || zTarget > zMaxAllowed)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(zTarget), $"Z轴目标位置 [{zTarget}] 越界,当前原点下最大范围 0-{zMaxAllowed}");
|
|
||||||
|
|
||||||
// 4. 全部联动校验通过,三轴点动同步启动
|
|
||||||
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_X_START, true, ct);
|
|
||||||
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_Y_START, true, ct);
|
|
||||||
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_Z_START, true, ct);
|
|
||||||
|
|
||||||
await Task.Delay(100, ct); // 保持高电平状态以适配 PLC 扫描周期
|
|
||||||
|
|
||||||
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_X_START, false, ct);
|
|
||||||
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_Y_START, false, ct);
|
|
||||||
await WriteSingleCoilAsync(_slaveAddress, ADDR_M_Z_START, false, ct);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 三轴相对移动:以(初始绝对值 + 累计偏移 + 本次偏移)做越界校验,
|
||||||
|
/// 清零绝对位置寄存器后将偏移量写入目标寄存器,启动三轴联动,
|
||||||
|
/// 等待到达后清零目标寄存器并更新累计偏移。偏移量可为负值。
|
||||||
|
/// 调用前必须先执行回零()完成初始化。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="X偏移量">X轴相对偏移量(可为负)</param>
|
||||||
|
/// <param name="Y偏移量">Y轴相对偏移量(可为负)</param>
|
||||||
|
/// <param name="Z偏移量">Z轴相对偏移量(可为负)</param>
|
||||||
|
/// <param name="超时秒数">等待到达目标位置的最大超时时间(秒)</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task 相对移动(int X偏移量, int Y偏移量, int Z偏移量, int 超时秒数 = 60, CancellationToken 取消令牌 = default)
|
||||||
|
{
|
||||||
|
if (!_isHomed)
|
||||||
|
throw new InvalidOperationException("尚未完成回零初始化,请先调用 回零() 方法。");
|
||||||
|
|
||||||
|
// 1. 越界校验:当前位置 + 本次偏移是否在合法范围
|
||||||
|
int targetAbsX = _originX + X偏移量;
|
||||||
|
int targetAbsY = _originY + Y偏移量;
|
||||||
|
int targetAbsZ = _originZ + Z偏移量;
|
||||||
|
|
||||||
|
if (targetAbsX > 50000)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(X偏移量),
|
||||||
|
$"X轴越界:当前 {_originX} + 偏移 {X偏移量} = {targetAbsX},最大范围 50000");
|
||||||
|
|
||||||
|
if (targetAbsY > 50000)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(Y偏移量),
|
||||||
|
$"Y轴越界:当前 {_originY} + 偏移 {Y偏移量} = {targetAbsY},最大范围 50000");
|
||||||
|
|
||||||
|
if (targetAbsZ > 40000)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(Z偏移量),
|
||||||
|
$"Z轴越界:当前 {_originZ} + 偏移 {Z偏移量} = {targetAbsZ},最大范围 40000");
|
||||||
|
|
||||||
|
// 2. 将偏移量写入目标位置寄存器(ABS已在上次移动后清零,目标 = 偏移量)
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_X_TARGET, Int32ToUshorts(X偏移量), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Y_TARGET, Int32ToUshorts(Y偏移量), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_HD_Z_TARGET, Int32ToUshorts(Z偏移量), 取消令牌);
|
||||||
|
|
||||||
|
// 3. 启动三轴联动
|
||||||
|
await 启动全部轴(取消令牌);
|
||||||
|
bool IsArrived= await 等待到达指定位置(超时秒数,取消令牌);
|
||||||
|
if (IsArrived)
|
||||||
|
{
|
||||||
|
// 5. 到达后更新当前位置(累加偏移)
|
||||||
|
_originX += X偏移量;
|
||||||
|
_originY += Y偏移量;
|
||||||
|
_originZ += Z偏移量;
|
||||||
|
await 清空绝对寄存器();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var pos = await 查询全部轴绝对位置();
|
||||||
|
_originX += pos.Axis1;
|
||||||
|
_originY += pos.Axis6;
|
||||||
|
_originZ += pos.Axis2;
|
||||||
|
await 清空绝对寄存器();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public async Task 清空绝对寄存器( CancellationToken 取消令牌 = default)
|
||||||
|
{
|
||||||
|
// 6. 清零 6 轴绝对位置寄存器
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS1_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS2_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS3_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS4_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS5_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
await WriteMultipleRegistersAsync(_slaveAddress, ADDR_ABS_AXIS6_POS, Int32ToUshorts(0), 取消令牌);
|
||||||
|
}
|
||||||
#region 内部高低字工具转化方法
|
#region 内部高低字工具转化方法
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -492,10 +628,12 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取指定轴的绝对位置(相对原点的实时位置,只读)
|
/// 读取指定轴的绝对位置(相对原点的实时位置,只读)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="axis">轴编号 (1-6)</param>
|
/// <param name="轴编号">轴编号 (1-6)</param>
|
||||||
public async Task<int> GetAbsolutePositionAsync(int axis, CancellationToken ct = default)
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
/// <returns>指定轴的绝对位置值</returns>
|
||||||
|
public async Task<int> 查询轴绝对位置(int 轴编号, CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
ushort address = axis switch
|
ushort address = 轴编号 switch
|
||||||
{
|
{
|
||||||
1 => ADDR_ABS_AXIS1_POS,
|
1 => ADDR_ABS_AXIS1_POS,
|
||||||
2 => ADDR_ABS_AXIS2_POS,
|
2 => ADDR_ABS_AXIS2_POS,
|
||||||
@@ -503,12 +641,12 @@ namespace DeviceCommand.Devices
|
|||||||
4 => ADDR_ABS_AXIS4_POS,
|
4 => ADDR_ABS_AXIS4_POS,
|
||||||
5 => ADDR_ABS_AXIS5_POS,
|
5 => ADDR_ABS_AXIS5_POS,
|
||||||
6 => ADDR_ABS_AXIS6_POS,
|
6 => ADDR_ABS_AXIS6_POS,
|
||||||
_ => throw new ArgumentException("轴编号错误,仅支持 1-6", nameof(axis))
|
_ => throw new ArgumentException("轴编号错误,仅支持 1-6", nameof(轴编号))
|
||||||
};
|
};
|
||||||
|
|
||||||
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, ct);
|
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, address, 2, 取消令牌);
|
||||||
if (registers == null || registers.Length < 2)
|
if (registers == null || registers.Length < 2)
|
||||||
throw new InvalidOperationException($"读取轴 {axis} 绝对位置失败。");
|
throw new InvalidOperationException($"读取轴 {轴编号} 绝对位置失败。");
|
||||||
|
|
||||||
return UshortsToInt32(registers);
|
return UshortsToInt32(registers);
|
||||||
}
|
}
|
||||||
@@ -516,11 +654,12 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 批量读取 1-6 轴的绝对位置(一次性读取 22 个寄存器,高效)
|
/// 批量读取 1-6 轴的绝对位置(一次性读取 22 个寄存器,高效)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
/// <returns>返回 6 轴绝对位置元组</returns>
|
/// <returns>返回 6 轴绝对位置元组</returns>
|
||||||
public async Task<(int Axis1, int Axis2, int Axis3, int Axis4, int Axis5, int Axis6)> GetAllAbsolutePositionsAsync(CancellationToken ct = default)
|
public async Task<(int Axis1, int Axis2, int Axis3, int Axis4, int Axis5, int Axis6)> 查询全部轴绝对位置(CancellationToken 取消令牌 = default)
|
||||||
{
|
{
|
||||||
// 从 47232 开始连续读取 22 个寄存器(覆盖 47232-47253)
|
// 从 47232 开始连续读取 22 个寄存器(覆盖 47232-47253)
|
||||||
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_ABS_AXIS1_POS, 22, ct);
|
ushort[] registers = await ReadHoldingRegistersAsync(_slaveAddress, ADDR_ABS_AXIS1_POS, 22, 取消令牌);
|
||||||
if (registers == null || registers.Length < 22)
|
if (registers == null || registers.Length < 22)
|
||||||
throw new InvalidOperationException("批量读取 6 轴绝对位置失败,返回数据长度不足。");
|
throw new InvalidOperationException("批量读取 6 轴绝对位置失败,返回数据长度不足。");
|
||||||
|
|
||||||
@@ -536,5 +675,63 @@ namespace DeviceCommand.Devices
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region 位置到达判定(超时轮询)
|
||||||
|
/// <summary>
|
||||||
|
/// 轮询等待三轴绝对位置均回到机械零点(容差 ±10),超时则抛出异常
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="超时秒数">最大等待时间(秒)</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
[Browsable(false)]
|
||||||
|
public async Task 等待到达零点(int 超时秒数, CancellationToken 取消令牌 = default)
|
||||||
|
{
|
||||||
|
var deadline = DateTime.Now.AddSeconds(超时秒数);
|
||||||
|
|
||||||
|
while (DateTime.Now < deadline)
|
||||||
|
{
|
||||||
|
取消令牌.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var pos = await 查询全部轴绝对位置(取消令牌);
|
||||||
|
if (Math.Abs(pos.Axis1) <= 10
|
||||||
|
&& Math.Abs(pos.Axis6) <= 10
|
||||||
|
&& Math.Abs(pos.Axis2) <= 10)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await Task.Delay(100, 取消令牌);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TimeoutException($"等待 {超时秒数} 秒后,三轴仍未到达机械零点");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 轮询等待三轴绝对位置均到达各自设定的目标位置(容差 ±10),超时则抛出异常
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="超时秒数">最大等待时间(秒)</param>
|
||||||
|
/// <param name="取消令牌">取消令牌</param>
|
||||||
|
public async Task<bool> 等待到达指定位置(int 超时秒数, CancellationToken 取消令牌 = default)
|
||||||
|
{
|
||||||
|
var targetX = await 查询轴目标位置(1, 取消令牌);
|
||||||
|
var targetY = await 查询轴目标位置(2, 取消令牌);
|
||||||
|
var targetZ = await 查询轴目标位置(3, 取消令牌);
|
||||||
|
|
||||||
|
var deadline = DateTime.Now.AddSeconds(超时秒数);
|
||||||
|
|
||||||
|
while (DateTime.Now < deadline)
|
||||||
|
{
|
||||||
|
取消令牌.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
var pos = await 查询全部轴绝对位置(取消令牌);
|
||||||
|
if (Math.Abs(pos.Axis1 - targetX) <= 10
|
||||||
|
&& Math.Abs(pos.Axis6 - targetY) <= 10
|
||||||
|
&& Math.Abs(pos.Axis2 - targetZ) <= 10)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
await Task.Delay(100, 取消令牌);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TimeoutException($"等待 {超时秒数} 秒后,三轴仍未到达指定位置 (X:{targetX}, Y:{targetY}, Z:{targetZ})");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
public async Task 写输出开关(byte 站号, ushort 开始地址, bool data)
|
public async Task 写输出开关(byte 站号, ushort 开始地址, bool data)
|
||||||
{
|
{
|
||||||
await Modbus.WriteSingleCoilAsync(站号, 开始地址, data);
|
bool[] mydata = { data };
|
||||||
|
await 批量写输出开关(站号, 开始地址, mydata);
|
||||||
}
|
}
|
||||||
public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas)
|
public async Task 批量写输出开关(byte 站号, ushort 开始地址, bool[] datas)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -52,37 +52,37 @@ namespace DeviceCommand.Devices
|
|||||||
{ 1, new Dictionary<功能动作, IoPortInfo> {
|
{ 1, new Dictionary<功能动作, IoPortInfo> {
|
||||||
{ 功能动作.单相充电, new IoPortInfo(1, 5) }, // Y6 -> 5
|
{ 功能动作.单相充电, new IoPortInfo(1, 5) }, // Y6 -> 5
|
||||||
{ 功能动作.抛负载, new IoPortInfo(1, 0) }, // Y1 -> 0
|
{ 功能动作.抛负载, new IoPortInfo(1, 0) }, // Y1 -> 0
|
||||||
{ 功能动作.短路测试, new IoPortInfo(1, 1) }, // Y2 -> 1
|
{ 功能动作.短路测试, new IoPortInfo(1, 4) }, // Y2 -> 1
|
||||||
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 15) } // Y9 -> 8
|
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 15) } // Y9 -> 8
|
||||||
}},
|
}},
|
||||||
{ 2, new Dictionary<功能动作, IoPortInfo> {
|
{ 2, new Dictionary<功能动作, IoPortInfo> {
|
||||||
{ 功能动作.单相充电, new IoPortInfo(1, 6) }, // Y7 -> 6
|
{ 功能动作.单相充电, new IoPortInfo(1, 6) }, // Y7 -> 6
|
||||||
{ 功能动作.抛负载, new IoPortInfo(1, 2) }, // Y3 -> 2
|
{ 功能动作.抛负载, new IoPortInfo(1, 1) }, // Y3 -> 2
|
||||||
{ 功能动作.短路测试, new IoPortInfo(1, 1) }, // Y2 -> 1
|
{ 功能动作.短路测试, new IoPortInfo(1, 4) }, // Y2 -> 1
|
||||||
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 14) } // Y10 -> 9
|
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 14) } // Y10 -> 9
|
||||||
}},
|
}},
|
||||||
{ 3, new Dictionary<功能动作, IoPortInfo> {
|
{ 3, new Dictionary<功能动作, IoPortInfo> {
|
||||||
{ 功能动作.单相充电, new IoPortInfo(1, 7) }, // Y8 -> 7
|
{ 功能动作.单相充电, new IoPortInfo(1, 7) }, // Y8 -> 7
|
||||||
{ 功能动作.抛负载, new IoPortInfo(1, 3) }, // Y4 -> 3
|
{ 功能动作.抛负载, new IoPortInfo(1, 2) }, // Y4 -> 3
|
||||||
{ 功能动作.短路测试, new IoPortInfo(1, 1) }, // Y2 -> 1
|
{ 功能动作.短路测试, new IoPortInfo(1, 4) }, // Y2 -> 1
|
||||||
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 13) } // Y11 -> 10
|
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 13) } // Y11 -> 10
|
||||||
}},
|
}},
|
||||||
{ 4, new Dictionary<功能动作, IoPortInfo> {
|
{ 4, new Dictionary<功能动作, IoPortInfo> {
|
||||||
{ 功能动作.单相充电, new IoPortInfo(1, 8) }, // Y9 -> 8
|
{ 功能动作.单相充电, new IoPortInfo(1, 8) }, // Y9 -> 8
|
||||||
{ 功能动作.抛负载, new IoPortInfo(1, 4) }, // Y5 -> 4
|
{ 功能动作.抛负载, new IoPortInfo(1, 3) }, // Y5 -> 4
|
||||||
{ 功能动作.短路测试, new IoPortInfo(1, 1) }, // Y2 -> 1
|
{ 功能动作.短路测试, new IoPortInfo(1, 4) }, // Y2 -> 1
|
||||||
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 12) } // Y12 -> 11
|
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 12) } // Y12 -> 11
|
||||||
}},
|
}},
|
||||||
{ 5, new Dictionary<功能动作, IoPortInfo> {
|
{ 5, new Dictionary<功能动作, IoPortInfo> {
|
||||||
{ 功能动作.单相充电, new IoPortInfo(2, 4) }, // Y5 -> 4
|
{ 功能动作.单相充电, new IoPortInfo(2, 4) }, // Y5 -> 4
|
||||||
{ 功能动作.抛负载, new IoPortInfo(2, 0) }, // Y1 -> 0
|
{ 功能动作.抛负载, new IoPortInfo(2, 0) }, // Y1 -> 0
|
||||||
{ 功能动作.短路测试, new IoPortInfo(1, 1) }, // Y2 -> 1
|
{ 功能动作.短路测试, new IoPortInfo(1, 4) }, // Y2 -> 1
|
||||||
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 11) } // Y13 -> 12
|
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 11) } // Y13 -> 12
|
||||||
}},
|
}},
|
||||||
{ 6, new Dictionary<功能动作, IoPortInfo> {
|
{ 6, new Dictionary<功能动作, IoPortInfo> {
|
||||||
{ 功能动作.单相充电, new IoPortInfo(2, 5) }, // Y6 -> 5
|
{ 功能动作.单相充电, new IoPortInfo(2, 5) }, // Y6 -> 5
|
||||||
{ 功能动作.抛负载, new IoPortInfo(2, 1) }, // Y2 -> 1
|
{ 功能动作.抛负载, new IoPortInfo(2, 1) }, // Y2 -> 1
|
||||||
{ 功能动作.短路测试, new IoPortInfo(1, 1) }, // Y2 -> 1
|
{ 功能动作.短路测试, new IoPortInfo(1,4) }, // Y2 -> 1
|
||||||
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 10) } // Y14 -> 13
|
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 10) } // Y14 -> 13
|
||||||
}},
|
}},
|
||||||
{ 7, new Dictionary<功能动作, IoPortInfo> {
|
{ 7, new Dictionary<功能动作, IoPortInfo> {
|
||||||
@@ -94,7 +94,7 @@ namespace DeviceCommand.Devices
|
|||||||
{ 8, new Dictionary<功能动作, IoPortInfo> {
|
{ 8, new Dictionary<功能动作, IoPortInfo> {
|
||||||
{ 功能动作.单相充电, new IoPortInfo(2, 7) }, // Y8 -> 7
|
{ 功能动作.单相充电, new IoPortInfo(2, 7) }, // Y8 -> 7
|
||||||
{ 功能动作.抛负载, new IoPortInfo(2, 3) }, // Y4 -> 3
|
{ 功能动作.抛负载, new IoPortInfo(2, 3) }, // Y4 -> 3
|
||||||
{ 功能动作.短路测试, new IoPortInfo(1, 1) }, // Y2 -> 1
|
{ 功能动作.短路测试, new IoPortInfo(1, 4) }, // Y2 -> 1
|
||||||
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 8) } // Y16 -> 15
|
{ 功能动作.蜂鸣器报警, new IoPortInfo(2, 8) } // Y16 -> 15
|
||||||
}}
|
}}
|
||||||
};
|
};
|
||||||
@@ -141,7 +141,17 @@ namespace DeviceCommand.Devices
|
|||||||
IOBoard 目标板卡 = GetBoardInstance(io信息.ModuleIndex);
|
IOBoard 目标板卡 = GetBoardInstance(io信息.ModuleIndex);
|
||||||
await 目标板卡.写输出开关(_slaveAddress, io信息.Address, 开关状态);
|
await 目标板卡.写输出开关(_slaveAddress, io信息.Address, 开关状态);
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 执行指定台架的功能动作控制(带高低电平开关及底层安全互锁)
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="板卡序号">板卡序号 (0 - 1)</param>
|
||||||
|
/// <param name="开始地址">寄存器初始地址(0-15)</param>
|
||||||
|
/// <param name="数据">开关状态数组 (true: 开启吸合, false: 关闭断开)[true,true,false]</param>
|
||||||
|
/// <param name="取消令牌">异步取消令牌</param>
|
||||||
|
public async Task 批量写输出开关(int 板卡序号,byte 站号, ushort 开始地址, bool[] 数据, CancellationToken 取消令牌 = default)
|
||||||
|
{
|
||||||
|
await GetBoardInstance(板卡序号).批量写输出开关(站号, 开始地址, 数据);
|
||||||
|
}
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 内部路由辅助方法:根据板卡索引安全获取当前连接的实例对象
|
/// 内部路由辅助方法:根据板卡索引安全获取当前连接的实例对象
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -38,6 +38,21 @@ namespace DeviceCommand.Devices
|
|||||||
CURRent
|
CURRent
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设备相位/输出模式[cite: 2]
|
||||||
|
/// </summary>
|
||||||
|
public enum DevicePhaseMode
|
||||||
|
{
|
||||||
|
/// <summary> 单相模式 </summary>
|
||||||
|
ONE,
|
||||||
|
/// <summary> 三相模式 </summary>
|
||||||
|
THRee,
|
||||||
|
/// <summary> 反相模式 </summary>
|
||||||
|
DIFFerence,
|
||||||
|
/// <summary> 多通道模式 </summary>
|
||||||
|
MULTichannel
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
[ADPCommand]
|
[ADPCommand]
|
||||||
@@ -52,21 +67,39 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
#region 1. IEEE 488.2 公共命令
|
#region 1. IEEE 488.2 公共命令
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除错误队列和状态字节
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
|
public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*CLS{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($"*CLS{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询设备标识
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>设备标识字符串(制造商, 产品型号, 序列号, 固件版本)</returns>
|
||||||
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重置设备
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 重置设备(CancellationToken ct = default)
|
public virtual async Task 重置设备(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*RST{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($"*RST{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 读取状态字节
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>状态字节寄存器值</returns>
|
||||||
public virtual async Task<string> 读取状态字节(CancellationToken ct = default)
|
public virtual async Task<string> 读取状态字节(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
@@ -79,6 +112,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置电源输出源的电气模式/工作耦合模式 (AC, DC, ACDC, DCAC)[cite: 1]
|
/// 设置电源输出源的电气模式/工作耦合模式 (AC, DC, ACDC, DCAC)[cite: 1]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="模式">电气耦合模式 (AC/DC/ACDC/DCAC)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电源模式(PowerCouplingMode 模式, CancellationToken ct = default)
|
public virtual async Task 设置电源模式(PowerCouplingMode 模式, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":SOURce:FUNCtion {模式}{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($":SOURce:FUNCtion {模式}{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
@@ -87,17 +122,29 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询电源当前的工作电气模式[cite: 1]
|
/// 查询电源当前的工作电气模式[cite: 1]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>当前工作电气模式字符串</returns>
|
||||||
public virtual async Task<string> 查询电源模式(CancellationToken ct = default)
|
public virtual async Task<string> 查询电源模式(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":SOURce:FUNCtion?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($":SOURce:FUNCtion?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置DC输出开关
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="开启">true: 开启输出, false: 关闭输出</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string 参数 = 开启 ? "ON" : "OFF"; //[cite: 1]
|
string 参数 = 开启 ? "ON" : "OFF"; //[cite: 1]
|
||||||
await SendAsync($":OUTPut {参数}{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($":OUTPut {参数}{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询DC输出状态
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>输出状态字符串(ON 或 OFF)</returns>
|
||||||
public virtual async Task<string> 查询DC输出状态(CancellationToken ct = default)
|
public virtual async Task<string> 查询DC输出状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":OUTPut?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($":OUTPut?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
@@ -107,24 +154,44 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
#region 3. 设定输出参数设置 (AC 电压/频率/DC 电压)
|
#region 3. 设定输出参数设置 (AC 电压/频率/DC 电压)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置交流电压
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="电压">交流电压设定值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置交流电压(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置交流电压(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置直流电压
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="电压">直流电压设定值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置直流电压(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置直流电压(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:DC {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:DC {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置频率
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="频率">频率设定值 (单位: Hz)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置频率(double 频率, CancellationToken ct = default)
|
public virtual async Task 设置频率(double 频率, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:FREQuency {0:F2}{1}", 频率, ScpiDelimiter); //[cite: 1]
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:FREQuency {0:F2}{1}", 频率, ScpiDelimiter); //[cite: 1]
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置电流
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="电流">电流设定值 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:CURRent {0:F3}{1}", 电流, ScpiDelimiter); //[cite: 1]
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:CURRent {0:F3}{1}", 电流, ScpiDelimiter); //[cite: 1]
|
||||||
@@ -135,70 +202,132 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
#region 4. 测量与数据回测(高频数据轮询核心)
|
#region 4. 测量与数据回测(高频数据轮询核心)
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询实际电压
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实际电压测量值 (单位: V)</returns>
|
||||||
[Monitorable("交流电源电压")]
|
[Monitorable("交流电源电压")]
|
||||||
public virtual async Task<string> 查询实际电压(CancellationToken ct = default)
|
public virtual async Task<double> 查询实际电压(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
string response = await WriteReadAsync($":MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询实际电流
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实际电流测量值 (单位: A)</returns>
|
||||||
[Monitorable("交流电源电流")]
|
[Monitorable("交流电源电流")]
|
||||||
public virtual async Task<string> 查询实际电流(CancellationToken ct = default)
|
public virtual async Task<double> 查询实际电流(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
string response = await WriteReadAsync($":MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询实际功率
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实际有功功率测量值 (单位: W)</returns>
|
||||||
[Monitorable("交流电源功率")]
|
[Monitorable("交流电源功率")]
|
||||||
public virtual async Task<string> 查询实际功率(CancellationToken ct = default)
|
public virtual async Task<double> 查询实际功率(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
string response = await WriteReadAsync($":MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<string> 查询视在功率(CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 查询视在功率
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>视在功率测量值 (单位: VA)</returns>
|
||||||
|
public virtual async Task<double> 查询视在功率(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":MEASure:POWer:APParent?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
string response = await WriteReadAsync($":MEASure:POWer:APParent?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<string> 查询实际频率(CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 查询实际频率
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实际频率测量值 (单位: Hz)</returns>
|
||||||
|
public virtual async Task<double> 查询实际频率(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":MEASure:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
string response = await WriteReadAsync($":MEASure:FREQuency?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual async Task<string> 查询功率因数(CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 查询功率因数
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>功率因数测量值</returns>
|
||||||
|
public virtual async Task<double> 查询功率因数(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":MEASure:POWer:PFACtor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
string response = await WriteReadAsync($":MEASure:POWer:PFACtor?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 5. 保护参数设置与系统监控
|
#region 5. 保护参数设置与系统监控
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置过流保护_OCP
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="电流">过流保护阈值 (单位: A, RMS值)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置过流保护_OCP(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置过流保护_OCP(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:CURRent:PROTection:RMS {0:F3}{1}", 电流, ScpiDelimiter); //[cite: 1]
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:CURRent:PROTection:RMS {0:F3}{1}", 电流, ScpiDelimiter); //[cite: 1]
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置过压保护_OVP
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="电压">过压保护阈值 (单位: V, 峰值)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置过压保护_OVP(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置过压保护_OVP(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:PROTection:PEAK {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:PROTection:PEAK {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询错误信息
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>错误代码和描述信息</returns>
|
||||||
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换远程控制模式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 切换远程控制模式(CancellationToken ct = default)
|
public virtual async Task 切换远程控制模式(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":SYSTem:REMote{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($":SYSTem:REMote{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换本地控制模式
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 切换本地控制模式(CancellationToken ct = default)
|
public virtual async Task 切换本地控制模式(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":SYSTem:LOCal{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($":SYSTem:LOCal{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清除保护告警
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除保护告警(CancellationToken ct = default)
|
public virtual async Task 清除保护告警(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":OUTPut:PROTection:CLEar{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($":OUTPut:PROTection:CLEar{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
@@ -211,6 +340,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清除最近的安时(Ah)与瓦时(Wh)统计数据[cite: 1]
|
/// 清除最近的安时(Ah)与瓦时(Wh)统计数据[cite: 1]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除充放电电量统计(CancellationToken ct = default)
|
public virtual async Task 清除充放电电量统计(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"SENSe:AHOur:RESet{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($"SENSe:AHOur:RESet{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
@@ -220,6 +350,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取当前充放电累计的安时值 (单位: Ah)[cite: 1]
|
/// 读取当前充放电累计的安时值 (单位: Ah)[cite: 1]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>累计安时值 (单位: Ah)</returns>
|
||||||
public virtual async Task<string> 查询累计安时_Ah(CancellationToken ct = default)
|
public virtual async Task<string> 查询累计安时_Ah(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"FETCh:AHOur?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($"FETCh:AHOur?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
@@ -228,6 +360,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取当前充放电累计的瓦时值 (单位: Wh)[cite: 1]
|
/// 读取当前充放电累计的瓦时值 (单位: Wh)[cite: 1]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>累计瓦时值 (单位: Wh)</returns>
|
||||||
public virtual async Task<string> 查询累计瓦时_Wh(CancellationToken ct = default)
|
public virtual async Task<string> 查询累计瓦时_Wh(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"FETCh:WHOur?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($"FETCh:WHOur?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
@@ -236,6 +370,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取充放电积分持续时间 (单位: 秒)[cite: 1]
|
/// 读取充放电积分持续时间 (单位: 秒)[cite: 1]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>积分持续时间 (单位: 秒)</returns>
|
||||||
public virtual async Task<string> 查询积分时间(CancellationToken ct = default)
|
public virtual async Task<string> 查询积分时间(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"FETCh:ENERgy:TIME?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($"FETCh:ENERgy:TIME?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
@@ -244,6 +380,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 切换设备的操作模式 (VOLTage: 电源模式, LOAD: 负载模式/放电, CURRent: 电流源)[cite: 1]
|
/// 切换设备的操作模式 (VOLTage: 电源模式, LOAD: 负载模式/放电, CURRent: 电流源)[cite: 1]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="模式">操作模式 (VOLTage/LOAD/CURRent)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置操作模式(DeviceOperationMode 模式, CancellationToken ct = default)
|
public virtual async Task 设置操作模式(DeviceOperationMode 模式, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"SYSTem:OPERation:MODE {模式}{ScpiDelimiter}", ct); //[cite: 1]
|
await SendAsync($"SYSTem:OPERation:MODE {模式}{ScpiDelimiter}", ct); //[cite: 1]
|
||||||
@@ -252,6 +390,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置放电截止电压/电压下限 (单位: V,用于防止电池过放)[cite: 1]
|
/// 设置放电截止电压/电压下限 (单位: V,用于防止电池过放)[cite: 1]
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">放电截止电压/电压下限 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电压下限(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置电压下限(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:LIMit:LOW {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
|
string cmd = string.Format(CultureInfo.InvariantCulture, ":SOURce:VOLTage:LIMit:LOW {0:F2}{1}", 电压, ScpiDelimiter); //[cite: 1]
|
||||||
@@ -259,5 +399,47 @@ namespace DeviceCommand.Devices
|
|||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
|
#region 7. 相位输出模式设置 (单相/三相/反相)[cite: 2]
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置电源相位输出模式 (ONE:单相 / THRee:三相 / DIFFerence:反相 / MULTichannel:多通道)[cite: 2]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="模式">相位输出模式</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
public virtual async Task 设置输出模式(DevicePhaseMode 模式, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await SendAsync($"SYSTem:FUNCtion {模式}{ScpiDelimiter}", ct); //[cite: 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换为单相模式 (SYST:FUNC ONE)[cite: 2]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
public virtual async Task 切换为单相模式(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await SendAsync($"SYSTem:FUNCtion ONE{ScpiDelimiter}", ct); //[cite: 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换为反相模式 (SYST:FUNC DIFFerence)[cite: 2]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
public virtual async Task 切换为反相模式(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
await SendAsync($"SYSTem:FUNCtion DIFFerence{ScpiDelimiter}", ct); //[cite: 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 查询当前输出模式 (单相/三相/反相/多通道)[cite: 2]
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>返回 ONE, THRee, DIFFerence 或 MULTichannel[cite: 2]</returns>
|
||||||
|
public virtual async Task<string> 查询输出模式(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
return await WriteReadAsync($"SYSTem:FUNCtion?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 2]
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@ using DeviceCommand.Base;
|
|||||||
using Model.Models;
|
using Model.Models;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -22,11 +23,31 @@ namespace DeviceCommand.Devices
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从设备返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||||
|
/// 先按逗号分割取数据段,再提取数值。
|
||||||
|
/// </summary>
|
||||||
|
private static double ExtractDouble(string raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||||
|
|
||||||
|
string dataPart = raw;
|
||||||
|
int commaIdx = raw.LastIndexOf(',');
|
||||||
|
if (commaIdx >= 0 && commaIdx < raw.Length - 1)
|
||||||
|
dataPart = raw.Substring(commaIdx + 1);
|
||||||
|
|
||||||
|
var match = Regex.Match(dataPart, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
|
||||||
|
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
|
||||||
|
? val
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#region 3.1. IEEE 488.2 公共命令
|
#region 3.1. IEEE 488.2 公共命令
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.1. 清除标准事件状态寄存器和错误队列
|
/// 3.1.1. 清除标准事件状态寄存器和错误队列
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
|
public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
||||||
@@ -35,6 +56,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.4. 读取直流电源相关信息(制造商、产品型号、系统 SN、软件版本号)
|
/// 3.1.4. 读取直流电源相关信息(制造商、产品型号、系统 SN、软件版本号)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>设备标识字符串(制造商, 产品型号, 序列号, 固件版本)</returns>
|
||||||
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -43,6 +66,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.7. 恢复出厂设置 (注意:设备重置保存数据大约需要 10 秒)
|
/// 3.1.7. 恢复出厂设置 (注意:设备重置保存数据大约需要 10 秒)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 重置设备(CancellationToken ct = default)
|
public virtual async Task 重置设备(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
||||||
@@ -51,6 +75,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.8. 读取状态字节寄存器(只读寄存器,读取时不会清除位)
|
/// 3.1.8. 读取状态字节寄存器(只读寄存器,读取时不会清除位)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>状态字节寄存器值</returns>
|
||||||
public virtual async Task<string> 读取状态字节(CancellationToken ct = default)
|
public virtual async Task<string> 读取状态字节(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -63,6 +89,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.1. 设定输出电压值 (单位: V)
|
/// 3.2.1. 设定输出电压值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">输出电压设定值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电压(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置电压(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
|
||||||
@@ -72,6 +100,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.1. 查询输出电压设定值 (单位: V)
|
/// 3.2.1. 查询输出电压设定值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>输出电压设定值 (单位: V)</returns>
|
||||||
public virtual async Task<string> 查询电压设定(CancellationToken ct = default)
|
public virtual async Task<string> 查询电压设定(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"SOURce:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"SOURce:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -80,6 +110,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.2. 设置输出限流值 (单位: A)
|
/// 3.2.2. 设置输出限流值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电流">输出限流设定值 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
|
||||||
@@ -89,6 +121,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.2. 查询输出限流值设定值 (单位: A)
|
/// 3.2.2. 查询输出限流值设定值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>输出限流设定值 (单位: A)</returns>
|
||||||
public virtual async Task<string> 查询电流设定(CancellationToken ct = default)
|
public virtual async Task<string> 查询电流设定(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"SOURce:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"SOURce:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -97,6 +131,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.3. 设置输出模拟内阻值 (单位: mΩ)
|
/// 3.2.3. 设置输出模拟内阻值 (单位: mΩ)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="内阻">模拟内阻设定值 (单位: mΩ)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置内阻(double 内阻, CancellationToken ct = default)
|
public virtual async Task 设置内阻(double 内阻, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:INTErnalres {0:F1}{1}", 内阻, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:INTErnalres {0:F1}{1}", 内阻, ScpiDelimiter);
|
||||||
@@ -106,6 +142,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.4. 保存当前测试参数到指定存储组 (范围: 1-10)
|
/// 3.2.4. 保存当前测试参数到指定存储组 (范围: 1-10)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="组别">存储组编号 (范围: 1-10)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 保存测试参数(int 组别, CancellationToken ct = default)
|
public virtual async Task 保存测试参数(int 组别, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (组别 < 1 || 组别 > 10) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~10");
|
if (组别 < 1 || 组别 > 10) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~10");
|
||||||
@@ -115,6 +153,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.5. 调用指定存储组的测试参数 (范围: 1-10)
|
/// 3.2.5. 调用指定存储组的测试参数 (范围: 1-10)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="组别">存储组编号 (范围: 1-10)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 调用测试参数(int 组别, CancellationToken ct = default)
|
public virtual async Task 调用测试参数(int 组别, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (组别 < 1 || 组别 > 10) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~10");
|
if (组别 < 1 || 组别 > 10) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~10");
|
||||||
@@ -128,6 +168,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.1. 控制电源输出开关 (True: 开启, False: 关闭)
|
/// 3.3.1. 控制电源输出开关 (True: 开启, False: 关闭)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="开启">true: 开启输出, false: 关闭输出</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string 参数 = 开启 ? "ON" : "OFF";
|
string 参数 = 开启 ? "ON" : "OFF";
|
||||||
@@ -137,6 +179,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.1. 查询电源输出开关状态 (返回 "ON" 或 "OFF")
|
/// 3.3.1. 查询电源输出开关状态 (返回 "ON" 或 "OFF")
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>输出开关状态字符串("ON" 或 "OFF")</returns>
|
||||||
public virtual async Task<string> 查询DC输出开关状态(CancellationToken ct = default)
|
public virtual async Task<string> 查询DC输出开关状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"OUTPut:ONOFF?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"OUTPut:ONOFF?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -146,6 +190,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// 3.3.2. 设定电源工作模式
|
/// 3.3.2. 设定电源工作模式
|
||||||
/// (NORMal: 普通模式, CHARge: 电池充电, SEQuence: 序列模式, CPOWer: 恒功率模式, CARWave: 汽车测试, APG: 外部编程)
|
/// (NORMal: 普通模式, CHARge: 电池充电, SEQuence: 序列模式, CPOWer: 恒功率模式, CARWave: 汽车测试, APG: 外部编程)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="模式">工作模式 (NORMal/CHARge/SEQuence/CPOWer/CARWave/APG)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置运行模式(string 模式, CancellationToken ct = default)
|
public virtual async Task 设置运行模式(string 模式, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"OUTPut:MODE {模式}{ScpiDelimiter}", ct);
|
await SendAsync($"OUTPut:MODE {模式}{ScpiDelimiter}", ct);
|
||||||
@@ -154,6 +200,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.2. 查询电源当前运行工作模式
|
/// 3.3.2. 查询电源当前运行工作模式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>当前运行工作模式字符串</returns>
|
||||||
public virtual async Task<string> 查询运行模式(CancellationToken ct = default)
|
public virtual async Task<string> 查询运行模式(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"OUTPut:MODE?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"OUTPut:MODE?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -162,6 +210,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.11. 设置电源输出 CV/CC 优先权 (CV 或 CC)
|
/// 3.3.11. 设置电源输出 CV/CC 优先权 (CV 或 CC)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="优先模式">优先模式,只能为 "CV" 或 "CC"</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置CVCC优先权(string 优先模式, CancellationToken ct = default)
|
public virtual async Task 设置CVCC优先权(string 优先模式, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (优先模式 != "CV" && 优先模式 != "CC") throw new ArgumentException("优先模式只能为 'CV' 或 'CC'");
|
if (优先模式 != "CV" && 优先模式 != "CC") throw new ArgumentException("优先模式只能为 'CV' 或 'CC'");
|
||||||
@@ -171,6 +221,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.12. 获取电源状态字(通过解析返回整数的二进制 Bit 位获取全状态环路及告警)
|
/// 3.3.12. 获取电源状态字(通过解析返回整数的二进制 Bit 位获取全状态环路及告警)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>电源状态字(整数字符串,通过二进制位标识各状态及告警)</returns>
|
||||||
public virtual async Task<string> 查询设备状态字(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备状态字(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"OUTPut:STATe?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"OUTPut:STATe?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -179,6 +231,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.13. 获取电源事件告警状态值 (返回整数通过位定义标识 UVP/OVP/OCP/OPP/OTP)
|
/// 3.3.13. 获取电源事件告警状态值 (返回整数通过位定义标识 UVP/OVP/OCP/OPP/OTP)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>事件告警状态值(整数字符串,通过位定义标识 UVP/OVP/OCP/OPP/OTP)</returns>
|
||||||
public virtual async Task<string> 查询事件告警状态(CancellationToken ct = default)
|
public virtual async Task<string> 查询事件告警状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"OUTPut:EVENT?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"OUTPut:EVENT?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -187,6 +241,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.13. 清除当前的告警状态
|
/// 3.3.13. 清除当前的告警状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除告警(CancellationToken ct = default)
|
public virtual async Task 清除告警(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"OUTPut:EVENT 0{ScpiDelimiter}", ct);
|
await SendAsync($"OUTPut:EVENT 0{ScpiDelimiter}", ct);
|
||||||
@@ -195,6 +250,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.16. 打开/关闭设备定时关机功能
|
/// 3.3.16. 打开/关闭设备定时关机功能
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="开启">true: 开启定时关机, false: 关闭定时关机</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置定时关机开关(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置定时关机开关(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string 参数 = 开启 ? "ON" : "OFF";
|
string 参数 = 开启 ? "ON" : "OFF";
|
||||||
@@ -204,6 +261,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.17. 设定设备定时关机倒计时时间 (单位: s)
|
/// 3.3.17. 设定设备定时关机倒计时时间 (单位: s)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="秒数">定时关机倒计时时间 (单位: s)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置定时关机时间(double 秒数, CancellationToken ct = default)
|
public virtual async Task 设置定时关机时间(double 秒数, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "OUTPut:TIMing:DWELI {0:F1}{1}", 秒数, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "OUTPut:TIMing:DWELI {0:F1}{1}", 秒数, ScpiDelimiter);
|
||||||
@@ -213,6 +272,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.3.18. 控制泄放电路(Bleeder)开关状态
|
/// 3.3.18. 控制泄放电路(Bleeder)开关状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="开启">true: 开启泄放电路, false: 关闭泄放电路</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置泄放电路开关(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置泄放电路开关(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string 参数 = 开启 ? "ON" : "OFF";
|
string 参数 = 开启 ? "ON" : "OFF";
|
||||||
@@ -226,33 +287,44 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.4.1. 回读通道输出端子上的实时测得电压值 (单位: V)
|
/// 3.4.1. 回读通道输出端子上的实时测得电压值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实时测量电压值 (单位: V)</returns>
|
||||||
[Monitorable("高压直流电源电压")]
|
[Monitorable("高压直流电源电压")]
|
||||||
public virtual async Task<string> 查询实际电压(CancellationToken ct = default)
|
public virtual async Task<double> 查询实际电压(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
|
string res = await WriteReadAsync($"MEASure:VOLTage?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return ExtractDouble(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.4.2. 回读通道输出端子上的实时测得电流值 (单位: A)
|
/// 3.4.2. 回读通道输出端子上的实时测得电流值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实时测量电流值 (单位: A)</returns>
|
||||||
[Monitorable("高压直流电源电流")]
|
[Monitorable("高压直流电源电流")]
|
||||||
public virtual async Task<string> 查询实际电流(CancellationToken ct = default)
|
public virtual async Task<double> 查询实际电流(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
|
string res = await WriteReadAsync($"MEASure:CURRent?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return ExtractDouble(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.4.3. 回读通道输出端子上的实时测得功率值 (单位: W)
|
/// 3.4.3. 回读通道输出端子上的实时测得功率值 (单位: W)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[Monitorable("高压直流电源功率")]
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task<string> 查询实际功率(CancellationToken ct = default)
|
/// <returns>实时测量功率值 (单位: W)</returns>
|
||||||
|
[Monitorable("高压直流电源功率")]
|
||||||
|
public virtual async Task<double> 查询实际功率(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
|
string res = await WriteReadAsync($"MEASure:POWer?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return ExtractDouble(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.4.4. 回读电池充电模式下当前累计已充入的容量值 (单位: mAh)
|
/// 3.4.4. 回读电池充电模式下当前累计已充入的容量值 (单位: mAh)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>累计已充入容量值 (单位: mAh)</returns>
|
||||||
public virtual async Task<string> 查询已充电容量MAH(CancellationToken ct = default)
|
public virtual async Task<string> 查询已充电容量MAH(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"MEASure:MAH?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"MEASure:MAH?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -261,6 +333,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.4.5. 获取当前电源的硬件额定电压上限值 (单位: V)
|
/// 3.4.5. 获取当前电源的硬件额定电压上限值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>硬件额定电压上限值 (单位: V)</returns>
|
||||||
public virtual async Task<string> 获取设备额定电压(CancellationToken ct = default)
|
public virtual async Task<string> 获取设备额定电压(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"MEASure:VOLTage:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"MEASure:VOLTage:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -269,6 +343,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.4.6. 获取当前电源的硬件额定电流上限值 (单位: A)
|
/// 3.4.6. 获取当前电源的硬件额定电流上限值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>硬件额定电流上限值 (单位: A)</returns>
|
||||||
public virtual async Task<string> 获取设备额定电流(CancellationToken ct = default)
|
public virtual async Task<string> 获取设备额定电流(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"MEASure:CURRent:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"MEASure:CURRent:MAXimum?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -281,6 +357,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.1. 设置欠压保护门限值 (单位: V)
|
/// 3.5.1. 设置欠压保护门限值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">欠压保护门限值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置过欠压保护_UVP(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置过欠压保护_UVP(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:LESS:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:LESS:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
|
||||||
@@ -290,6 +368,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.4. 设置过压保护门限值 (单位: V)
|
/// 3.5.4. 设置过压保护门限值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">过压保护门限值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置过压保护_OVP(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置过压保护_OVP(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:OVER:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:OVER:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
|
||||||
@@ -299,6 +379,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.6. 设置硬件硬件过流保护硬指标参数 (单位: A)
|
/// 3.5.6. 设置硬件硬件过流保护硬指标参数 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电流">过流保护硬指标参数 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置过流保护_OCP(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置过流保护_OCP(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
|
||||||
@@ -308,6 +390,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.8. 设置硬件过功率保护门限值 (单位: W)
|
/// 3.5.8. 设置硬件过功率保护门限值 (单位: W)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="功率">过功率保护门限值 (单位: W)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置过功率保护_OPP(double 功率, CancellationToken ct = default)
|
public virtual async Task 设置过功率保护_OPP(double 功率, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:POWer {0:F3}{1}", 功率, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "PROTect:POWer {0:F3}{1}", 功率, ScpiDelimiter);
|
||||||
@@ -317,6 +401,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.12. 设置可调节的用户软输出电压下限值 (单位: V)
|
/// 3.5.12. 设置可调节的用户软输出电压下限值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">输出电压下限值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电压下限(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置电压下限(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage:LEVel:LIMit:LOW {0:F3}{1}", 电压, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage:LEVel:LIMit:LOW {0:F3}{1}", 电压, ScpiDelimiter);
|
||||||
@@ -326,6 +412,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.13. 设置可调节的用户软输出电压上限值 (单位: V)
|
/// 3.5.13. 设置可调节的用户软输出电压上限值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">输出电压上限值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电压上限(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置电压上限(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage:LEVel:LIMit {0:F3}{1}", 电压, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:VOLTage:LEVel:LIMit {0:F3}{1}", 电压, ScpiDelimiter);
|
||||||
@@ -335,6 +423,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.14. 设置可调节的用户软输出电流下限值 (单位: A)
|
/// 3.5.14. 设置可调节的用户软输出电流下限值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电流">输出电流下限值 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电流下限(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置电流下限(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent:LEVel:LIMit:LOW {0:F3}{1}", 电流, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent:LEVel:LIMit:LOW {0:F3}{1}", 电流, ScpiDelimiter);
|
||||||
@@ -344,6 +434,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.15. 设置可调节的用户软输出电流上限值 (单位: A)
|
/// 3.5.15. 设置可调节的用户软输出电流上限值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电流">输出电流上限值 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电流上限(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置电流上限(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent:LEVel:LIMit {0:F3}{1}", 电流, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "SOURce:CURRent:LEVel:LIMit {0:F3}{1}", 电流, ScpiDelimiter);
|
||||||
@@ -357,6 +449,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.6.1. 设定恒功率工作模式下的限定电压值 (单位: V)
|
/// 3.6.1. 设定恒功率工作模式下的限定电压值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">恒功率模式限定电压值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置恒功率模式电压(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置恒功率模式电压(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:VOLTage {0:F3}{1}", 电压, ScpiDelimiter);
|
||||||
@@ -366,6 +460,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.6.2. 设定恒功率工作模式下的限定电流值 (单位: A)
|
/// 3.6.2. 设定恒功率工作模式下的限定电流值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电流">恒功率模式限定电流值 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置恒功率模式电流(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置恒功率模式电流(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:CURRent {0:F3}{1}", 电流, ScpiDelimiter);
|
||||||
@@ -375,6 +471,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.6.3. 设定恒功率工作模式下的运行功率目标值 (单位: W)
|
/// 3.6.3. 设定恒功率工作模式下的运行功率目标值 (单位: W)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="功率">恒功率模式运行功率目标值 (单位: W)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置恒功率模式功率(double 功率, CancellationToken ct = default)
|
public virtual async Task 设置恒功率模式功率(double 功率, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:POWer {0:F3}{1}", 功率, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "CPOWer:POWer {0:F3}{1}", 功率, ScpiDelimiter);
|
||||||
@@ -383,4 +481,4 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 建立 TCP 连接,成功后自动激活心跳
|
/// 建立 TCP 连接,成功后自动激活心跳
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>连接是否成功</returns>
|
||||||
public new async Task<bool> ConnectAsync(CancellationToken ct = default)
|
public new async Task<bool> ConnectAsync(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
bool isConnected = await base.ConnectAsync(ct);
|
bool isConnected = await base.ConnectAsync(ct);
|
||||||
@@ -124,6 +126,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.1 读取电源的相关信息(制造商, 产品标号, 产品序列号, 软件版本号)
|
/// 3.1.1 读取电源的相关信息(制造商, 产品标号, 产品序列号, 软件版本号)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>设备标识字符串(制造商, 产品标号, 产品序列号, 软件版本号)</returns>
|
||||||
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*IDN?{SCPIDelimiter}", SCPIDelimiter, ct);
|
return await WriteReadAsync($"*IDN?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
@@ -132,6 +136,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.2 从指定的单位中恢复 *SAV 命令保存的设定值 (参数:1~99)
|
/// 3.1.2 从指定的单位中恢复 *SAV 命令保存的设定值 (参数:1~99)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="组别">存储组编号 (范围: 1~99)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 调用存储状态(int 组别, CancellationToken ct = default)
|
public virtual async Task 调用存储状态(int 组别, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (组别 < 1 || 组别 > 99) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~99");
|
if (组别 < 1 || 组别 > 99) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~99");
|
||||||
@@ -141,6 +147,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.3 将仪器当前系统状态保存到非易失性内存中 (参数:1~99)
|
/// 3.1.3 将仪器当前系统状态保存到非易失性内存中 (参数:1~99)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="组别">存储组编号 (范围: 1~99)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 保存当前状态(int 组别, CancellationToken ct = default)
|
public virtual async Task 保存当前状态(int 组别, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (组别 < 1 || 组别 > 99) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~99");
|
if (组别 < 1 || 组别 > 99) throw new ArgumentOutOfRangeException(nameof(组别), "组别有效范围为 1~99");
|
||||||
@@ -150,6 +158,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.4 远程控制触发一次
|
/// 3.1.4 远程控制触发一次
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 远程触发(CancellationToken ct = default)
|
public virtual async Task 远程触发(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*TRG{SCPIDelimiter}", ct);
|
await SendAsync($"*TRG{SCPIDelimiter}", ct);
|
||||||
@@ -158,6 +167,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.1.5 返回状态、采集电压、采集电流 (格式: 状态码,电压值,电流值)
|
/// 3.1.5 返回状态、采集电压、采集电流 (格式: 状态码,电压值,电流值)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>状态码,电压值,电流值的组合字符串</returns>
|
||||||
public virtual async Task<string> 查询全部状态及采样(CancellationToken ct = default)
|
public virtual async Task<string> 查询全部状态及采样(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*ALL?{SCPIDelimiter}", SCPIDelimiter, ct);
|
return await WriteReadAsync($"*ALL?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
@@ -170,6 +181,9 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.1 设置输出电压和输出电流值
|
/// 3.2.1 设置输出电压和输出电流值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">输出电压设定值 (单位: V)</param>
|
||||||
|
/// <param name="电流">输出电流设定值 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 快捷设置电压电流(double 电压, double 电流, CancellationToken ct = default)
|
public virtual async Task 快捷设置电压电流(double 电压, double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,规范应用 APPL 短格式
|
// 修正:去除前缀冒号,规范应用 APPL 短格式
|
||||||
@@ -180,6 +194,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.2.1 查询当前设定的输出电压和电流值
|
/// 3.2.1 查询当前设定的输出电压和电流值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>当前输出电压和电流设定值的组合字符串</returns>
|
||||||
public virtual async Task<string> 查询快捷电压电流设定(CancellationToken ct = default)
|
public virtual async Task<string> 查询快捷电压电流设定(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -193,50 +209,59 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.1 查询通道输出端子上测得的直流电流值 (A)
|
/// 3.5.1 查询通道输出端子上测得的直流电流值 (A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实测直流电流值 (单位: A)</returns>
|
||||||
[Monitorable("低压直流电源电流")]
|
[Monitorable("低压直流电源电流")]
|
||||||
public virtual async Task<string> 查询实际电流(CancellationToken ct = default)
|
public virtual async Task<double> 查询实际电流(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,改用标准 MEAS:CURR?
|
string response = await WriteReadAsync($"MEAS:CURR?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
return await WriteReadAsync($"MEAS:CURR?{SCPIDelimiter}", SCPIDelimiter, ct);
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.2 查询通道输出端子上测得的直流功率值 (W)
|
/// 3.5.2 查询通道输出端子上测得的直流功率值 (W)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实测直流功率值 (单位: W)</returns>
|
||||||
[Monitorable("低压直流电源功率")]
|
[Monitorable("低压直流电源功率")]
|
||||||
public virtual async Task<string> 查询实际功率(CancellationToken ct = default)
|
public virtual async Task<double> 查询实际功率(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,改用标准 MEAS:POW?
|
string response = await WriteReadAsync($"MEAS:POW?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
return await WriteReadAsync($"MEAS:POW?{SCPIDelimiter}", SCPIDelimiter, ct);
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.3 查询通道输出端子上测得的直流电压值 (V)
|
/// 3.5.3 查询通道输出端子上测得的直流电压值 (V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实测直流电压值 (单位: V)</returns>
|
||||||
[Monitorable("低压直流电源电压")]
|
[Monitorable("低压直流电源电压")]
|
||||||
public virtual async Task<string> 查询实际电压(CancellationToken ct = default)
|
public virtual async Task<double> 查询实际电压(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,改用标准 MEAS:VOLT?
|
string response = await WriteReadAsync($"MEAS:VOLT?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
return await WriteReadAsync($"MEAS:VOLT?{SCPIDelimiter}", SCPIDelimiter, ct);
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.4 查询通道输出端子上测得的电压、电流和功率的组合值
|
/// 3.5.4 查询通道输出端子上测得的电压、电流和功率的组合值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>电压、电流和功率的组合值字符串</returns>
|
||||||
public virtual async Task<string> 查询电压电流功率数组(CancellationToken ct = default)
|
public virtual async Task<string> 查询电压电流功率数组(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
return await WriteReadAsync($"MEAS:VAP?{SCPIDelimiter}", SCPIDelimiter, ct);
|
return await WriteReadAsync($"MEAS:VAP?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.5.5 查询通道输出端子上测得的输出时间长度
|
/// 3.5.5 查询通道输出端子上测得的输出时间长度
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询总输出时间长度(CancellationToken ct = default)
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>输出时间长度</returns>
|
||||||
|
public virtual async Task<double> 查询总输出时间长度(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
string response = await WriteReadAsync($"MEAS:TIME:OUTP?{SCPIDelimiter}", SCPIDelimiter, ct);
|
||||||
return await WriteReadAsync($"MEAS:TIME:OUTP?{SCPIDelimiter}", SCPIDelimiter, ct);
|
return double.TryParse(response, out double result) ? result : double.NaN;
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -246,6 +271,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.6.1 设置电源输出开关
|
/// 3.6.1 设置电源输出开关
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="开启">true: 开启输出, false: 关闭输出</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置DC输出(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,改用 OUTP 短格式
|
// 修正:去除前缀冒号,改用 OUTP 短格式
|
||||||
@@ -256,6 +283,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.6.1 查询通道输出状态 (返回 ON 或 OFF)
|
/// 3.6.1 查询通道输出状态 (返回 ON 或 OFF)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>通道输出状态字符串("ON" 或 "OFF")</returns>
|
||||||
public virtual async Task<string> 查询DC输出状态(CancellationToken ct = default)
|
public virtual async Task<string> 查询DC输出状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -265,6 +294,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.6.2 控制电源输出定时器的开关状态
|
/// 3.6.2 控制电源输出定时器的开关状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="开启">true: 开启定时器, false: 关闭定时器</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置定时器状态(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置定时器状态(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -275,6 +306,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.6.3 设定电源输出定时器的时间 (单位: s, 范围: 0.1 ~ 999999.9)
|
/// 3.6.3 设定电源输出定时器的时间 (单位: s, 范围: 0.1 ~ 999999.9)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="秒数">定时器时间 (单位: s, 范围: 0.1 ~ 999999.9)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置定时器时间(double 秒数, CancellationToken ct = default)
|
public virtual async Task 设置定时器时间(double 秒数, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -285,6 +318,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.6.4 设定电源输出的模式优先权 (CV优先 或 CC优先)
|
/// 3.6.4 设定电源输出的模式优先权 (CV优先 或 CC优先)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="是CV优先">true: CV优先, false: CC优先</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置输出模式优先(bool 是CV优先, CancellationToken ct = default)
|
public virtual async Task 设置输出模式优先(bool 是CV优先, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -299,6 +334,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.1.1 清除输出电流保护(OCP)电路状态
|
/// 3.7.1.1 清除输出电流保护(OCP)电路状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除电流保护状态(CancellationToken ct = default)
|
public virtual async Task 清除电流保护状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -308,6 +344,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.1.2 使能或禁止当前输出电流保护(OCP)电路
|
/// 3.7.1.2 使能或禁止当前输出电流保护(OCP)电路
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="启用">true: 使能OCP, false: 禁止OCP</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电流保护使能(bool 启用, CancellationToken ct = default)
|
public virtual async Task 设置电流保护使能(bool 启用, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,将 STATE 改为更常用的 STAT 简写
|
// 修正:去除前缀冒号,将 STATE 改为更常用的 STAT 简写
|
||||||
@@ -318,6 +356,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.1.4 设置输出电流保护(OCP)阀值 (A)
|
/// 3.7.1.4 设置输出电流保护(OCP)阀值 (A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电流">OCP 保护阀值 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置过流保护值(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置过流保护值(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号。按 NGI 通用指令树,加上层级限制或者直接作用于 CURR:PROT
|
// 修正:去除前缀冒号。按 NGI 通用指令树,加上层级限制或者直接作用于 CURR:PROT
|
||||||
@@ -328,6 +368,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.1.7 设置电源的输出电流值 (A)
|
/// 3.7.1.7 设置电源的输出电流值 (A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电流">输出电流设定值 (单位: A)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
|
public virtual async Task 设置电流(double 电流, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,改用标准 CURR 短格式
|
// 修正:去除前缀冒号,改用标准 CURR 短格式
|
||||||
@@ -338,6 +380,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.2.1 清除输出电压保护(OVP)电路状态
|
/// 3.7.2.1 清除输出电压保护(OVP)电路状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除电压保护状态(CancellationToken ct = default)
|
public virtual async Task 清除电压保护状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -347,6 +390,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.2.2 使能或禁止当前输出电压保护(OVP)电路
|
/// 3.7.2.2 使能或禁止当前输出电压保护(OVP)电路
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="启用">true: 使能OVP, false: 禁止OVP</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电压保护使能(bool 启用, CancellationToken ct = default)
|
public virtual async Task 设置电压保护使能(bool 启用, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -357,6 +402,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.2.4 设置输出电压保护(OVP)阀值 (V)
|
/// 3.7.2.4 设置输出电压保护(OVP)阀值 (V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">OVP 保护阀值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置过压保护值(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置过压保护值(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -367,6 +414,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.2.7 设置电源的输出电压值 (V)
|
/// 3.7.2.7 设置电源的输出电压值 (V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">输出电压设定值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电压(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置电压(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,改用标准 VOLT 短格式
|
// 修正:去除前缀冒号,改用标准 VOLT 短格式
|
||||||
@@ -377,23 +426,30 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.2.8 设定电压输出范围的上限电压值 (V)
|
/// 3.7.2.8 设定电压输出范围的上限电压值 (V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="电压">上限电压值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置电压上限限制(double 电压, CancellationToken ct = default)
|
public virtual async Task 设置电压上限限制(double 电压, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT:LIM {0:F3}{1}", 电压, SCPIDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "VOLT:LIM {0:F3}{1}", 电压, SCPIDelimiter);
|
||||||
await SendAsync(cmd, ct);
|
await SendAsync(cmd, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.2.9 清除电压上限限制(恢复为设备物理允许的最大电压值)
|
/// 3.7.2.9 清除电压上限限制(恢复为设备物理允许的最大电压值)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除电压上限限制(CancellationToken ct = default)
|
public virtual async Task 清除电压上限限制(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 发送 MAXimum 设为最大值以达到“清除限制”的效果
|
// 发送 MAXimum 设为最大值以达到"清除限制"的效果
|
||||||
await SendAsync($"VOLT:LIMIT MAX{SCPIDelimiter}", ct); // 对应手册 3.7.2.8
|
await SendAsync($"VOLT:LIMIT MAX{SCPIDelimiter}", ct); // 对应手册 3.7.2.8
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.3.1 设置输出功率值 (W)
|
/// 3.7.3.1 设置输出功率值 (W)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="功率">输出功率设定值 (单位: W)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置功率(double 功率, CancellationToken ct = default)
|
public virtual async Task 设置功率(double 功率, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号,改用标准 POW 短格式
|
// 修正:去除前缀冒号,改用标准 POW 短格式
|
||||||
@@ -404,6 +460,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.7.3.2 禁止或使能当前恒功率(CP)输出
|
/// 3.7.3.2 禁止或使能当前恒功率(CP)输出
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="启用">true: 使能恒功率输出, false: 禁止恒功率输出</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置恒功率输出使能(bool 启用, CancellationToken ct = default)
|
public virtual async Task 设置恒功率输出使能(bool 启用, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -418,6 +476,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.9.1 控制蜂鸣器开关
|
/// 3.9.1 控制蜂鸣器开关
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="开启">true: 开启蜂鸣器, false: 关闭蜂鸣器</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置蜂鸣器状态(bool 开启, CancellationToken ct = default)
|
public virtual async Task 设置蜂鸣器状态(bool 开启, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -428,6 +488,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.9.2 使蜂鸣器强制鸣叫一声
|
/// 3.9.2 使蜂鸣器强制鸣叫一声
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 蜂鸣器鸣叫(CancellationToken ct = default)
|
public virtual async Task 蜂鸣器鸣叫(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -437,6 +498,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.9.4 查询仪器当前出错记录数量 (最大 18 组)
|
/// 3.9.4 查询仪器当前出错记录数量 (最大 18 组)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>当前出错记录数量(最大 18 组)</returns>
|
||||||
public virtual async Task<string> 查询错误记录数量(CancellationToken ct = default)
|
public virtual async Task<string> 查询错误记录数量(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -446,6 +509,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.9.5 读取仪器的出错信息 (成功返回 0,"No error")
|
/// 3.9.5 读取仪器的出错信息 (成功返回 0,"No error")
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>错误代码和描述信息(成功返回 0,"No error")</returns>
|
||||||
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -455,6 +520,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.9.7 设置电源为面板控制模式 (本地 Local 状态,前面板按键可用)
|
/// 3.9.7 设置电源为面板控制模式 (本地 Local 状态,前面板按键可用)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 切换本地控制模式(CancellationToken ct = default)
|
public virtual async Task 切换本地控制模式(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -464,6 +530,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.9.8 设置电源为远程控制模式 (Remote 状态)
|
/// 3.9.8 设置电源为远程控制模式 (Remote 状态)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 切换远程控制模式(CancellationToken ct = default)
|
public virtual async Task 切换远程控制模式(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
@@ -473,6 +540,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 3.9.9 通过通信接口设置电源为远程控制锁定模式
|
/// 3.9.9 通过通信接口设置电源为远程控制锁定模式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 远程控制模式锁定(CancellationToken ct = default)
|
public virtual async Task 远程控制模式锁定(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 修正:去除前缀冒号
|
// 修正:去除前缀冒号
|
||||||
|
|||||||
+556
-37
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ using Model.Models;
|
|||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
@@ -22,11 +23,35 @@ namespace DeviceCommand.Devices
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从示波器返回的 SCPI 响应字符串中提取数值部分并转换为 double。
|
||||||
|
/// 响应格式示例: "C1:PAVA PKPK,2.48E-03V" → 按逗号分割后提取 2.48E-03
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="raw">示波器返回的原始字符串</param>
|
||||||
|
/// <returns>提取到的数值,解析失败时返回 0</returns>
|
||||||
|
private static double ExtractDouble(string raw)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return 0;
|
||||||
|
|
||||||
|
string dataPart = raw;
|
||||||
|
// 按逗号分割,取最后一段(数值通常在逗号后面)
|
||||||
|
int commaIdx = raw.LastIndexOf(',');
|
||||||
|
if (commaIdx >= 0 && commaIdx < raw.Length - 1)
|
||||||
|
dataPart = raw.Substring(commaIdx + 1);
|
||||||
|
|
||||||
|
// 从数据段中提取数值(含正负号、小数点、科学计数法)
|
||||||
|
var match = Regex.Match(dataPart, @"[+-]?(?:\d+\.?\d*|\.\d+)(?:[Ee][+-]?\d+)?");
|
||||||
|
return match.Success && double.TryParse(match.Value, NumberStyles.Float, CultureInfo.InvariantCulture, out double val)
|
||||||
|
? val
|
||||||
|
: 0;
|
||||||
|
}
|
||||||
|
|
||||||
#region 1. IEEE 488.2 公共命令
|
#region 1. IEEE 488.2 公共命令
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清除标准事件状态寄存器和错误队列
|
/// 清除标准事件状态寄存器和错误队列
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除状态(CancellationToken ct = default)
|
public virtual async Task 清除状态(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
||||||
@@ -34,6 +59,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询错误信息
|
/// 查询错误信息
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>错误代码和描述信息</returns>
|
||||||
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
public virtual async Task<string> 查询错误信息(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
return await WriteReadAsync($":SYSTem:ERRor?{ScpiDelimiter}", ScpiDelimiter, ct); //[cite: 1]
|
||||||
@@ -41,6 +68,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取示波器识别字符串(制造商、型号、序列号、固件版本)
|
/// 读取示波器识别字符串(制造商、型号、序列号、固件版本)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>设备标识字符串(制造商, 型号, 序列号, 固件版本)</returns>
|
||||||
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -49,6 +78,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 复位命令。使示波器恢复到默认的出厂设置
|
/// 复位命令。使示波器恢复到默认的出厂设置
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 重置设备(CancellationToken ct = default)
|
public virtual async Task 重置设备(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
||||||
@@ -57,6 +87,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询先前操作是否完成。
|
/// 查询先前操作是否完成。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>true: 操作已完成, false: 操作仍在执行中</returns>
|
||||||
public virtual async Task<bool> 检查操作完成_OPC(CancellationToken ct = default)
|
public virtual async Task<bool> 检查操作完成_OPC(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string res = await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
string res = await WriteReadAsync($"*OPC?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -70,6 +102,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 控制示波器开始捕获波形
|
/// 控制示波器开始捕获波形
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 启动捕获_RUN(CancellationToken ct = default)
|
public virtual async Task 启动捕获_RUN(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct);
|
await SendAsync($":TRIGger:RUN{ScpiDelimiter}", ct);
|
||||||
@@ -78,6 +111,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 停止捕获波形
|
/// 停止捕获波形
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 停止捕获_STOP(CancellationToken ct = default)
|
public virtual async Task 停止捕获_STOP(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"TRIGger:STOP{ScpiDelimiter}", ct);
|
await SendAsync($"TRIGger:STOP{ScpiDelimiter}", ct);
|
||||||
@@ -86,6 +120,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 强制示波器进入单次触发捕获模式
|
/// 强制示波器进入单次触发捕获模式
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 单次触发_SINGLE(CancellationToken ct = default)
|
public virtual async Task 单次触发_SINGLE(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":TRIGger:MODE SINGle{ScpiDelimiter}", ct);
|
await SendAsync($":TRIGger:MODE SINGle{ScpiDelimiter}", ct);
|
||||||
@@ -94,6 +129,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 触发一次波形采样
|
/// 触发一次波形采样
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 强制触发(CancellationToken ct = default)
|
public virtual async Task 强制触发(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"::TRIGger:MODE FTRIG{ScpiDelimiter}", ct);
|
await SendAsync($"::TRIGger:MODE FTRIG{ScpiDelimiter}", ct);
|
||||||
@@ -136,6 +172,9 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 开启或关闭指定的模拟通道 (符合手册 57 页规范)
|
/// 开启或关闭指定的模拟通道 (符合手册 57 页规范)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="enable">true: 开启通道, false: 关闭通道</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default)
|
public virtual async Task 设置通道开关(int channel, bool enable, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string state = enable ? "ON" : "OFF";
|
string state = enable ? "ON" : "OFF";
|
||||||
@@ -145,6 +184,9 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定通道的垂直电压档位 (Volts/Div)
|
/// 设置指定通道的垂直电压档位 (Volts/Div)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="volts">电压档位 (Volts/Div)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置通道电压档位(int channel, double volts, CancellationToken ct = default)
|
public virtual async Task 设置通道电压档位(int channel, double volts, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:VDIV {1:F4}{2}", channel, volts, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:VDIV {1:F4}{2}", channel, volts, ScpiDelimiter);
|
||||||
@@ -154,14 +196,21 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道当前的电压档位
|
/// 查询指定通道当前的电压档位
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询通道电压档位(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>当前电压档位值 (Volts/Div)</returns>
|
||||||
|
public virtual async Task<double> 查询通道电压档位(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"C{channel}:VDIV?{ScpiDelimiter}", ScpiDelimiter, ct);
|
string res = await WriteReadAsync($"C{channel}:VDIV?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
|
return ExtractDouble(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置指定通道的垂直偏移量 (Offset)
|
/// 设置指定通道的垂直偏移量 (Offset)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="offset">垂直偏移量 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置通道垂直偏移(int channel, double offset, CancellationToken ct = default)
|
public virtual async Task 设置通道垂直偏移(int channel, double offset, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:OFST {1:F4}{2}", channel, offset, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "C{0}:OFST {1:F4}{2}", channel, offset, ScpiDelimiter);
|
||||||
@@ -178,6 +227,13 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>1MΩ 阻抗</summary>
|
/// <summary>1MΩ 阻抗</summary>
|
||||||
ONEM
|
ONEM
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 设置指定通道的输入阻抗类型
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="impedance">阻抗类型枚举 (FIFty: 50Ω, ONEM: 1MΩ)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置通道阻抗(int channel, ImpedanceType impedance, CancellationToken ct = default)
|
public virtual async Task 设置通道阻抗(int channel, ImpedanceType impedance, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 例如发送:C1:IMPedance FIFty\n 或 C1:IMPedance ONEM\n
|
// 例如发送:C1:IMPedance FIFty\n 或 C1:IMPedance ONEM\n
|
||||||
@@ -192,6 +248,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置示波器的水平时基档位 (Time/Div)
|
/// 设置示波器的水平时基档位 (Time/Div)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="scale">水平时基档位 (Time/Div)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置水平时基(double scale, CancellationToken ct = default)
|
public virtual async Task 设置水平时基(double scale, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "TDIV {0:E6}{1}", scale, ScpiDelimiter);
|
||||||
@@ -201,6 +259,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置示波器的触发水平延迟位置 (Horizontal Delay)
|
/// 设置示波器的触发水平延迟位置 (Horizontal Delay)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="delay">水平延迟位置 (单位: s)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置水平延迟(double delay, CancellationToken ct = default)
|
public virtual async Task 设置水平延迟(double delay, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter);
|
string cmd = string.Format(CultureInfo.InvariantCulture, "TRDL {0:E6}{1}", delay, ScpiDelimiter);
|
||||||
@@ -214,6 +274,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置边沿触发的电平值 (Trigger Level)
|
/// 设置边沿触发的电平值 (Trigger Level)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="level">触发电平值 (单位: V)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置触发电平(double level, CancellationToken ct = default)
|
public virtual async Task 设置触发电平(double level, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
|
|
||||||
@@ -224,6 +286,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 设置边沿触发的触发源 (例如: C1, C2, C3, C4, EX, LINE)
|
/// 设置边沿触发的触发源 (例如: C1, C2, C3, C4, EX, LINE)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="source">触发源通道标识 (例如: "C1", "C2", "EX", "LINE")</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置触发源(string source, CancellationToken ct = default)
|
public virtual async Task 设置触发源(string source, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"TRSE EDGE,SR,{source.ToUpper()}{ScpiDelimiter}", ct);
|
await SendAsync($"TRSE EDGE,SR,{source.ToUpper()}{ScpiDelimiter}", ct);
|
||||||
@@ -236,16 +300,24 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道自动测量项的当前实时测量数值
|
/// 查询指定通道自动测量项的当前实时测量数值
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询通道测量项参数(int channel, string paramName, CancellationToken ct = default)
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="paramName">测量参数名称 (如 "PKPK", "FREQ", "RMS" 等)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>测量参数的实时数值 (double)</returns>
|
||||||
|
public virtual async Task<double> 查询通道测量项参数(int channel, string paramName, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, "C{0}:PAVA? {1}{2}", channel, paramName.ToUpper(), ScpiDelimiter);
|
string query = string.Format(CultureInfo.InvariantCulture, "C{0}:PAVA? {1}{2}", channel, paramName.ToUpper(), ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
string res = await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
|
return ExtractDouble(res);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的电压峰峰值 (Vpp)
|
/// 查询指定通道的电压峰峰值 (Vpp)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际电压峰峰值(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>电压峰峰值 (Vpp)</returns>
|
||||||
|
public virtual async Task<double> 查询实际电压峰峰值(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await 查询通道测量项参数(channel, "PKPK", ct);
|
return await 查询通道测量项参数(channel, "PKPK", ct);
|
||||||
}
|
}
|
||||||
@@ -253,7 +325,10 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的频率值 (Frequency)
|
/// 查询指定通道的频率值 (Frequency)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际频率(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>频率值 (单位: Hz)</returns>
|
||||||
|
public virtual async Task<double> 查询实际频率(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await 查询通道测量项参数(channel, "FREQ", ct);
|
return await 查询通道测量项参数(channel, "FREQ", ct);
|
||||||
}
|
}
|
||||||
@@ -261,7 +336,10 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的真均方根电压值 (Vrms)
|
/// 查询指定通道的真均方根电压值 (Vrms)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际电压均方根(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号 (1 - 4)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>真均方根电压值 (Vrms)</returns>
|
||||||
|
public virtual async Task<double> 查询实际电压均方根(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await 查询通道测量项参数(channel, "RMS", ct);
|
return await 查询通道测量项参数(channel, "RMS", ct);
|
||||||
}
|
}
|
||||||
@@ -269,6 +347,7 @@ namespace DeviceCommand.Devices
|
|||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 7. 屏幕截图导出子系统
|
#region 7. 屏幕截图导出子系统
|
||||||
|
/// <summary>
|
||||||
/// 【一键获取并保存截图】
|
/// 【一键获取并保存截图】
|
||||||
/// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到指定路径。
|
/// 自动下发 :PRINt? PNG 指令,接收示波器返回的纯 PNG 二进制流并直接保存到指定路径。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -279,6 +358,14 @@ namespace DeviceCommand.Devices
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
string FileDirectory = Path.GetDirectoryName(saveFilePath) ?? ""; // 获取目录
|
||||||
|
string fileNameWithoutExt = Path.GetFileNameWithoutExtension(saveFilePath); // 获取纯文件名
|
||||||
|
string extension = Path.GetExtension(saveFilePath); // 获取后缀名 (如 .png)
|
||||||
|
// 生成时间戳,格式为 年月日_时分秒 (例如 20260817_123045)
|
||||||
|
string timeStamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||||
|
|
||||||
|
// 组合成新的路径
|
||||||
|
saveFilePath = Path.Combine(FileDirectory, $"{fileNameWithoutExt}_{timeStamp}{extension}");
|
||||||
// 1. 调用我们在基类 Tcp 中全新实现的 ReadRawAsync
|
// 1. 调用我们在基类 Tcp 中全新实现的 ReadRawAsync
|
||||||
// 它会在内部下发 ":PRINt? PNG\n",自动接收完整的网络字节流
|
// 它会在内部下发 ":PRINt? PNG\n",自动接收完整的网络字节流
|
||||||
byte[] pureImageBytes = await ReadAllBytesAsync($":PRINt? PNG{ScpiDelimiter}", ct);
|
byte[] pureImageBytes = await ReadAllBytesAsync($":PRINt? PNG{ScpiDelimiter}", ct);
|
||||||
@@ -323,4 +410,4 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,11 +22,27 @@ namespace DeviceCommand.Devices
|
|||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 辅助方法:解析科学计数法字符串为普通 double。解析失败返回 double.NaN。
|
||||||
|
/// </summary>
|
||||||
|
private double ParseScientificResponse(string response)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(response)) return double.NaN;
|
||||||
|
|
||||||
|
// 使用 NumberStyles.Float 允许原生的科学计数法解析 (例如 "1.23E+04")
|
||||||
|
if (double.TryParse(response.Trim(), NumberStyles.Float, CultureInfo.InvariantCulture, out double result))
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return double.NaN;
|
||||||
|
}
|
||||||
|
|
||||||
#region 1. IEEE 488.2 公共命令
|
#region 1. IEEE 488.2 公共命令
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清除状态命令。清除标准事件状态寄存器和错误队列
|
/// 清除状态命令。清除标准事件状态寄存器和错误队列
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
|
public virtual async Task 清除错误队列和状态字节(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
await SendAsync($"*CLS{ScpiDelimiter}", ct);
|
||||||
@@ -42,6 +58,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取功率分析仪识别字符串(制造商、产品型号、系统 SN、软件版本号)
|
/// 读取功率分析仪识别字符串(制造商、产品型号、系统 SN、软件版本号)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>设备标识字符串(制造商, 产品型号, 系统 SN, 软件版本号)</returns>
|
||||||
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备标识(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"*IDN?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -50,6 +68,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 复位命令。使功率分析仪恢复到出厂默认配置状态
|
/// 复位命令。使功率分析仪恢复到出厂默认配置状态
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 重置设备(CancellationToken ct = default)
|
public virtual async Task 重置设备(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
await SendAsync($"*RST{ScpiDelimiter}", ct);
|
||||||
@@ -58,6 +77,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 读取标准状态字节寄存器
|
/// 读取标准状态字节寄存器
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>标准状态字节寄存器值</returns>
|
||||||
public virtual async Task<string> 读取状态字节(CancellationToken ct = default)
|
public virtual async Task<string> 读取状态字节(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct);
|
return await WriteReadAsync($"*STB?{ScpiDelimiter}", ScpiDelimiter, ct);
|
||||||
@@ -70,7 +91,10 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的实时 RMS 电压值 (单位: V)
|
/// 查询指定通道的实时 RMS 电压值 (单位: V)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际电压(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号/单元号</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实时 RMS 电压值 (单位: V)</returns>
|
||||||
|
public virtual async Task<double> 查询实际电压(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 1. 先配置 ITEM1 为指定通道的电压有效值 (URMS)
|
// 1. 先配置 ITEM1 为指定通道的电压有效值 (URMS)
|
||||||
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 URMS,{0}{1}", channel, ScpiDelimiter);
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 URMS,{0}{1}", channel, ScpiDelimiter);
|
||||||
@@ -78,13 +102,17 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
// 2. 再读取 ITEM1 的值
|
// 2. 再读取 ITEM1 的值
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
string response = await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
|
return ParseScientificResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的实时 RMS 电流值 (单位: A)
|
/// 查询指定通道的实时 RMS 电流值 (单位: A)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际电流(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号/单元号</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实时 RMS 电流值 (单位: A)</returns>
|
||||||
|
public virtual async Task<double> 查询实际电流(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 1. 配置 ITEM1 为指定通道的电流有效值 (IRMS)
|
// 1. 配置 ITEM1 为指定通道的电流有效值 (IRMS)
|
||||||
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 IRMS,{0}{1}", channel, ScpiDelimiter);
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 IRMS,{0}{1}", channel, ScpiDelimiter);
|
||||||
@@ -92,13 +120,17 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
// 2. 读取 ITEM1 的值
|
// 2. 读取 ITEM1 的值
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
string response = await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
|
return ParseScientificResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的实时有功功率值 (单位: W)
|
/// 查询指定通道的实时有功功率值 (单位: W)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询实际功率(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号/单元号</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实时有功功率值 (单位: W)</returns>
|
||||||
|
public virtual async Task<double> 查询实际功率(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 1. 配置 ITEM1 为指定通道的有功功率 (P)
|
// 1. 配置 ITEM1 为指定通道的有功功率 (P)
|
||||||
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 P,{0}{1}", channel, ScpiDelimiter);
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 P,{0}{1}", channel, ScpiDelimiter);
|
||||||
@@ -106,13 +138,17 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
// 2. 读取 ITEM1 的值
|
// 2. 读取 ITEM1 的值
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
string response = await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
|
return ParseScientificResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的实时频率值 (单位: Hz)
|
/// 查询指定通道的实时频率值 (单位: Hz)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询频率(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号/单元号</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>实时频率值 (单位: Hz)</returns>
|
||||||
|
public virtual async Task<double> 查询频率(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 1. 配置 ITEM1 为指定通道的电压频率 (FU)
|
// 1. 配置 ITEM1 为指定通道的电压频率 (FU)
|
||||||
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 FU,{0}{1}", channel, ScpiDelimiter);
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 FU,{0}{1}", channel, ScpiDelimiter);
|
||||||
@@ -120,13 +156,17 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
// 2. 读取 ITEM1 的值
|
// 2. 读取 ITEM1 的值
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
string response = await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
|
return ParseScientificResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询指定通道的功率因数 (Power Factor)
|
/// 查询指定通道的功率因数 (Power Factor)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual async Task<string> 查询功率因数(int channel, CancellationToken ct = default)
|
/// <param name="channel">通道号/单元号</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>功率因数值</returns>
|
||||||
|
public virtual async Task<double> 查询功率因数(int channel, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
// 1. 配置 ITEM1 为指定通道的功率因数 (LAMBda)
|
// 1. 配置 ITEM1 为指定通道的功率因数 (LAMBda)
|
||||||
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 LAMBda,{0}{1}", channel, ScpiDelimiter);
|
string setItem = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:ITEM1 LAMBda,{0}{1}", channel, ScpiDelimiter);
|
||||||
@@ -134,7 +174,8 @@ namespace DeviceCommand.Devices
|
|||||||
|
|
||||||
// 2. 读取 ITEM1 的值
|
// 2. 读取 ITEM1 的值
|
||||||
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
string query = string.Format(CultureInfo.InvariantCulture, ":NUMeric:NORMal:VALue? 1{0}", ScpiDelimiter);
|
||||||
return await WriteReadAsync(query, ScpiDelimiter, ct);
|
string response = await WriteReadAsync(query, ScpiDelimiter, ct);
|
||||||
|
return ParseScientificResponse(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
@@ -204,6 +245,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 14. 查询仪器型号名称
|
/// 14. 查询仪器型号名称
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>仪器型号名称字符串</returns>
|
||||||
public virtual async Task<string> 查询设备型号(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备型号(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":SYSTem:MODel?{ScpiDelimiter}", ScpiDelimiter, ct); //
|
return await WriteReadAsync($":SYSTem:MODel?{ScpiDelimiter}", ScpiDelimiter, ct); //
|
||||||
@@ -212,6 +255,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 16. 查询仪器唯一序列号
|
/// 16. 查询仪器唯一序列号
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
|
/// <returns>仪器唯一序列号字符串</returns>
|
||||||
public virtual async Task<string> 查询设备序列号(CancellationToken ct = default)
|
public virtual async Task<string> 查询设备序列号(CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return await WriteReadAsync($":SYSTem:SERial?{ScpiDelimiter}", ScpiDelimiter, ct); //
|
return await WriteReadAsync($":SYSTem:SERial?{ScpiDelimiter}", ScpiDelimiter, ct); //
|
||||||
@@ -221,6 +266,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// 15. 设置数值数据显示的分辨率 (5位或6位)
|
/// 15. 设置数值数据显示的分辨率 (5位或6位)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="resolution">有效值只能为 5 或 6</param>
|
/// <param name="resolution">有效值只能为 5 或 6</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置显示分辨率(int resolution, CancellationToken ct = default)
|
public virtual async Task 设置显示分辨率(int resolution, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (resolution != 5 && resolution != 6) throw new ArgumentException("分辨率只能设置为 5 或 6 位");
|
if (resolution != 5 && resolution != 6) throw new ArgumentException("分辨率只能设置为 5 或 6 位");
|
||||||
@@ -230,6 +276,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 13. 设置或查询屏幕 LCD 的亮度级别 (1-10)
|
/// 13. 设置或查询屏幕 LCD 的亮度级别 (1-10)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="brightness">亮度级别 (范围: 1 - 10)</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置显示亮度(int brightness, CancellationToken ct = default)
|
public virtual async Task 设置显示亮度(int brightness, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
if (brightness < 1 || brightness > 10) throw new ArgumentOutOfRangeException(nameof(brightness), "亮度范围必须在 1~10 之间");
|
if (brightness < 1 || brightness > 10) throw new ArgumentOutOfRangeException(nameof(brightness), "亮度范围必须在 1~10 之间");
|
||||||
@@ -239,6 +287,8 @@ namespace DeviceCommand.Devices
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 18. 设置或查询触摸锁的开/关状态 (锁定时防止人工误触触控屏)
|
/// 18. 设置或查询触摸锁的开/关状态 (锁定时防止人工误触触控屏)
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="isLocked">true: 锁定触摸, false: 解锁触摸</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置屏幕触摸锁定(bool isLocked, CancellationToken ct = default)
|
public virtual async Task 设置屏幕触摸锁定(bool isLocked, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
string state = isLocked ? "ON" : "OFF";
|
string state = isLocked ? "ON" : "OFF";
|
||||||
@@ -249,6 +299,7 @@ namespace DeviceCommand.Devices
|
|||||||
/// 17. 设置或读取分析仪当前的系统内部时间
|
/// 17. 设置或读取分析仪当前的系统内部时间
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="timeStr">格式必须为 "HH:MM:SS"</param>
|
/// <param name="timeStr">格式必须为 "HH:MM:SS"</param>
|
||||||
|
/// <param name="ct">取消令牌</param>
|
||||||
public virtual async Task 设置系统时间(string timeStr, CancellationToken ct = default)
|
public virtual async Task 设置系统时间(string timeStr, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await SendAsync($":SYSTem:TIME \"{timeStr}\"{ScpiDelimiter}", ct); //
|
await SendAsync($":SYSTem:TIME \"{timeStr}\"{ScpiDelimiter}", ct); //
|
||||||
|
|||||||
@@ -2,17 +2,31 @@
|
|||||||
using NModbus;
|
using NModbus;
|
||||||
using NModbus.Serial;
|
using NModbus.Serial;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.IO.Ports;
|
using System.IO.Ports;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace DeviceCommand.Flexible
|
namespace DeviceCommand.Flexible
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 灵活型 Modbus RTU 串口通信类(免实例化,每次调用临时创建串口连接)。
|
||||||
|
/// 支持保持寄存器与线圈的读写操作,内部使用按串口名称粒度的通信锁保证同一串口同一时刻只有一个事务在执行,
|
||||||
|
/// 不同串口之间互不阻塞,适用于偶发性、无需保持长连接的 Modbus RTU 设备读写场景。
|
||||||
|
/// </summary>
|
||||||
[ADPCommand]
|
[ADPCommand]
|
||||||
public static class FModbusRTU
|
public static class FModbusRTU
|
||||||
{
|
{
|
||||||
private static readonly SemaphoreSlim _commLock = new(1, 1);
|
// 按串口名称粒度的通信锁:同一串口同一时刻只有一个事务在执行,不同串口互不阻塞
|
||||||
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
|
||||||
|
|
||||||
|
/// <summary>获取指定串口的通信锁(不存在则自动创建)</summary>
|
||||||
|
private static SemaphoreSlim GetLock(string portName)
|
||||||
|
=> _commLocks.GetOrAdd(portName, _ => new SemaphoreSlim(1, 1));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建串口实例并配置超时参数。
|
||||||
|
/// </summary>
|
||||||
private static SerialPort CreatePort(
|
private static SerialPort CreatePort(
|
||||||
string portName,
|
string portName,
|
||||||
int baudRate,
|
int baudRate,
|
||||||
@@ -29,6 +43,9 @@ namespace DeviceCommand.Flexible
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 基于指定串口创建 Modbus RTU 主站。
|
||||||
|
/// </summary>
|
||||||
private static IModbusMaster CreateMaster(SerialPort port)
|
private static IModbusMaster CreateMaster(SerialPort port)
|
||||||
{
|
{
|
||||||
return new ModbusFactory().CreateRtuMaster(port);
|
return new ModbusFactory().CreateRtuMaster(port);
|
||||||
@@ -36,7 +53,22 @@ namespace DeviceCommand.Flexible
|
|||||||
|
|
||||||
#region Holding Register
|
#region Holding Register
|
||||||
|
|
||||||
public static async Task<ushort[]> ReadHoldingRegistersAsync(
|
/// <summary>
|
||||||
|
/// 读取保持寄存器(功能码 03),返回指定数量的寄存器值数组。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||||
|
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||||
|
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
|
||||||
|
/// <param name="startAddress">起始寄存器地址</param>
|
||||||
|
/// <param name="numberOfPoints">读取的寄存器数量</param>
|
||||||
|
/// <param name="dataBits">数据位,默认 8</param>
|
||||||
|
/// <param name="stopBits">停止位,默认 1 位</param>
|
||||||
|
/// <param name="parity">校验位,默认无校验</param>
|
||||||
|
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>读取到的寄存器值数组</returns>
|
||||||
|
public static async Task<ushort[]> 读保持寄存器(
|
||||||
string portName,
|
string portName,
|
||||||
int baudRate,
|
int baudRate,
|
||||||
byte slaveAddress,
|
byte slaveAddress,
|
||||||
@@ -49,7 +81,7 @@ namespace DeviceCommand.Flexible
|
|||||||
int writeTimeout = 3000,
|
int writeTimeout = 3000,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(portName).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var port = CreatePort(
|
using var port = CreatePort(
|
||||||
@@ -69,11 +101,25 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(portName).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task WriteSingleRegisterAsync(
|
/// <summary>
|
||||||
|
/// 写入单个保持寄存器(功能码 06)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||||
|
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||||
|
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
|
||||||
|
/// <param name="registerAddress">要写入的寄存器地址</param>
|
||||||
|
/// <param name="value">要写入的寄存器值 (0-65535)</param>
|
||||||
|
/// <param name="dataBits">数据位,默认 8</param>
|
||||||
|
/// <param name="stopBits">停止位,默认 1 位</param>
|
||||||
|
/// <param name="parity">校验位,默认无校验</param>
|
||||||
|
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
public static async Task 写单个寄存器(
|
||||||
string portName,
|
string portName,
|
||||||
int baudRate,
|
int baudRate,
|
||||||
byte slaveAddress,
|
byte slaveAddress,
|
||||||
@@ -86,7 +132,7 @@ namespace DeviceCommand.Flexible
|
|||||||
int writeTimeout = 3000,
|
int writeTimeout = 3000,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(portName).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var port = CreatePort(
|
using var port = CreatePort(
|
||||||
@@ -106,7 +152,7 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(portName).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +160,22 @@ namespace DeviceCommand.Flexible
|
|||||||
|
|
||||||
#region Coil
|
#region Coil
|
||||||
|
|
||||||
public static async Task<bool[]> ReadCoilsAsync(
|
/// <summary>
|
||||||
|
/// 读取线圈状态(功能码 01),返回指定数量的线圈开关状态数组。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||||
|
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||||
|
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
|
||||||
|
/// <param name="startAddress">起始线圈地址</param>
|
||||||
|
/// <param name="numberOfPoints">读取的线圈数量</param>
|
||||||
|
/// <param name="dataBits">数据位,默认 8</param>
|
||||||
|
/// <param name="stopBits">停止位,默认 1 位</param>
|
||||||
|
/// <param name="parity">校验位,默认无校验</param>
|
||||||
|
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>线圈状态数组,true 表示吸合/导通</returns>
|
||||||
|
public static async Task<bool[]> 读线圈(
|
||||||
string portName,
|
string portName,
|
||||||
int baudRate,
|
int baudRate,
|
||||||
byte slaveAddress,
|
byte slaveAddress,
|
||||||
@@ -127,7 +188,7 @@ namespace DeviceCommand.Flexible
|
|||||||
int writeTimeout = 3000,
|
int writeTimeout = 3000,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(portName).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var port = CreatePort(
|
using var port = CreatePort(
|
||||||
@@ -147,11 +208,25 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(portName).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task WriteSingleCoilAsync(
|
/// <summary>
|
||||||
|
/// 写入单个线圈(功能码 05),控制单个开关量输出。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||||
|
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||||
|
/// <param name="slaveAddress">Modbus 从站站号 (1-247)</param>
|
||||||
|
/// <param name="coilAddress">要写入的线圈地址</param>
|
||||||
|
/// <param name="value">线圈状态(true: 吸合/导通, false: 断开)</param>
|
||||||
|
/// <param name="dataBits">数据位,默认 8</param>
|
||||||
|
/// <param name="stopBits">停止位,默认 1 位</param>
|
||||||
|
/// <param name="parity">校验位,默认无校验</param>
|
||||||
|
/// <param name="readTimeout">读取超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="writeTimeout">写入超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
public static async Task 写单个线圈(
|
||||||
string portName,
|
string portName,
|
||||||
int baudRate,
|
int baudRate,
|
||||||
byte slaveAddress,
|
byte slaveAddress,
|
||||||
@@ -164,7 +239,7 @@ namespace DeviceCommand.Flexible
|
|||||||
int writeTimeout = 3000,
|
int writeTimeout = 3000,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(portName).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var port = CreatePort(
|
using var port = CreatePort(
|
||||||
@@ -184,7 +259,7 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(portName).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using Common.Attributes;
|
using Common.Attributes;
|
||||||
using NModbus;
|
using NModbus;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -8,11 +9,24 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace DeviceCommand.Flexible
|
namespace DeviceCommand.Flexible
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 灵活型 Modbus TCP 通信类(免实例化,每次调用临时建立 TCP 连接)。
|
||||||
|
/// 支持保持寄存器与线圈的读写操作,内部使用按端点粒度的通信锁保证同一端点同一时刻只有一个事务在执行,
|
||||||
|
/// 不同端点之间互不阻塞,适用于偶发性、无需保持长连接的 Modbus TCP 设备读写场景。
|
||||||
|
/// </summary>
|
||||||
[ADPCommand]
|
[ADPCommand]
|
||||||
public static class FModbusTCP
|
public static class FModbusTCP
|
||||||
{
|
{
|
||||||
private static readonly SemaphoreSlim _commLock = new(1, 1);
|
// 按端点粒度的通信锁:同一端点同一时刻只有一个事务在执行,不同端点互不阻塞
|
||||||
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
|
||||||
|
|
||||||
|
/// <summary>获取指定端点的通信锁(不存在则自动创建)</summary>
|
||||||
|
private static SemaphoreSlim GetLock(string ipAddress, int port)
|
||||||
|
=> _commLocks.GetOrAdd($"{ipAddress}:{port}", _ => new SemaphoreSlim(1, 1));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 建立 TCP 连接并创建 Modbus TCP 主站。
|
||||||
|
/// </summary>
|
||||||
private static async Task<IModbusMaster> ConnectAsync(string ipAddress, int port, int sendTimeout, int receiveTimeout, CancellationToken ct)
|
private static async Task<IModbusMaster> ConnectAsync(string ipAddress, int port, int sendTimeout, int receiveTimeout, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var tcpClient = new TcpClient();
|
var tcpClient = new TcpClient();
|
||||||
@@ -26,7 +40,19 @@ namespace DeviceCommand.Flexible
|
|||||||
|
|
||||||
#region Holding Registers
|
#region Holding Registers
|
||||||
|
|
||||||
public static async Task<ushort[]> ReadHoldingRegistersAsync(
|
/// <summary>
|
||||||
|
/// 读取保持寄存器(功能码 03),返回指定数量的寄存器值数组。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">设备 IP 地址</param>
|
||||||
|
/// <param name="port">Modbus TCP 端口号,默认常用 502</param>
|
||||||
|
/// <param name="slaveAddress">Modbus 从站站号(单元标识符)</param>
|
||||||
|
/// <param name="startAddress">起始寄存器地址</param>
|
||||||
|
/// <param name="numberOfPoints">读取的寄存器数量</param>
|
||||||
|
/// <param name="sendTimeout">发送/连接超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="receiveTimeout">接收超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>读取到的寄存器值数组</returns>
|
||||||
|
public static async Task<ushort[]> 读保持寄存器(
|
||||||
string ipAddress,
|
string ipAddress,
|
||||||
int port,
|
int port,
|
||||||
byte slaveAddress,
|
byte slaveAddress,
|
||||||
@@ -36,7 +62,7 @@ namespace DeviceCommand.Flexible
|
|||||||
int receiveTimeout = 3000,
|
int receiveTimeout = 3000,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
||||||
@@ -47,11 +73,22 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(ipAddress, port).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task WriteSingleRegisterAsync(
|
/// <summary>
|
||||||
|
/// 写入单个保持寄存器(功能码 06)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">设备 IP 地址</param>
|
||||||
|
/// <param name="port">Modbus TCP 端口号,默认常用 502</param>
|
||||||
|
/// <param name="slaveAddress">Modbus 从站站号(单元标识符)</param>
|
||||||
|
/// <param name="registerAddress">要写入的寄存器地址</param>
|
||||||
|
/// <param name="value">要写入的寄存器值 (0-65535)</param>
|
||||||
|
/// <param name="sendTimeout">发送/连接超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="receiveTimeout">接收超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
public static async Task 写单个寄存器(
|
||||||
string ipAddress,
|
string ipAddress,
|
||||||
int port,
|
int port,
|
||||||
byte slaveAddress,
|
byte slaveAddress,
|
||||||
@@ -61,7 +98,7 @@ namespace DeviceCommand.Flexible
|
|||||||
int receiveTimeout = 3000,
|
int receiveTimeout = 3000,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
||||||
@@ -71,7 +108,7 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(ipAddress, port).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,7 +116,19 @@ namespace DeviceCommand.Flexible
|
|||||||
|
|
||||||
#region Coils
|
#region Coils
|
||||||
|
|
||||||
public static async Task<bool[]> ReadCoilsAsync(
|
/// <summary>
|
||||||
|
/// 读取线圈状态(功能码 01),返回指定数量的线圈开关状态数组。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">设备 IP 地址</param>
|
||||||
|
/// <param name="port">Modbus TCP 端口号,默认常用 502</param>
|
||||||
|
/// <param name="slaveAddress">Modbus 从站站号(单元标识符)</param>
|
||||||
|
/// <param name="startAddress">起始线圈地址</param>
|
||||||
|
/// <param name="numberOfPoints">读取的线圈数量</param>
|
||||||
|
/// <param name="sendTimeout">发送/连接超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="receiveTimeout">接收超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>线圈状态数组,true 表示吸合/导通</returns>
|
||||||
|
public static async Task<bool[]> 读线圈(
|
||||||
string ipAddress,
|
string ipAddress,
|
||||||
int port,
|
int port,
|
||||||
byte slaveAddress,
|
byte slaveAddress,
|
||||||
@@ -89,7 +138,7 @@ namespace DeviceCommand.Flexible
|
|||||||
int receiveTimeout = 3000,
|
int receiveTimeout = 3000,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
||||||
@@ -100,11 +149,22 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(ipAddress, port).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task WriteSingleCoilAsync(
|
/// <summary>
|
||||||
|
/// 写入单个线圈(功能码 05),控制单个开关量输出。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">设备 IP 地址</param>
|
||||||
|
/// <param name="port">Modbus TCP 端口号,默认常用 502</param>
|
||||||
|
/// <param name="slaveAddress">Modbus 从站站号(单元标识符)</param>
|
||||||
|
/// <param name="coilAddress">要写入的线圈地址</param>
|
||||||
|
/// <param name="value">线圈状态(true: 吸合/导通, false: 断开)</param>
|
||||||
|
/// <param name="sendTimeout">发送/连接超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="receiveTimeout">接收超时时间(毫秒),默认 3000</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
public static async Task 写单个线圈(
|
||||||
string ipAddress,
|
string ipAddress,
|
||||||
int port,
|
int port,
|
||||||
byte slaveAddress,
|
byte slaveAddress,
|
||||||
@@ -114,7 +174,7 @@ namespace DeviceCommand.Flexible
|
|||||||
int receiveTimeout = 3000,
|
int receiveTimeout = 3000,
|
||||||
CancellationToken ct = default)
|
CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
using var master = await ConnectAsync(ipAddress, port, sendTimeout, receiveTimeout, ct) as IDisposable;
|
||||||
@@ -124,7 +184,7 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(ipAddress, port).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using Common.Attributes;
|
using Common.Attributes;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.IO.Ports;
|
using System.IO.Ports;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -7,11 +8,25 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace DeviceCommand.Flexible
|
namespace DeviceCommand.Flexible
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 灵活型串口通信类(免实例化,每次调用临时创建串口连接)。
|
||||||
|
/// 支持只发送指令、发送并读取应答两种最常用操作,
|
||||||
|
/// 内部使用按串口名称粒度的通信锁保证同一串口同一时刻只有一个事务在执行,
|
||||||
|
/// 不同串口之间互不阻塞,适用于偶发性、无需保持长连接的串口设备通信场景(如示波器、电源的 SCPI 指令)。
|
||||||
|
/// </summary>
|
||||||
[ADPCommand]
|
[ADPCommand]
|
||||||
public static class FSerialPort
|
public static class FSerialPort
|
||||||
{
|
{
|
||||||
private static readonly SemaphoreSlim _commLock = new(1, 1);
|
// 按串口名称粒度的通信锁:同一串口同一时刻只有一个事务在执行,不同串口互不阻塞
|
||||||
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
|
||||||
|
|
||||||
|
/// <summary>获取指定串口的通信锁(不存在则自动创建)</summary>
|
||||||
|
private static SemaphoreSlim GetLock(string portName)
|
||||||
|
=> _commLocks.GetOrAdd(portName, _ => new SemaphoreSlim(1, 1));
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建串口实例并配置 UTF8 编码与超时参数。
|
||||||
|
/// </summary>
|
||||||
private static SerialPort CreatePort(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, int sendTimeout,int receiveTimeout)
|
private static SerialPort CreatePort(string portName, int baudRate, Parity parity, int dataBits, StopBits stopBits, int sendTimeout,int receiveTimeout)
|
||||||
{
|
{
|
||||||
return new SerialPort(portName, baudRate, parity, dataBits, stopBits)
|
return new SerialPort(portName, baudRate, parity, dataBits, stopBits)
|
||||||
@@ -24,9 +39,21 @@ namespace DeviceCommand.Flexible
|
|||||||
|
|
||||||
#region 最常用:只发送字符串
|
#region 最常用:只发送字符串
|
||||||
|
|
||||||
public static async Task SendAsync(string portName,int baudRate,Parity parity,int dataBits,StopBits stopBits,int sendTimeout,int receiveTimeout,string command,CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 向串口发送一条字符串指令(只发送,不等待应答)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||||
|
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||||
|
/// <param name="parity">校验位</param>
|
||||||
|
/// <param name="dataBits">数据位,常用 8</param>
|
||||||
|
/// <param name="stopBits">停止位</param>
|
||||||
|
/// <param name="sendTimeout">发送超时时间(毫秒)</param>
|
||||||
|
/// <param name="receiveTimeout">接收超时时间(毫秒)</param>
|
||||||
|
/// <param name="command">要发送的指令字符串</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
public static async Task 发送指令(string portName,int baudRate,Parity parity,int dataBits,StopBits stopBits,int sendTimeout,int receiveTimeout,string command,CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(portName).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var port = CreatePort(
|
using var port = CreatePort(
|
||||||
@@ -44,7 +71,7 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(portName).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,9 +79,23 @@ namespace DeviceCommand.Flexible
|
|||||||
|
|
||||||
#region 最常用:发送字符串并读取字符串
|
#region 最常用:发送字符串并读取字符串
|
||||||
|
|
||||||
public static async Task<string> SendReadAsync(string portName,int baudRate, Parity parity, int dataBits, StopBits stopBits,int sendTimeout, int receiveTimeout,string command,string delimiter = "\n", CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 向串口发送一条字符串指令并读取应答,读取到结束符为止(应答不含结束符)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="portName">串口名称,如 "COM1"</param>
|
||||||
|
/// <param name="baudRate">波特率,如 9600、115200</param>
|
||||||
|
/// <param name="parity">校验位</param>
|
||||||
|
/// <param name="dataBits">数据位,常用 8</param>
|
||||||
|
/// <param name="stopBits">停止位</param>
|
||||||
|
/// <param name="sendTimeout">发送超时时间(毫秒)</param>
|
||||||
|
/// <param name="receiveTimeout">接收超时时间(毫秒),超时未收到结束符抛出超时异常</param>
|
||||||
|
/// <param name="command">要发送的指令字符串</param>
|
||||||
|
/// <param name="delimiter">应答结束符,默认换行符 "\n"</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>去除结束符并去除首尾空白后的应答字符串</returns>
|
||||||
|
public static async Task<string> 发送并读取应答(string portName,int baudRate, Parity parity, int dataBits, StopBits stopBits,int sendTimeout, int receiveTimeout,string command,string delimiter = "\n", CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(portName).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var port = CreatePort(
|
using var port = CreatePort(
|
||||||
@@ -95,7 +136,7 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(portName).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,39 @@
|
|||||||
using Common.Attributes;
|
using Common.Attributes;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Net.Sockets;
|
using System.Net.Sockets;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace DeviceCommand.Flexible
|
namespace DeviceCommand.Flexible
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 灵活型 TCP 通信类(免实例化,每次调用临时建立 TCP 连接)。
|
||||||
|
/// 支持字节/文本发送、定长字节读取、按结束符读取文本行四种操作,
|
||||||
|
/// 内部使用按端点粒度的通信锁保证同一端点同一时刻只有一个 TCP 事务在执行,
|
||||||
|
/// 不同端点之间互不阻塞,适用于偶发性、无需保持长连接的 TCP 设备通信场景。
|
||||||
|
/// </summary>
|
||||||
[ADPCommand]
|
[ADPCommand]
|
||||||
public static class FTCP
|
public static class FTCP
|
||||||
{
|
{
|
||||||
private static readonly SemaphoreSlim _commLock = new(1, 1);
|
// 按端点粒度的通信锁:同一端点同一时刻只有一个事务在执行,不同端点互不阻塞
|
||||||
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _commLocks = new();
|
||||||
|
|
||||||
|
/// <summary>获取指定端点的通信锁(不存在则自动创建)</summary>
|
||||||
|
private static SemaphoreSlim GetLock(string ipAddress, int port)
|
||||||
|
=> _commLocks.GetOrAdd($"{ipAddress}:{port}", _ => new SemaphoreSlim(1, 1));
|
||||||
|
|
||||||
#region Send
|
#region Send
|
||||||
|
|
||||||
public static async Task SendAsync(string ipAddress,int port,int sendTimeout, byte[] buffer, CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 向指定 TCP 端点发送一段字节数据(发送完成后立即断开)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">设备 IP 地址</param>
|
||||||
|
/// <param name="port">TCP 端口号</param>
|
||||||
|
/// <param name="sendTimeout">发送超时时间(毫秒)</param>
|
||||||
|
/// <param name="buffer">要发送的字节数组</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
public static async Task 发送字节数据(string ipAddress,int port,int sendTimeout, byte[] buffer, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var client = new TcpClient();
|
using var client = new TcpClient();
|
||||||
@@ -26,22 +46,39 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(ipAddress, port).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Task SendAsync(string ipAddress, int port,int sendTimeout, string text,CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 向指定 TCP 端点发送一段文本(以 UTF8 编码为字节后发送,发送完成后立即断开)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">设备 IP 地址</param>
|
||||||
|
/// <param name="port">TCP 端口号</param>
|
||||||
|
/// <param name="sendTimeout">发送超时时间(毫秒)</param>
|
||||||
|
/// <param name="text">要发送的文本字符串</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
public static Task 发送文本数据(string ipAddress, int port,int sendTimeout, string text,CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
return SendAsync( ipAddress, port, sendTimeout, Encoding.UTF8.GetBytes(text),ct);
|
return 发送字节数据( ipAddress, port, sendTimeout, Encoding.UTF8.GetBytes(text),ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region Read
|
#region Read
|
||||||
|
|
||||||
public static async Task<byte[]> ReadAsync(string ipAddress,int port,int receiveTimeout,int length,CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 连接指定 TCP 端点并读取指定长度的字节数据(连接关闭或读满为止)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">设备 IP 地址</param>
|
||||||
|
/// <param name="port">TCP 端口号</param>
|
||||||
|
/// <param name="receiveTimeout">接收超时时间(毫秒)</param>
|
||||||
|
/// <param name="length">要读取的字节长度</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>实际读取到的字节数组(对端提前关闭时可能短于请求长度)</returns>
|
||||||
|
public static async Task<byte[]> 读取字节数据(string ipAddress,int port,int receiveTimeout,int length,CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var client = new TcpClient();
|
using var client = new TcpClient();
|
||||||
@@ -68,13 +105,22 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(ipAddress, port).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async Task<string> ReadLineAsync( string ipAddress, int port, int receiveTimeout, string delimiter = "\n",CancellationToken ct = default)
|
/// <summary>
|
||||||
|
/// 连接指定 TCP 端点并读取一行文本,读取到结束符为止(返回内容不含结束符)。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="ipAddress">设备 IP 地址</param>
|
||||||
|
/// <param name="port">TCP 端口号</param>
|
||||||
|
/// <param name="receiveTimeout">接收超时时间(毫秒),超时未收到结束符抛出超时异常</param>
|
||||||
|
/// <param name="delimiter">文本行结束符,默认换行符 "\n"</param>
|
||||||
|
/// <param name="ct">异步取消令牌</param>
|
||||||
|
/// <returns>去除结束符并去除首尾空白后的文本行</returns>
|
||||||
|
public static async Task<string> 读取文本行( string ipAddress, int port, int receiveTimeout, string delimiter = "\n",CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
await _commLock.WaitAsync(ct);
|
await GetLock(ipAddress, port).WaitAsync(ct);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var client = new TcpClient();
|
using var client = new TcpClient();
|
||||||
@@ -106,7 +152,7 @@ namespace DeviceCommand.Flexible
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_commLock.Release();
|
GetLock(ipAddress, port).Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -120,29 +120,29 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 测量结果属性
|
#region 测量结果属性
|
||||||
|
|
||||||
private string _measuredVoltage = "—";
|
private double _measuredVoltage;
|
||||||
public string MeasuredVoltage
|
public double MeasuredVoltage
|
||||||
{
|
{
|
||||||
get => _measuredVoltage;
|
get => _measuredVoltage;
|
||||||
set => SetProperty(ref _measuredVoltage, value);
|
set => SetProperty(ref _measuredVoltage, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredCurrent = "—";
|
private double _measuredCurrent;
|
||||||
public string MeasuredCurrent
|
public double MeasuredCurrent
|
||||||
{
|
{
|
||||||
get => _measuredCurrent;
|
get => _measuredCurrent;
|
||||||
set => SetProperty(ref _measuredCurrent, value);
|
set => SetProperty(ref _measuredCurrent, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredPower = "—";
|
private double _measuredPower;
|
||||||
public string MeasuredPower
|
public double MeasuredPower
|
||||||
{
|
{
|
||||||
get => _measuredPower;
|
get => _measuredPower;
|
||||||
set => SetProperty(ref _measuredPower, value);
|
set => SetProperty(ref _measuredPower, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredFrequency = "—";
|
private double _measuredFrequency;
|
||||||
public string MeasuredFrequency
|
public double MeasuredFrequency
|
||||||
{
|
{
|
||||||
get => _measuredFrequency;
|
get => _measuredFrequency;
|
||||||
set => SetProperty(ref _measuredFrequency, value);
|
set => SetProperty(ref _measuredFrequency, value);
|
||||||
|
|||||||
@@ -118,22 +118,22 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 测量结果属性
|
#region 测量结果属性
|
||||||
|
|
||||||
private string _measuredVoltage = "—";
|
private double _measuredVoltage;
|
||||||
public string MeasuredVoltage
|
public double MeasuredVoltage
|
||||||
{
|
{
|
||||||
get => _measuredVoltage;
|
get => _measuredVoltage;
|
||||||
set => SetProperty(ref _measuredVoltage, value);
|
set => SetProperty(ref _measuredVoltage, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredCurrent = "—";
|
private double _measuredCurrent;
|
||||||
public string MeasuredCurrent
|
public double MeasuredCurrent
|
||||||
{
|
{
|
||||||
get => _measuredCurrent;
|
get => _measuredCurrent;
|
||||||
set => SetProperty(ref _measuredCurrent, value);
|
set => SetProperty(ref _measuredCurrent, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredPower = "—";
|
private double _measuredPower;
|
||||||
public string MeasuredPower
|
public double MeasuredPower
|
||||||
{
|
{
|
||||||
get => _measuredPower;
|
get => _measuredPower;
|
||||||
set => SetProperty(ref _measuredPower, value);
|
set => SetProperty(ref _measuredPower, value);
|
||||||
|
|||||||
@@ -93,22 +93,22 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 测量结果属性
|
#region 测量结果属性
|
||||||
|
|
||||||
private string _measuredVoltage = "—";
|
private double _measuredVoltage;
|
||||||
public string MeasuredVoltage
|
public double MeasuredVoltage
|
||||||
{
|
{
|
||||||
get => _measuredVoltage;
|
get => _measuredVoltage;
|
||||||
set => SetProperty(ref _measuredVoltage, value);
|
set => SetProperty(ref _measuredVoltage, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredCurrent = "—";
|
private double _measuredCurrent;
|
||||||
public string MeasuredCurrent
|
public double MeasuredCurrent
|
||||||
{
|
{
|
||||||
get => _measuredCurrent;
|
get => _measuredCurrent;
|
||||||
set => SetProperty(ref _measuredCurrent, value);
|
set => SetProperty(ref _measuredCurrent, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredPower = "—";
|
private double _measuredPower;
|
||||||
public string MeasuredPower
|
public double MeasuredPower
|
||||||
{
|
{
|
||||||
get => _measuredPower;
|
get => _measuredPower;
|
||||||
set => SetProperty(ref _measuredPower, value);
|
set => SetProperty(ref _measuredPower, value);
|
||||||
|
|||||||
@@ -94,22 +94,22 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 测量结果属性
|
#region 测量结果属性
|
||||||
|
|
||||||
private string _measuredVoltage = "—";
|
private double _measuredVoltage;
|
||||||
public string MeasuredVoltage
|
public double MeasuredVoltage
|
||||||
{
|
{
|
||||||
get => _measuredVoltage;
|
get => _measuredVoltage;
|
||||||
set => SetProperty(ref _measuredVoltage, value);
|
set => SetProperty(ref _measuredVoltage, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredCurrent = "—";
|
private double _measuredCurrent;
|
||||||
public string MeasuredCurrent
|
public double MeasuredCurrent
|
||||||
{
|
{
|
||||||
get => _measuredCurrent;
|
get => _measuredCurrent;
|
||||||
set => SetProperty(ref _measuredCurrent, value);
|
set => SetProperty(ref _measuredCurrent, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredPower = "—";
|
private double _measuredPower;
|
||||||
public string MeasuredPower
|
public double MeasuredPower
|
||||||
{
|
{
|
||||||
get => _measuredPower;
|
get => _measuredPower;
|
||||||
set => SetProperty(ref _measuredPower, value);
|
set => SetProperty(ref _measuredPower, value);
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ using Prism.Commands;
|
|||||||
using Prism.Ioc;
|
using Prism.Ioc;
|
||||||
using System;
|
using System;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
@@ -113,22 +112,22 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 测量结果属性
|
#region 测量结果属性
|
||||||
|
|
||||||
private string _measuredVpp = "—";
|
private double _measuredVpp;
|
||||||
public string MeasuredVpp
|
public double MeasuredVpp
|
||||||
{
|
{
|
||||||
get => _measuredVpp;
|
get => _measuredVpp;
|
||||||
set => SetProperty(ref _measuredVpp, value);
|
set => SetProperty(ref _measuredVpp, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredFrequency = "—";
|
private double _measuredFrequency;
|
||||||
public string MeasuredFrequency
|
public double MeasuredFrequency
|
||||||
{
|
{
|
||||||
get => _measuredFrequency;
|
get => _measuredFrequency;
|
||||||
set => SetProperty(ref _measuredFrequency, value);
|
set => SetProperty(ref _measuredFrequency, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredRms = "—";
|
private double _measuredRms;
|
||||||
public string MeasuredRms
|
public double MeasuredRms
|
||||||
{
|
{
|
||||||
get => _measuredRms;
|
get => _measuredRms;
|
||||||
set => SetProperty(ref _measuredRms, value);
|
set => SetProperty(ref _measuredRms, value);
|
||||||
@@ -207,40 +206,30 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryMeasurementsCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
// 1. 保留设备原生吐出的完整原始字符串
|
// 设备层已自动提取纯数值并返回 double
|
||||||
string rawVpp = await _device!.查询实际电压峰峰值(Channel, Ct());
|
MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct());
|
||||||
string rawFreq = await _device!.查询实际频率(Channel, Ct());
|
MeasuredFrequency = await _device!.查询实际频率(Channel, Ct());
|
||||||
string rawRms = await _device!.查询实际电压均方根(Channel, Ct());
|
MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct());
|
||||||
|
|
||||||
// 2. 清洗数据并将科学计数法转为常规小数后赋值给 UI 属性
|
AppendLog($"C{Channel} 测量结果 → Vpp:{MeasuredVpp:0.######}V Freq:{MeasuredFrequency:0.######}Hz RMS:{MeasuredRms:0.######}V");
|
||||||
MeasuredVpp = ParseMeasurement(rawVpp);
|
|
||||||
MeasuredFrequency = ParseMeasurement(rawFreq);
|
|
||||||
MeasuredRms = ParseMeasurement(rawRms);
|
|
||||||
|
|
||||||
// 3. 在日志中同时体现设备原始报文与解析呈现值,极方便联调
|
|
||||||
AppendLog($"C{Channel} 测量原始数据 → Vpp:{rawVpp.Trim()} Freq:{rawFreq.Trim()} RMS:{rawRms.Trim()}");
|
|
||||||
AppendLog($"C{Channel} 界面呈现数值 → Vpp:{MeasuredVpp}V Freq:{MeasuredFrequency}Hz RMS:{MeasuredRms}V");
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
QueryVppCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryVppCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
string raw = await _device!.查询实际电压峰峰值(Channel, Ct());
|
MeasuredVpp = await _device!.查询实际电压峰峰值(Channel, Ct());
|
||||||
MeasuredVpp = ParseMeasurement(raw);
|
AppendLog($"C{Channel} Vpp: {MeasuredVpp:0.######} V");
|
||||||
AppendLog($"C{Channel} Vpp: {MeasuredVpp} (Raw: {raw.Trim()})");
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryFrequencyCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
string raw = await _device!.查询实际频率(Channel, Ct());
|
MeasuredFrequency = await _device!.查询实际频率(Channel, Ct());
|
||||||
MeasuredFrequency = ParseMeasurement(raw);
|
AppendLog($"C{Channel} Freq: {MeasuredFrequency:0.######} Hz");
|
||||||
AppendLog($"C{Channel} Freq: {MeasuredFrequency} (Raw: {raw.Trim()})");
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
QueryRmsCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryRmsCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
string raw = await _device!.查询实际电压均方根(Channel, Ct());
|
MeasuredRms = await _device!.查询实际电压均方根(Channel, Ct());
|
||||||
MeasuredRms = ParseMeasurement(raw);
|
AppendLog($"C{Channel} RMS: {MeasuredRms:0.######} V");
|
||||||
AppendLog($"C{Channel} RMS: {MeasuredRms} (Raw: {raw.Trim()})");
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Initialize();
|
Initialize();
|
||||||
@@ -330,52 +319,6 @@ namespace DeviceEditModule.ViewModels
|
|||||||
: line + "\n" + ResponseLog;
|
: line + "\n" + ResponseLog;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 清洗示波器原始返回的测量字符串(例如 "C1:PAVA RMS,5.57E-03V")并安全转化为无科学计数法的小数形式。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="rawResponse">设备原始应答数据</param>
|
|
||||||
/// <returns>可直接绑定到 UI 呈现的字符串数字</returns>
|
|
||||||
private string ParseMeasurement(string rawResponse)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(rawResponse)) return "—";
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string cleanData = rawResponse.Trim();
|
|
||||||
|
|
||||||
// 1. 斩断报头,提取逗号后面的具体内容(如 "5.57E-03V" 或 "****")
|
|
||||||
int commaIndex = cleanData.IndexOf(',');
|
|
||||||
if (commaIndex == -1) return "—";
|
|
||||||
|
|
||||||
string valStr = cleanData.Substring(commaIndex + 1);
|
|
||||||
|
|
||||||
var match = Regex.Match(valStr, @"[-+]?[0-9]*\.?[0-9]+([eE][-+]?[0-9]+)?");
|
|
||||||
if (match.Success)
|
|
||||||
{
|
|
||||||
valStr = match.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 校验并拦截设备未测出时的无效星号 "****"
|
|
||||||
if (valStr.Contains("*") || string.IsNullOrWhiteSpace(valStr))
|
|
||||||
{
|
|
||||||
return "0"; // 回归为零或 "—",防止触发数据异常
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 解析科学计数法,并重新以不带科学计数法的小数样式展开
|
|
||||||
if (double.TryParse(valStr, NumberStyles.Any, CultureInfo.InvariantCulture, out double result))
|
|
||||||
{
|
|
||||||
// "0.######" 样式会自动消除末尾无用的冗余零,并显示为普通小数(如 0.00557)
|
|
||||||
return result.ToString("0.######", CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// 捕获可能产生的边缘转换故障,保证轮询线程绝不崩溃
|
|
||||||
}
|
|
||||||
|
|
||||||
return "—";
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
|||||||
@@ -101,36 +101,36 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
#region 测量结果属性
|
#region 测量结果属性
|
||||||
|
|
||||||
private string _measuredVoltage = "—";
|
private double _measuredVoltage;
|
||||||
public string MeasuredVoltage
|
public double MeasuredVoltage
|
||||||
{
|
{
|
||||||
get => _measuredVoltage;
|
get => _measuredVoltage;
|
||||||
set => SetProperty(ref _measuredVoltage, value);
|
set => SetProperty(ref _measuredVoltage, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredCurrent = "—";
|
private double _measuredCurrent;
|
||||||
public string MeasuredCurrent
|
public double MeasuredCurrent
|
||||||
{
|
{
|
||||||
get => _measuredCurrent;
|
get => _measuredCurrent;
|
||||||
set => SetProperty(ref _measuredCurrent, value);
|
set => SetProperty(ref _measuredCurrent, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredPower = "—";
|
private double _measuredPower;
|
||||||
public string MeasuredPower
|
public double MeasuredPower
|
||||||
{
|
{
|
||||||
get => _measuredPower;
|
get => _measuredPower;
|
||||||
set => SetProperty(ref _measuredPower, value);
|
set => SetProperty(ref _measuredPower, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredFrequency = "—";
|
private double _measuredFrequency;
|
||||||
public string MeasuredFrequency
|
public double MeasuredFrequency
|
||||||
{
|
{
|
||||||
get => _measuredFrequency;
|
get => _measuredFrequency;
|
||||||
set => SetProperty(ref _measuredFrequency, value);
|
set => SetProperty(ref _measuredFrequency, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
private string _measuredPowerFactor = "—";
|
private double _measuredPowerFactor;
|
||||||
public string MeasuredPowerFactor
|
public double MeasuredPowerFactor
|
||||||
{
|
{
|
||||||
get => _measuredPowerFactor;
|
get => _measuredPowerFactor;
|
||||||
set => SetProperty(ref _measuredPowerFactor, value);
|
set => SetProperty(ref _measuredPowerFactor, value);
|
||||||
@@ -196,20 +196,14 @@ namespace DeviceEditModule.ViewModels
|
|||||||
// 只修改这里:保持你原本的 5 次驱动查询调用不变,仅对读回来的科学计数法进行两位小数格式化
|
// 只修改这里:保持你原本的 5 次驱动查询调用不变,仅对读回来的科学计数法进行两位小数格式化
|
||||||
QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () =>
|
QueryAllMeasureCommand = new DelegateCommand(async () => await Exec(async () =>
|
||||||
{
|
{
|
||||||
string rawU = await _device!.查询实际电压(Channel, Ct());
|
// 设备层已返回 double 数值
|
||||||
string rawI = await _device!.查询实际电流(Channel, Ct());
|
MeasuredVoltage = await _device!.查询实际电压(Channel, Ct());
|
||||||
string rawP = await _device!.查询实际功率(Channel, Ct());
|
MeasuredCurrent = await _device!.查询实际电流(Channel, Ct());
|
||||||
string rawF = await _device!.查询频率(Channel, Ct());
|
MeasuredPower = await _device!.查询实际功率(Channel, Ct());
|
||||||
string rawPF = await _device!.查询功率因数(Channel, Ct());
|
MeasuredFrequency = await _device!.查询频率(Channel, Ct());
|
||||||
|
MeasuredPowerFactor = await _device!.查询功率因数(Channel, Ct());
|
||||||
|
|
||||||
// 转换科学计数法格式,如果失败会自动保持原样
|
AppendLog($"CH{Channel} 测量 → U:{MeasuredVoltage:F2}V I:{MeasuredCurrent:F3}A P:{MeasuredPower:F2}W F:{MeasuredFrequency:F2}Hz PF:{MeasuredPowerFactor:F3}");
|
||||||
MeasuredVoltage = FormatToDecimal(rawU, "F2"); // 电压两位小数
|
|
||||||
MeasuredCurrent = FormatToDecimal(rawI, "F3"); // 电流通常较小,建议保留3位小数
|
|
||||||
MeasuredPower = FormatToDecimal(rawP, "F2"); // 功率两位小数
|
|
||||||
MeasuredFrequency = FormatToDecimal(rawF, "F2"); // 频率两位小数
|
|
||||||
MeasuredPowerFactor = FormatToDecimal(rawPF, "F3"); // 功率因数保留3位小数
|
|
||||||
|
|
||||||
AppendLog($"CH{Channel} 测量 → U:{MeasuredVoltage}V I:{MeasuredCurrent}A P:{MeasuredPower}W F:{MeasuredFrequency}Hz PF:{MeasuredPowerFactor}");
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Initialize();
|
Initialize();
|
||||||
@@ -263,41 +257,6 @@ namespace DeviceEditModule.ViewModels
|
|||||||
|
|
||||||
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
private CancellationToken Ct() => (_cts = new CancellationTokenSource(TimeSpan.FromSeconds(10))).Token;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 将科学计数法字符串转换为标准小数格式
|
|
||||||
/// </summary>
|
|
||||||
private string FormatToDecimal(string rawInput, string decimalFormat = "F2")
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(rawInput))
|
|
||||||
return "—";
|
|
||||||
|
|
||||||
// 去除可能夹杂的命令头,保留数据部分
|
|
||||||
string cleanInput = CleanResponseHeader(rawInput);
|
|
||||||
|
|
||||||
// 解析科学计数法(NumberStyles.Any 和 InvariantCulture 是关键)
|
|
||||||
if (double.TryParse(cleanInput, NumberStyles.Any, CultureInfo.InvariantCulture, out double value))
|
|
||||||
{
|
|
||||||
return value.ToString(decimalFormat, CultureInfo.InvariantCulture);
|
|
||||||
}
|
|
||||||
|
|
||||||
return cleanInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 辅助清洗:剥离可能伴随吐回的命令头或双引号
|
|
||||||
/// </summary>
|
|
||||||
private string CleanResponseHeader(string response)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(response)) return string.Empty;
|
|
||||||
string result = response.Trim();
|
|
||||||
if (result.Contains(" "))
|
|
||||||
{
|
|
||||||
int lastSpaceIndex = result.LastIndexOf(' ');
|
|
||||||
result = result.Substring(lastSpaceIndex + 1).Trim();
|
|
||||||
}
|
|
||||||
return result.Replace("\"", "").Replace("'", "").Trim();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Exec(Func<Task> action)
|
private async Task Exec(Func<Task> action)
|
||||||
{
|
{
|
||||||
if (_device == null)
|
if (_device == null)
|
||||||
|
|||||||
@@ -82,19 +82,23 @@ namespace ExportModule.ViewModels
|
|||||||
public ICommand LoadedCommand { get; }
|
public ICommand LoadedCommand { get; }
|
||||||
public ICommand QueryCommand { get; }
|
public ICommand QueryCommand { get; }
|
||||||
public ICommand ExportCommand { get; }
|
public ICommand ExportCommand { get; }
|
||||||
|
public ICommand ExportReportCommand { get; }
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 私有字段
|
#region 私有字段
|
||||||
private readonly ITestReportService _testReportService;
|
private readonly ITestReportService _testReportService;
|
||||||
|
private readonly ITestCheckRecordService _testCheckRecordService;
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public ExportViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
public ExportViewModel(IContainerProvider containerProvider) : base(containerProvider)
|
||||||
{
|
{
|
||||||
_testReportService = containerProvider.Resolve<ITestReportService>();
|
_testReportService = containerProvider.Resolve<ITestReportService>();
|
||||||
|
_testCheckRecordService = containerProvider.Resolve<ITestCheckRecordService>();
|
||||||
|
|
||||||
LoadedCommand = new AsyncDelegateCommand(OnLoad);
|
LoadedCommand = new AsyncDelegateCommand(OnLoad);
|
||||||
QueryCommand = new AsyncDelegateCommand(OnQuery);
|
QueryCommand = new AsyncDelegateCommand(OnQuery);
|
||||||
ExportCommand = new AsyncDelegateCommand(OnExport);
|
ExportCommand = new AsyncDelegateCommand(OnExport);
|
||||||
|
ExportReportCommand = new AsyncDelegateCommand(OnExportReportCommand);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region 命令处理
|
#region 命令处理
|
||||||
@@ -183,6 +187,39 @@ namespace ExportModule.ViewModels
|
|||||||
await Task.Run(() => ExportToExcel(dialog.FileName, entities));
|
await Task.Run(() => ExportToExcel(dialog.FileName, entities));
|
||||||
ShowInfoMessageBox($"导出完成,共 {entities.Count} 条步骤记录,已保存至:{dialog.FileName}", () => { });
|
ShowInfoMessageBox($"导出完成,共 {entities.Count} 条步骤记录,已保存至:{dialog.FileName}", () => { });
|
||||||
}
|
}
|
||||||
|
/// <summary>
|
||||||
|
/// 导出测试报告:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细
|
||||||
|
/// </summary>
|
||||||
|
private async Task OnExportReportCommand()
|
||||||
|
{
|
||||||
|
if (SelectedTestReport == null)
|
||||||
|
{
|
||||||
|
ShowErrorMessageBox("请先在列表中选择一条测试记录。", () => { });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusMessage = "正在查询测试项判断记录...";
|
||||||
|
var checkResult = await _testCheckRecordService.GetByTestRoundIdAsync(SelectedTestReport.TestRoundId);
|
||||||
|
if (!checkResult.IsSuccess || checkResult.Data == null || checkResult.Data.Count == 0)
|
||||||
|
{
|
||||||
|
ShowErrorMessageBox("未找到该测试记录的测试项判断数据(需将子程序标记为测试项后运行)。", () => { });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dialog = new SaveFileDialog
|
||||||
|
{
|
||||||
|
Filter = "Excel 工作簿 (*.xlsx)|*.xlsx|所有文件 (*.*)|*.*",
|
||||||
|
DefaultExt = ".xlsx",
|
||||||
|
FileName = $"测试报告_{SelectedTestReport.Scope}_{SelectedTestReport.StartTime:yyyyMMdd_HHmmss}.xlsx"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (dialog.ShowDialog() != true) return;
|
||||||
|
|
||||||
|
var records = checkResult.Data;
|
||||||
|
StatusMessage = $"正在导出 {records.Count} 条测试项判断记录...";
|
||||||
|
await Task.Run(() => ExportReportToExcel(dialog.FileName, records, SelectedTestReport));
|
||||||
|
ShowInfoMessageBox($"导出完成,已保存至:{dialog.FileName}", () => { });
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
@@ -239,6 +276,152 @@ namespace ExportModule.ViewModels
|
|||||||
var dataRange = ws.Range(row, 1, row, headers.Length);
|
var dataRange = ws.Range(row, 1, row, headers.Length);
|
||||||
dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||||
dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
|
||||||
|
// 根据 Result 设置行背景色(与 UI DataGrid 配色一致)
|
||||||
|
// Result: "0"=运行中, "1"=通过, "2"=失败
|
||||||
|
if (e.Result is "运行中" or "成功" or "失败")
|
||||||
|
{
|
||||||
|
XLColor rowColor = e.Result switch
|
||||||
|
{
|
||||||
|
"运行中" => XLColor.FromArgb(0xB3, 0xD9, 0xFF), // 运行中 - 浅蓝
|
||||||
|
"成功" => XLColor.FromArgb(0xB2, 0xFF, 0xB2), // 通过 - 浅绿
|
||||||
|
"失败" => XLColor.FromArgb(0xFF, 0xB2, 0xB2), // 失败 - 浅红
|
||||||
|
_ => XLColor.White
|
||||||
|
};
|
||||||
|
dataRange.Style.Fill.BackgroundColor = rowColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 异常流程步骤额外标记(字体加粗 + 橙色字体)
|
||||||
|
if (e.IsErrorStep)
|
||||||
|
dataRange.Style.Font.FontColor = XLColor.DarkOrange;
|
||||||
|
}
|
||||||
|
|
||||||
|
ws.Columns().AdjustToContents();
|
||||||
|
workbook.SaveAs(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试报告导出:上层为各测试项(IsTestItem 子程序)的 PASS/NG 汇总,下层为每次 OKExpression 判断明细(含重复判断、判断时间、数据值)
|
||||||
|
/// </summary>
|
||||||
|
private static void ExportReportToExcel(string filePath, List<TestCheckRecordEntity> records, TestReportModel report)
|
||||||
|
{
|
||||||
|
using var workbook = new XLWorkbook();
|
||||||
|
var ws = workbook.Worksheets.Add("测试报告");
|
||||||
|
const int colCount = 6;
|
||||||
|
|
||||||
|
var passColor = XLColor.FromArgb(0xB2, 0xFF, 0xB2); // 通过 - 浅绿(与现有导出配色一致)
|
||||||
|
var ngColor = XLColor.FromArgb(0xFF, 0xB2, 0xB2); // 失败 - 浅红
|
||||||
|
var headerColor = XLColor.FromArgb(0xEC, 0xEF, 0xF4);
|
||||||
|
|
||||||
|
int row = 1;
|
||||||
|
|
||||||
|
// ===== 标题与信息行 =====
|
||||||
|
ws.Range(row, 1, row, colCount).Merge();
|
||||||
|
ws.Cell(row, 1).Value = "测 试 报 告";
|
||||||
|
ws.Range(row, 1, row, colCount).Style.Font.Bold = true;
|
||||||
|
ws.Range(row, 1, row, colCount).Style.Font.FontSize = 16;
|
||||||
|
ws.Range(row, 1, row, colCount).Style.Alignment.Horizontal = XLAlignmentHorizontalValues.Center;
|
||||||
|
row++;
|
||||||
|
ws.Cell(row, 1).Value = $"台架:{report.Scope ?? ""}";
|
||||||
|
ws.Cell(row, 3).Value = $"测试文件:{report.FileName ?? ""}";
|
||||||
|
row++;
|
||||||
|
ws.Cell(row, 1).Value = $"开始时间:{report.StartTime:yyyy-MM-dd HH:mm:ss}";
|
||||||
|
ws.Cell(row, 3).Value = $"结束时间:{report.EndTime:yyyy-MM-dd HH:mm:ss}";
|
||||||
|
row += 2;
|
||||||
|
|
||||||
|
// 按测试项分组(保持首次出现顺序)
|
||||||
|
var groups = records.GroupBy(r => r.TestItemName).ToList();
|
||||||
|
|
||||||
|
// ===== 上层:测试项汇总 =====
|
||||||
|
ws.Cell(row, 1).Value = "【测试项汇总】";
|
||||||
|
ws.Cell(row, 1).Style.Font.Bold = true;
|
||||||
|
row++;
|
||||||
|
|
||||||
|
string[] summaryHeaders = { "序号", "测试项", "判定结果", "执行次数", "判定次数", "NG次数" };
|
||||||
|
for (int c = 0; c < summaryHeaders.Length; c++)
|
||||||
|
ws.Cell(row, c + 1).Value = summaryHeaders[c];
|
||||||
|
var headerRange = ws.Range(row, 1, row, colCount);
|
||||||
|
headerRange.Style.Font.Bold = true;
|
||||||
|
headerRange.Style.Fill.BackgroundColor = headerColor;
|
||||||
|
headerRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
headerRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
row++;
|
||||||
|
|
||||||
|
int seq = 1;
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
var summaries = group.Where(x => x.IsSummary).ToList();
|
||||||
|
var details = group.Where(x => !x.IsSummary).ToList();
|
||||||
|
bool overallPass = summaries.All(s => s.Pass) && details.All(d => d.Pass);
|
||||||
|
|
||||||
|
ws.Cell(row, 1).Value = seq++;
|
||||||
|
ws.Cell(row, 2).Value = group.Key;
|
||||||
|
ws.Cell(row, 3).Value = overallPass ? "PASS" : "NG";
|
||||||
|
ws.Cell(row, 4).Value = summaries.Count;
|
||||||
|
ws.Cell(row, 5).Value = details.Count;
|
||||||
|
ws.Cell(row, 6).Value = details.Count(d => !d.Pass);
|
||||||
|
|
||||||
|
var dataRange = ws.Range(row, 1, row, colCount);
|
||||||
|
dataRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
dataRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
ws.Cell(row, 3).Style.Font.Bold = true;
|
||||||
|
ws.Cell(row, 3).Style.Fill.BackgroundColor = overallPass ? passColor : ngColor;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
row++;
|
||||||
|
|
||||||
|
// ===== 下层:判断明细 =====
|
||||||
|
ws.Cell(row, 1).Value = "【判断明细】";
|
||||||
|
ws.Cell(row, 1).Style.Font.Bold = true;
|
||||||
|
row++;
|
||||||
|
|
||||||
|
string[] detailHeaders = { "序号", "判定时间", "步骤名称", "OKExpression", "判定结果", "数据值" };
|
||||||
|
foreach (var group in groups)
|
||||||
|
{
|
||||||
|
var details = group.Where(x => !x.IsSummary).ToList();
|
||||||
|
bool overallPass = group.Where(x => x.IsSummary).All(s => s.Pass) && details.All(d => d.Pass);
|
||||||
|
|
||||||
|
// 测试项块标题行(合并单元格)
|
||||||
|
ws.Range(row, 1, row, colCount).Merge();
|
||||||
|
ws.Cell(row, 1).Value = $"■ 测试项:{group.Key} 总体结果:{(overallPass ? "PASS" : "NG")}";
|
||||||
|
ws.Cell(row, 1).Style.Font.Bold = true;
|
||||||
|
ws.Range(row, 1, row, colCount).Style.Fill.BackgroundColor = overallPass ? passColor : ngColor;
|
||||||
|
row++;
|
||||||
|
|
||||||
|
for (int c = 0; c < detailHeaders.Length; c++)
|
||||||
|
ws.Cell(row, c + 1).Value = detailHeaders[c];
|
||||||
|
var detailHeaderRange = ws.Range(row, 1, row, colCount);
|
||||||
|
detailHeaderRange.Style.Font.Bold = true;
|
||||||
|
detailHeaderRange.Style.Fill.BackgroundColor = headerColor;
|
||||||
|
detailHeaderRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
detailHeaderRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
row++;
|
||||||
|
|
||||||
|
if (details.Count == 0)
|
||||||
|
{
|
||||||
|
ws.Range(row, 1, row, colCount).Merge();
|
||||||
|
ws.Cell(row, 1).Value = "(无 OKExpression 判断,结果由步骤执行成败决定)";
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
|
||||||
|
int detailSeq = 1;
|
||||||
|
foreach (var d in details)
|
||||||
|
{
|
||||||
|
ws.Cell(row, 1).Value = detailSeq++;
|
||||||
|
ws.Cell(row, 2).Value = d.CreateTime.ToString("yyyy-MM-dd HH:mm:ss.fff");
|
||||||
|
ws.Cell(row, 3).Value = d.StepName ?? "";
|
||||||
|
ws.Cell(row, 4).Value = d.OKExpression ?? "";
|
||||||
|
ws.Cell(row, 5).Value = d.Pass ? "PASS" : "NG";
|
||||||
|
ws.Cell(row, 6).Value = d.Values ?? "";
|
||||||
|
|
||||||
|
var detailRange = ws.Range(row, 1, row, colCount);
|
||||||
|
detailRange.Style.Border.OutsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
detailRange.Style.Border.InsideBorder = XLBorderStyleValues.Thin;
|
||||||
|
ws.Cell(row, 5).Style.Font.Bold = true;
|
||||||
|
ws.Cell(row, 5).Style.Fill.BackgroundColor = d.Pass ? passColor : ngColor;
|
||||||
|
row++;
|
||||||
|
}
|
||||||
|
row++;
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.Columns().AdjustToContents();
|
ws.Columns().AdjustToContents();
|
||||||
|
|||||||
@@ -60,6 +60,7 @@
|
|||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="Auto"/>
|
<ColumnDefinition Width="Auto"/>
|
||||||
|
<ColumnDefinition Width="Auto"/>
|
||||||
<ColumnDefinition Width="*"/>
|
<ColumnDefinition Width="*"/>
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|
||||||
@@ -109,13 +110,20 @@
|
|||||||
Margin="4,0"
|
Margin="4,0"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<Button Grid.Column="7"
|
<Button Grid.Column="7"
|
||||||
Content="导出 Excel"
|
Content="导出详细测试步骤"
|
||||||
Command="{Binding ExportCommand}"
|
Command="{Binding ExportCommand}"
|
||||||
Style="{StaticResource MaterialDesignFlatButton}"
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
Padding="16,6"
|
Padding="16,6"
|
||||||
Margin="4,0"
|
Margin="4,0"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<Button Grid.Column="8"
|
<Button Grid.Column="8"
|
||||||
|
Content="导出测试报告"
|
||||||
|
Command="{Binding ExportReportCommand}"
|
||||||
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
|
Padding="16,6"
|
||||||
|
Margin="4,0"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="9"
|
||||||
Content="加载全部"
|
Content="加载全部"
|
||||||
Command="{Binding LoadedCommand}"
|
Command="{Binding LoadedCommand}"
|
||||||
Style="{StaticResource MaterialDesignFlatButton}"
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ namespace MainModule.ViewModels
|
|||||||
string json = System.IO.File.ReadAllText(filePath);
|
string json = System.IO.File.ReadAllText(filePath);
|
||||||
Newtonsoft.Json.JsonConvert.PopulateObject(json, _systemConfig);
|
Newtonsoft.Json.JsonConvert.PopulateObject(json, _systemConfig);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_systemConfig = new SystemConfig();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
//容器解析顺序不要改变!!!
|
//容器解析顺序不要改变!!!
|
||||||
_scopedContext = _scope.Resolve<ScopedContext>();
|
_scopedContext = _scope.Resolve<ScopedContext>();
|
||||||
@@ -213,10 +217,10 @@ namespace MainModule.ViewModels
|
|||||||
(LogAreaVM as IDisposable)?.Dispose();
|
(LogAreaVM as IDisposable)?.Dispose();
|
||||||
(ParametersManagerVM as IDisposable)?.Dispose();
|
(ParametersManagerVM as IDisposable)?.Dispose();
|
||||||
|
|
||||||
_globalInfo.ContextDic?.Remove(TestStatus);
|
_globalInfo.ContextDic?.TryRemove(TestStatus, out _);
|
||||||
_globalInfo.StepRunningDic?.Remove(TestStatus);
|
_globalInfo.StepRunningDic?.TryRemove(TestStatus, out _);
|
||||||
_globalInfo.ConfigDic?.Remove(TestStatus);
|
_globalInfo.ConfigDic?.TryRemove(TestStatus, out _);
|
||||||
_globalInfo.ScopeDic?.Remove(TestStatus);
|
_globalInfo.ScopeDic?.TryRemove(TestStatus, out _);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -230,11 +234,23 @@ namespace MainModule.ViewModels
|
|||||||
#region 命令处理与事件
|
#region 命令处理与事件
|
||||||
private async Task OnLoad()
|
private async Task OnLoad()
|
||||||
{
|
{
|
||||||
if (!IsInitialized)
|
// 设备初始化期间仅对当前台架显示灰度遮罩层,初始化完成后关闭(finally 保证异常时也能关闭)
|
||||||
|
_eventAggregator.GetEvent<ScopeOverlayEvent>().Publish(new ScopeOverlayArgs { Scope = TestStatus, Show = true });
|
||||||
|
// 让遮罩层先完成渲染,再进入同步阻塞的设备初始化(泵送 Dispatcher 至 Render 优先级)
|
||||||
|
System.Windows.Application.Current?.Dispatcher.Invoke(() => { }, System.Windows.Threading.DispatcherPriority.Render);
|
||||||
|
try
|
||||||
{
|
{
|
||||||
await _deviceManager.ConnectAllDevices();
|
if (!IsInitialized)
|
||||||
IsInitialized = true;
|
{
|
||||||
|
await _deviceManager.ConnectAllDevices();
|
||||||
|
IsInitialized = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<ScopeOverlayEvent>().Publish(new ScopeOverlayArgs { Scope = TestStatus, Show = false });
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnRefresh()
|
private void OnRefresh()
|
||||||
@@ -259,10 +275,10 @@ namespace MainModule.ViewModels
|
|||||||
if (navigationContext.Parameters.ContainsKey("Name"))
|
if (navigationContext.Parameters.ContainsKey("Name"))
|
||||||
{
|
{
|
||||||
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
TestStatus = navigationContext.Parameters.GetValue<string>("Name");
|
||||||
_globalInfo.ContextDic.Add(TestStatus, _scopedContext);
|
_globalInfo.ContextDic.TryAdd(TestStatus, _scopedContext);
|
||||||
_globalInfo.StepRunningDic.Add(TestStatus, _stepRunning);
|
_globalInfo.StepRunningDic.TryAdd(TestStatus, _stepRunning);
|
||||||
_globalInfo.ScopeDic.Add(TestStatus, _scope);
|
_globalInfo.ScopeDic.TryAdd(TestStatus, _scope);
|
||||||
_globalInfo.ConfigDic.Add(TestStatus, _systemConfig);
|
_globalInfo.ConfigDic.TryAdd(TestStatus, _systemConfig);
|
||||||
if(_systemConfig.DefaultProgramFilePath != null&&File.Exists(_systemConfig.DefaultProgramFilePath))
|
if(_systemConfig.DefaultProgramFilePath != null&&File.Exists(_systemConfig.DefaultProgramFilePath))
|
||||||
{
|
{
|
||||||
var filePath = _systemConfig.DefaultProgramFilePath;
|
var filePath = _systemConfig.DefaultProgramFilePath;
|
||||||
|
|||||||
@@ -128,6 +128,26 @@
|
|||||||
</vs:ParametersManager.Style>
|
</vs:ParametersManager.Style>
|
||||||
</vs:ParametersManager>
|
</vs:ParametersManager>
|
||||||
|
|
||||||
|
<Border x:Name="Overlay"
|
||||||
|
Background="#40000000"
|
||||||
|
Visibility="Collapsed"
|
||||||
|
Panel.ZIndex="1"
|
||||||
|
Grid.RowSpan="2"
|
||||||
|
Grid.ColumnSpan="4">
|
||||||
|
<StackPanel Width="150"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
Margin="0 0 0 100">
|
||||||
|
<ProgressBar Width="80"
|
||||||
|
Height="80"
|
||||||
|
Margin="20"
|
||||||
|
IsIndeterminate="True"
|
||||||
|
Style="{StaticResource MaterialDesignCircularProgressBar}" />
|
||||||
|
<TextBlock FontSize="30"
|
||||||
|
Text="加载中......"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</Border>
|
</Border>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
using System;
|
using Prism.Events;
|
||||||
|
using MainModule.ViewModels;
|
||||||
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
@@ -12,6 +14,7 @@ using System.Windows.Media;
|
|||||||
using System.Windows.Media.Imaging;
|
using System.Windows.Media.Imaging;
|
||||||
using System.Windows.Navigation;
|
using System.Windows.Navigation;
|
||||||
using System.Windows.Shapes;
|
using System.Windows.Shapes;
|
||||||
|
using UIShare.PubEvent;
|
||||||
|
|
||||||
namespace MainModule.Views
|
namespace MainModule.Views
|
||||||
{
|
{
|
||||||
@@ -20,9 +23,23 @@ namespace MainModule.Views
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class AutomatedTestingView : UserControl
|
public partial class AutomatedTestingView : UserControl
|
||||||
{
|
{
|
||||||
public AutomatedTestingView()
|
public AutomatedTestingView(IEventAggregator eventAggregator)
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
eventAggregator.GetEvent<OverlayEvent>().Subscribe(ShowOverlay);
|
||||||
|
// 台架级加载遮罩:仅台架名称匹配时响应,避免其他台架/主窗口同时变灰
|
||||||
|
eventAggregator.GetEvent<ScopeOverlayEvent>().Subscribe(ShowScopeOverlay);
|
||||||
|
}
|
||||||
|
private void ShowOverlay(bool arg)
|
||||||
|
{
|
||||||
|
Overlay.Visibility = arg ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ShowScopeOverlay(ScopeOverlayArgs args)
|
||||||
|
{
|
||||||
|
// 只处理属于当前台架的遮罩事件(TestStatus 在 OnNavigatedTo 时赋值)
|
||||||
|
if (DataContext is not AutomatedTestingViewModel vm || vm.TestStatus != args.Scope) return;
|
||||||
|
Overlay.Visibility = args.Show ? Visibility.Visible : Visibility.Collapsed;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Model.Entity
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 测试项判断记录:运行过程中测试项(IsTestItem 子程序)范围内每一次 OKExpression 判断。
|
||||||
|
/// 同一次运行的所有记录共享同一个 TestRoundId,导出测试报告时按此 Guid 查询。
|
||||||
|
/// IsSummary=true 的行为该测试项单次执行的汇总(PASS/NG),IsSummary=false 的行为单次判断明细。
|
||||||
|
/// </summary>
|
||||||
|
public class TestCheckRecordEntity : BaseEntity
|
||||||
|
{
|
||||||
|
/// <summary>同一次运行的统一标识(StepRunning.TestRoundID)</summary>
|
||||||
|
[SugarColumn(ColumnName = "TestRoundId", ColumnDescription = "运行轮次标识")]
|
||||||
|
public Guid TestRoundId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>作用域/台架名称</summary>
|
||||||
|
[SugarColumn(ColumnName = "Scope", ColumnDescription = "台架名称")]
|
||||||
|
public string Scope { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>测试项名称(当前ADP文件路径)</summary>
|
||||||
|
[SugarColumn(ColumnName = "FileName", ColumnDescription = "测试项名称")]
|
||||||
|
public string FileName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>测试项名称(IsTestItem 子程序步骤的名称)</summary>
|
||||||
|
[SugarColumn(ColumnName = "TestItemName", Length = 200)]
|
||||||
|
public string TestItemName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>发生判断的步骤名称</summary>
|
||||||
|
[SugarColumn(ColumnName = "StepName", Length = 200)]
|
||||||
|
public string StepName { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>子程序嵌套深度(0=主程序)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Depth")]
|
||||||
|
public int Depth { get; set; }
|
||||||
|
|
||||||
|
/// <summary>判断表达式</summary>
|
||||||
|
[SugarColumn(ColumnName = "OKExpression", Length = 1000, IsNullable = true)]
|
||||||
|
public string? OKExpression { get; set; }
|
||||||
|
|
||||||
|
/// <summary>判断结果(true=PASS / false=NG)</summary>
|
||||||
|
[SugarColumn(ColumnName = "Pass")]
|
||||||
|
public bool Pass { get; set; }
|
||||||
|
|
||||||
|
/// <summary>表达式变量的实际取值(如 "电压=12.5; 电流=3.2; ")</summary>
|
||||||
|
[SugarColumn(ColumnName = "Values", Length = 2000, IsNullable = true)]
|
||||||
|
public string? Values { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否为汇总行(每个测试项单次执行结束时写入一条)</summary>
|
||||||
|
[SugarColumn(ColumnName = "IsSummary")]
|
||||||
|
public bool IsSummary { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="SqlSugarCore" Version="5.1.4.215-preview13" />
|
<PackageReference Include="SqlSugarCore" Version="5.1.4.215-preview13" />
|
||||||
<PackageReference Include="System.IO.Ports" Version="11.0.0-preview.4.26230.115" />
|
<PackageReference Include="System.IO.Ports" Version="8.0.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -12,6 +12,9 @@ namespace Model.Models
|
|||||||
|
|
||||||
public bool IsUsed { get; set; } = true;
|
public bool IsUsed { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断)</summary>
|
||||||
|
public bool IsTestItem { get; set; } = false;
|
||||||
|
|
||||||
public int Index { get; set; }
|
public int Index { get; set; }
|
||||||
|
|
||||||
public string? Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|||||||
@@ -128,5 +128,44 @@ namespace ORM
|
|||||||
throw new Exception("连接数据库失败");
|
throw new Exception("连接数据库失败");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 归档过期数据库文件:将旧的 SQLite 文件重命名为 "SQL_归档_yyyyMMdd_HHmmss.db",
|
||||||
|
/// 后续 <see cref="CreateDatabaseAndCheckConnection"/> 会自动创建新的空数据库。
|
||||||
|
/// <para>
|
||||||
|
/// 必须在 <see cref="InitSqlite"/> 之后、<see cref="CreateDatabaseAndCheckConnection"/> 之前调用,
|
||||||
|
/// 此时连接字符串已就绪但数据库文件尚未被打开。
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="retentionDays">数据库文件保留天数,默认 180 天(半年)</param>
|
||||||
|
public static void TryArchiveOldDatabase(int retentionDays = 180)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// 从连接字符串中提取文件路径(格式:Data Source=xxx;)
|
||||||
|
string dbPath = DbConnectionString
|
||||||
|
.Replace("Data Source=", "", StringComparison.OrdinalIgnoreCase)
|
||||||
|
.TrimEnd(';');
|
||||||
|
|
||||||
|
if (!File.Exists(dbPath)) return;
|
||||||
|
|
||||||
|
var cutoff = DateTime.Now.AddDays(-retentionDays);
|
||||||
|
var fileInfo = new FileInfo(dbPath);
|
||||||
|
|
||||||
|
if (fileInfo.LastWriteTime < cutoff)
|
||||||
|
{
|
||||||
|
string archiveName = $"SQL_归档_{fileInfo.LastWriteTime:yyyyMMdd_HHmmss}.db";
|
||||||
|
string archivePath = Path.Combine(fileInfo.DirectoryName!, archiveName);
|
||||||
|
|
||||||
|
File.Move(dbPath, archivePath);
|
||||||
|
System.Diagnostics.Debug.WriteLine(
|
||||||
|
$"[数据库归档] {dbPath} → {archivePath}(文件最后修改于 {fileInfo.LastWriteTime:yyyy-MM-dd},已超过 {retentionDays} 天)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"数据库归档失败(不影响软件正常使用):{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,38 @@
|
|||||||
|
using Model;
|
||||||
|
using Model.Entity;
|
||||||
|
using ORM;
|
||||||
|
using Service.Interface;
|
||||||
|
using SqlSugar;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Service.Implement
|
||||||
|
{
|
||||||
|
public class TestCheckRecordService : BaseService<TestCheckRecordEntity>, ITestCheckRecordService
|
||||||
|
{
|
||||||
|
public TestCheckRecordService(SqlSugarRepository<TestCheckRecordEntity> repository) : base(repository)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序)
|
||||||
|
/// </summary>
|
||||||
|
public async Task<Result<List<TestCheckRecordEntity>>> GetByTestRoundIdAsync(Guid testRoundId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var list = await _repository.Entities
|
||||||
|
.Where(x => x.TestRoundId == testRoundId)
|
||||||
|
.OrderBy(x => x.CreateTime)
|
||||||
|
.ToListAsync();
|
||||||
|
|
||||||
|
return Result<List<TestCheckRecordEntity>>.Success(list);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return Result<List<TestCheckRecordEntity>>.Error("根据 TestRoundId 查询测试项判断记录失败", ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using Model;
|
||||||
|
using Model.Entity;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Service.Interface
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 测试项判断记录服务:运行时记录测试项范围内每一次 OKExpression 判断,导出测试报告时查询。
|
||||||
|
/// </summary>
|
||||||
|
public interface ITestCheckRecordService : IBaseService<TestCheckRecordEntity>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 根据 TestRoundId 查询该次运行的所有测试项判断记录(按创建时间升序)
|
||||||
|
/// </summary>
|
||||||
|
Task<Result<List<TestCheckRecordEntity>>> GetByTestRoundIdAsync(Guid testRoundId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -192,21 +192,26 @@ namespace TestingModule.ViewModels
|
|||||||
var assembly = Assembly.LoadFrom(dllPath);
|
var assembly = Assembly.LoadFrom(dllPath);
|
||||||
Assemblies.Add(assembly);
|
Assemblies.Add(assembly);
|
||||||
|
|
||||||
// 加载对应的XML注释文件 (项目没有用到)
|
//加载对应的XML注释文件
|
||||||
//string xmlPath = Path.ChangeExtension(dllPath, ".xml");
|
string xmlPath = Path.ChangeExtension(dllPath, ".xml");
|
||||||
//if (File.Exists(xmlPath))
|
if (File.Exists(xmlPath))
|
||||||
//{
|
{
|
||||||
// try
|
try
|
||||||
// {
|
{
|
||||||
// XmlDocument xmlDoc = new XmlDocument();
|
XmlDocument xmlDoc = new XmlDocument();
|
||||||
// xmlDoc.Load(xmlPath);
|
xmlDoc.Load(xmlPath);
|
||||||
// _xmlDocumentCache[assembly.FullName!] = xmlDoc;
|
XmlDocumentCache[assembly.FullName!] = xmlDoc;
|
||||||
// }
|
LoggerHelper.Info($"[XML注释] 成功加载: {Path.GetFileName(xmlPath)} -> 程序集 [{assembly.FullName}]");
|
||||||
// catch (Exception xmlEx)
|
}
|
||||||
// {
|
catch (Exception xmlEx)
|
||||||
// LoggerHelper.WarnWithNotify($"加载XML注释失败: {Path.GetFileName(xmlPath)} - {xmlEx.Message}");
|
{
|
||||||
// }
|
LoggerHelper.Warn($"加载XML注释失败: {Path.GetFileName(xmlPath)} - {xmlEx.Message}");
|
||||||
//}
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn($"[XML注释] XML文件不存在: {xmlPath}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -284,6 +289,7 @@ namespace TestingModule.ViewModels
|
|||||||
{
|
{
|
||||||
Name = subProgram.Name,
|
Name = subProgram.Name,
|
||||||
Tag = subProgram,
|
Tag = subProgram,
|
||||||
|
Tooltip = subProgram.FilePath
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,6 +374,7 @@ namespace TestingModule.ViewModels
|
|||||||
{
|
{
|
||||||
Name = $"{method.Name}({paramText})",
|
Name = $"{method.Name}({paramText})",
|
||||||
Tag = method,
|
Tag = method,
|
||||||
|
Tooltip = GetMethodDocumentation(method),
|
||||||
};
|
};
|
||||||
|
|
||||||
typeNode.Children.Add(methodNode);
|
typeNode.Children.Add(methodNode);
|
||||||
@@ -433,6 +440,95 @@ namespace TestingModule.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 添加获取注释的方法
|
||||||
|
private string? GetMethodDocumentation(MethodInfo method)
|
||||||
|
{
|
||||||
|
if (method.DeclaringType == null) return null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string assemblyName = method.DeclaringType.Assembly.FullName!;
|
||||||
|
if (!_xmlDocumentCache.TryGetValue(assemblyName, out XmlDocument? xmlDoc))
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn($"[XML注释] 缓存未命中: 程序集 [{assemblyName}],缓存包含 { _xmlDocumentCache.Count} 个条目: [{string.Join(", ", _xmlDocumentCache.Keys)}]");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成XML文档中的成员ID
|
||||||
|
string memberName = $"M:{method.DeclaringType.FullName}.{method.Name}";
|
||||||
|
var parameters = method.GetParameters();
|
||||||
|
if (parameters.Length > 0)
|
||||||
|
{
|
||||||
|
memberName += "(" + string.Join(",", parameters.Select(p => p.ParameterType.FullName)) + ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找注释节点
|
||||||
|
XmlNode? memberNode = xmlDoc.SelectSingleNode($"//member[@name='{memberName}']");
|
||||||
|
if (memberNode == null)
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn($"[XML注释] 节点未找到: {memberName}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取摘要(summary)
|
||||||
|
var summaryNode = memberNode.SelectSingleNode("summary");
|
||||||
|
string documentation = "";
|
||||||
|
|
||||||
|
if (summaryNode != null)
|
||||||
|
{
|
||||||
|
documentation += CleanXmlContent(summaryNode.InnerXml);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取参数注释(param)
|
||||||
|
var paramNodes = memberNode.SelectNodes("param");
|
||||||
|
if (paramNodes != null && paramNodes.Count > 0)
|
||||||
|
{
|
||||||
|
documentation += "\n\n参数:";
|
||||||
|
foreach (XmlNode paramNode in paramNodes)
|
||||||
|
{
|
||||||
|
string? paramName = paramNode.Attributes?["name"]?.Value;
|
||||||
|
if (!string.IsNullOrEmpty(paramName))
|
||||||
|
{
|
||||||
|
documentation += $"\n • {paramName}: {CleanXmlContent(paramNode.InnerXml)}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取返回值注释(returns)
|
||||||
|
var returnsNode = memberNode.SelectSingleNode("returns");
|
||||||
|
if (returnsNode != null)
|
||||||
|
{
|
||||||
|
documentation += $"\n\n返回值: {CleanXmlContent(returnsNode.InnerXml)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(documentation)
|
||||||
|
? null
|
||||||
|
: System.Net.WebUtility.HtmlDecode(documentation.Trim());
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn($"获取注释失败: {method.Name} - {ex.Message}");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助方法:清理XML内容
|
||||||
|
private string CleanXmlContent(string xmlContent)
|
||||||
|
{
|
||||||
|
return xmlContent
|
||||||
|
.Replace("<see cref=\"", "")
|
||||||
|
.Replace("\"/>", "")
|
||||||
|
.Replace("<para>", "\n")
|
||||||
|
.Replace("</para>", "")
|
||||||
|
.Replace("<seealso", "")
|
||||||
|
.Replace("/>", "")
|
||||||
|
.Replace("<c>", "") // 处理代码标签
|
||||||
|
.Replace("</c>", "")
|
||||||
|
.Replace("<code>", "")
|
||||||
|
.Replace("</code>", "")
|
||||||
|
.Trim();
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
#region 指令添加
|
#region 指令添加
|
||||||
|
|
||||||
@@ -515,7 +611,7 @@ namespace TestingModule.ViewModels
|
|||||||
var newStep = new StepVM
|
var newStep = new StepVM
|
||||||
{
|
{
|
||||||
Name = subProgram.Name,
|
Name = subProgram.Name,
|
||||||
StepType = "子程序"
|
StepType = "子程序",
|
||||||
};
|
};
|
||||||
var jsonstr = File.ReadAllText($"{subProgram.FilePath}");
|
var jsonstr = File.ReadAllText($"{subProgram.FilePath}");
|
||||||
var tmp = JsonConvert.DeserializeObject<ProgramVM>(jsonstr);
|
var tmp = JsonConvert.DeserializeObject<ProgramVM>(jsonstr);
|
||||||
@@ -568,16 +664,35 @@ namespace TestingModule.ViewModels
|
|||||||
{
|
{
|
||||||
// 查找最近的未匹配循环开始
|
// 查找最近的未匹配循环开始
|
||||||
StepVM? lastUnmatchedLoopStart = null;
|
StepVM? lastUnmatchedLoopStart = null;
|
||||||
for (int i = Program.StepCollection.Count - 1; i >= 0; i--)
|
if (_ScopedContext.SelectedStepList == "主程序")
|
||||||
{
|
{
|
||||||
if (Program.StepCollection[i].StepType == "循环开始")
|
for (int i = Program.StepCollection.Count - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
bool isMatched = Program.StepCollection.Any(s => s.StepType == "循环结束" && s.LoopStartStepId == Program.StepCollection[i].ID);
|
if (Program.StepCollection[i].StepType == "循环开始")
|
||||||
|
|
||||||
if (!isMatched)
|
|
||||||
{
|
{
|
||||||
lastUnmatchedLoopStart = Program.StepCollection[i];
|
bool isMatched = Program.StepCollection.Any(s => s.StepType == "循环结束" && s.LoopStartStepId == Program.StepCollection[i].ID);
|
||||||
break;
|
|
||||||
|
if (!isMatched)
|
||||||
|
{
|
||||||
|
lastUnmatchedLoopStart = Program.StepCollection[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
for (int i = Program.ErrorStepCollection.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
if (Program.ErrorStepCollection[i].StepType == "循环开始")
|
||||||
|
{
|
||||||
|
bool isMatched = Program.ErrorStepCollection.Any(s => s.StepType == "循环结束" && s.LoopStartStepId == Program.StepCollection[i].ID);
|
||||||
|
|
||||||
|
if (!isMatched)
|
||||||
|
{
|
||||||
|
lastUnmatchedLoopStart = Program.ErrorStepCollection[i];
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -94,6 +94,30 @@ namespace TestingModule.ViewModels
|
|||||||
public ICommand DeleteStepCommand { get;set; }
|
public ICommand DeleteStepCommand { get;set; }
|
||||||
public ICommand TabSelectionChangedCommand { get;set; }
|
public ICommand TabSelectionChangedCommand { get;set; }
|
||||||
public ICommand SelectionChangedCommand { get;set; }
|
public ICommand SelectionChangedCommand { get;set; }
|
||||||
|
public ICommand OpenSubProgramCommand { get; set; }
|
||||||
|
public ICommand GoBackCommand { get; set; }
|
||||||
|
public ICommand ToggleTestItemCommand { get; set; }
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 子程序导航(主程序 / 错误程序各自独立)
|
||||||
|
|
||||||
|
// --- 主程序 Tab 导航属性 ---
|
||||||
|
public ProgramVM MainCurrentProgram => _ScopedContext.MainNav.CurrentProgram;
|
||||||
|
public string MainBreadcrumbPath => _ScopedContext.MainNav.BreadcrumbPath;
|
||||||
|
public bool MainCanGoBack => _ScopedContext.MainNav.CanGoBack;
|
||||||
|
public ObservableCollection<StepVM> MainDisplaySteps => _ScopedContext.MainNav.DisplaySteps;
|
||||||
|
|
||||||
|
// --- 错误程序 Tab 导航属性 ---
|
||||||
|
public ProgramVM ErrorCurrentProgram => _ScopedContext.ErrorNav.CurrentProgram;
|
||||||
|
public string ErrorBreadcrumbPath => _ScopedContext.ErrorNav.BreadcrumbPath;
|
||||||
|
public bool ErrorCanGoBack => _ScopedContext.ErrorNav.CanGoBack;
|
||||||
|
public ObservableCollection<StepVM> ErrorDisplaySteps => _ScopedContext.ErrorNav.DisplaySteps;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 当前激活的导航状态(根据选中的 Tab 索引决定)
|
||||||
|
/// </summary>
|
||||||
|
private ProgramNavigationState ActiveNav => SelectedTabIndex == 1 ? _ScopedContext.ErrorNav : _ScopedContext.MainNav;
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
public StepsManagerViewModel(IContainerProvider containerProvider, ScopedContext scopedContext, SystemConfig systemConfig, GlobalInfo globalInfo) : base(containerProvider)
|
public StepsManagerViewModel(IContainerProvider containerProvider, ScopedContext scopedContext, SystemConfig systemConfig, GlobalInfo globalInfo) : base(containerProvider)
|
||||||
@@ -108,17 +132,62 @@ namespace TestingModule.ViewModels
|
|||||||
DeleteStepCommand = new DelegateCommand(DeleteStep);
|
DeleteStepCommand = new DelegateCommand(DeleteStep);
|
||||||
TabSelectionChangedCommand = new DelegateCommand<string>(TabSelectionChanged);
|
TabSelectionChangedCommand = new DelegateCommand<string>(TabSelectionChanged);
|
||||||
SelectionChangedCommand = new DelegateCommand<object>(SelectionChanged);
|
SelectionChangedCommand = new DelegateCommand<object>(SelectionChanged);
|
||||||
|
OpenSubProgramCommand = new DelegateCommand(OpenSubProgram);
|
||||||
|
GoBackCommand = new DelegateCommand(GoBack);
|
||||||
|
ToggleTestItemCommand = new DelegateCommand(ToggleTestItem);
|
||||||
SubscribeStepCollections();
|
SubscribeStepCollections();
|
||||||
Program.PropertyChanged += Program_PropertyChanged;
|
Program.PropertyChanged += Program_PropertyChanged;
|
||||||
Admin = _globalInfo.IsAdmin;
|
Admin = _globalInfo.IsAdmin;
|
||||||
|
|
||||||
|
// 初始化两套导航状态
|
||||||
|
_ScopedContext.MainNav.Initialize(Program, "主程序");
|
||||||
|
_ScopedContext.ErrorNav.Initialize(Program, "错误程序");
|
||||||
|
|
||||||
|
// 订阅主程序导航状态变化 → 转发给 UI
|
||||||
|
_ScopedContext.MainNav.PropertyChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ProgramNavigationState.CurrentProgram))
|
||||||
|
{
|
||||||
|
RaisePropertyChanged(nameof(MainCurrentProgram));
|
||||||
|
RaisePropertyChanged(nameof(MainDisplaySteps));
|
||||||
|
}
|
||||||
|
else if (e.PropertyName == nameof(ProgramNavigationState.BreadcrumbPath))
|
||||||
|
RaisePropertyChanged(nameof(MainBreadcrumbPath));
|
||||||
|
else if (e.PropertyName == nameof(ProgramNavigationState.CanGoBack))
|
||||||
|
RaisePropertyChanged(nameof(MainCanGoBack));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 订阅错误程序导航状态变化 → 转发给 UI
|
||||||
|
_ScopedContext.ErrorNav.PropertyChanged += (s, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName == nameof(ProgramNavigationState.CurrentProgram))
|
||||||
|
{
|
||||||
|
RaisePropertyChanged(nameof(ErrorCurrentProgram));
|
||||||
|
RaisePropertyChanged(nameof(ErrorDisplaySteps));
|
||||||
|
}
|
||||||
|
else if (e.PropertyName == nameof(ProgramNavigationState.BreadcrumbPath))
|
||||||
|
RaisePropertyChanged(nameof(ErrorBreadcrumbPath));
|
||||||
|
else if (e.PropertyName == nameof(ProgramNavigationState.CanGoBack))
|
||||||
|
RaisePropertyChanged(nameof(ErrorCanGoBack));
|
||||||
|
};
|
||||||
|
|
||||||
|
// 订阅子程序导航事件(运行时由 StepRunning 发布)
|
||||||
|
_eventAggregator.GetEvent<SubProgramNavigateEvent>()
|
||||||
|
.Subscribe(OnSubProgramNavigate, ThreadOption.UIThread, false,
|
||||||
|
payload => payload.Scope == _systemConfig.Title);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SelectionChanged(object parameter)
|
private void SelectionChanged(object parameter)
|
||||||
{
|
{
|
||||||
var selectedList = parameter as IList;
|
if (parameter is IList list && list.Count > 0)
|
||||||
if (selectedList != null)
|
|
||||||
{
|
{
|
||||||
SelectedItems = selectedList.Cast<StepVM>().ToList();
|
SelectedItems = list.Cast<StepVM>().ToList();
|
||||||
|
}
|
||||||
|
else if (parameter is IEnumerable enumerable && parameter is not string)
|
||||||
|
{
|
||||||
|
var items = enumerable.Cast<StepVM>().ToList();
|
||||||
|
if (items.Count > 0)
|
||||||
|
SelectedItems = items;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -137,13 +206,19 @@ namespace TestingModule.ViewModels
|
|||||||
}
|
}
|
||||||
private void CopyStep()
|
private void CopyStep()
|
||||||
{
|
{
|
||||||
if (_globalInfo.IsAdmin && SelectedItems.Any())
|
if (!_globalInfo.IsAdmin) return;
|
||||||
|
|
||||||
|
// 优先用多选列表,回退到单选
|
||||||
|
var source = (SelectedItems != null && SelectedItems.Any())
|
||||||
|
? SelectedItems
|
||||||
|
: (SelectedStep != null ? new List<StepVM> { SelectedStep } : null);
|
||||||
|
|
||||||
|
if (source == null || !source.Any()) return;
|
||||||
|
|
||||||
|
tmpCopyList.Clear();
|
||||||
|
foreach (var item in source)
|
||||||
{
|
{
|
||||||
tmpCopyList.Clear();
|
tmpCopyList.Add(item);
|
||||||
foreach (var item in SelectedItems)
|
|
||||||
{
|
|
||||||
tmpCopyList.Add(item);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private void PasteStep()
|
private void PasteStep()
|
||||||
@@ -180,32 +255,116 @@ namespace TestingModule.ViewModels
|
|||||||
}
|
}
|
||||||
private void DeleteStep()
|
private void DeleteStep()
|
||||||
{
|
{
|
||||||
// 确保有选中的项
|
if (!_globalInfo.IsAdmin) return;
|
||||||
if (_globalInfo.IsAdmin && SelectedItems != null && SelectedItems.Any())
|
|
||||||
|
// 优先用多选列表,回退到单选
|
||||||
|
var source = (SelectedItems != null && SelectedItems.Any())
|
||||||
|
? SelectedItems.ToList()
|
||||||
|
: (SelectedStep != null ? new List<StepVM> { SelectedStep } : null);
|
||||||
|
|
||||||
|
if (source == null || !source.Any()) return;
|
||||||
|
|
||||||
|
foreach (var item in source)
|
||||||
{
|
{
|
||||||
// 创建一个副本进行循环,防止在 Remove 过程中集合变化导致的问题
|
_eventAggregator.GetEvent<DeletedStepEvent>().Publish(item.ID);
|
||||||
var toDelete = SelectedItems.ToList();
|
|
||||||
|
|
||||||
foreach (var item in toDelete)
|
if (_ScopedContext.SelectedStepList == "主程序")
|
||||||
{
|
Program.StepCollection.Remove(item);
|
||||||
_eventAggregator.GetEvent<DeletedStepEvent>().Publish(item.ID);
|
else if (_ScopedContext.SelectedStepList == "错误程序")
|
||||||
|
Program.ErrorStepCollection.Remove(item);
|
||||||
|
}
|
||||||
|
|
||||||
if (_ScopedContext.SelectedStepList == "主程序")
|
// 清空 ViewModel 的选中状态,避免悬挂引用
|
||||||
{
|
SelectedStep = null;
|
||||||
Program.StepCollection.Remove(item);
|
SelectedItems?.Clear();
|
||||||
}
|
_ScopedContext.SelectedStep = null;
|
||||||
else if (_ScopedContext.SelectedStepList == "错误程序")
|
}
|
||||||
{
|
#endregion
|
||||||
Program.ErrorStepCollection.Remove(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 清空 ViewModel 的选中状态,避免悬挂引用
|
#region 子程序导航方法
|
||||||
SelectedStep = null;
|
|
||||||
SelectedItems.Clear();
|
/// <summary>
|
||||||
_ScopedContext.SelectedStep = null;
|
/// 编辑模式:右键打开子程序(操作当前激活的 Tab 的导航状态)
|
||||||
|
/// </summary>
|
||||||
|
private void OpenSubProgram()
|
||||||
|
{
|
||||||
|
if (SelectedStep == null || SelectedStep.StepType != "子程序" || SelectedStep.SubProgram == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var nav = ActiveNav;
|
||||||
|
nav.NavigationStack.Push(nav.CurrentProgram);
|
||||||
|
nav.UpdateCanGoBack();
|
||||||
|
var stepName = SelectedStep.Name ?? "子程序";
|
||||||
|
nav.BreadcrumbPath = $"{nav.BreadcrumbPath} > {stepName}";
|
||||||
|
nav.CurrentProgram = SelectedStep.SubProgram;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 切换选中步骤的测试项标记(仅子程序可标记,运行时将记录其范围内所有 OKExpression 判断)
|
||||||
|
/// </summary>
|
||||||
|
private void ToggleTestItem()
|
||||||
|
{
|
||||||
|
if (!_globalInfo.IsAdmin) return;
|
||||||
|
|
||||||
|
var source = (SelectedItems != null && SelectedItems.Any())
|
||||||
|
? SelectedItems
|
||||||
|
: (SelectedStep != null ? new List<StepVM> { SelectedStep } : null);
|
||||||
|
|
||||||
|
if (source == null || !source.Any()) return;
|
||||||
|
|
||||||
|
foreach (var item in source.Where(x => x.StepType == "子程序" && x.SubProgram != null))
|
||||||
|
{
|
||||||
|
item.IsTestItem = !item.IsTestItem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 返回上一级程序(操作当前激活的 Tab 的导航状态)
|
||||||
|
/// </summary>
|
||||||
|
private void GoBack()
|
||||||
|
{
|
||||||
|
var nav = ActiveNav;
|
||||||
|
if (nav.NavigationStack.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var parentProgram = nav.NavigationStack.Pop();
|
||||||
|
nav.UpdateCanGoBack();
|
||||||
|
nav.CurrentProgram = parentProgram;
|
||||||
|
|
||||||
|
var parts = nav.BreadcrumbPath.Split(" > ");
|
||||||
|
nav.BreadcrumbPath = parts.Length > 1
|
||||||
|
? string.Join(" > ", parts.Take(parts.Length - 1))
|
||||||
|
: nav == _ScopedContext.MainNav ? "主程序" : "错误程序";
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 运行时:处理 StepRunning 发布的子程序导航事件
|
||||||
|
/// </summary>
|
||||||
|
private void OnSubProgramNavigate(SubProgramNavigatePayload payload)
|
||||||
|
{
|
||||||
|
if (_ScopedContext == null || payload == null) return;
|
||||||
|
var nav = payload.IsErrorProgram ? _ScopedContext.ErrorNav : _ScopedContext.MainNav;
|
||||||
|
if (payload.Action == NavigateAction.Enter && payload.SubProgram != null)
|
||||||
|
{
|
||||||
|
nav.NavigationStack.Push(nav.CurrentProgram);
|
||||||
|
nav.UpdateCanGoBack();
|
||||||
|
nav.CurrentProgram = payload.SubProgram;
|
||||||
|
var stepName = payload.StepName ?? "子程序";
|
||||||
|
nav.BreadcrumbPath = $"{nav.BreadcrumbPath} > {stepName}";
|
||||||
|
}
|
||||||
|
else if (payload.Action == NavigateAction.Exit)
|
||||||
|
{
|
||||||
|
if (nav.NavigationStack.Count == 0) return;
|
||||||
|
var parentProgram = nav.NavigationStack.Pop();
|
||||||
|
nav.UpdateCanGoBack();
|
||||||
|
nav.CurrentProgram = parentProgram;
|
||||||
|
var parts = nav.BreadcrumbPath.Split(" > ");
|
||||||
|
nav.BreadcrumbPath = parts.Length > 1
|
||||||
|
? string.Join(" > ", parts.Take(parts.Length - 1))
|
||||||
|
: nav == _ScopedContext.MainNav ? "主程序" : "错误程序";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 辅助方法
|
#region 辅助方法
|
||||||
@@ -252,6 +411,12 @@ namespace TestingModule.ViewModels
|
|||||||
UnsubscribeStepCollections();
|
UnsubscribeStepCollections();
|
||||||
SubscribeStepCollections();
|
SubscribeStepCollections();
|
||||||
|
|
||||||
|
// 通知 UI 刷新 DisplaySteps(导航状态虽然引用同一 Program,但集合实例已变)
|
||||||
|
RaisePropertyChanged(nameof(MainDisplaySteps));
|
||||||
|
RaisePropertyChanged(nameof(ErrorDisplaySteps));
|
||||||
|
RaisePropertyChanged(nameof(MainCurrentProgram));
|
||||||
|
RaisePropertyChanged(nameof(ErrorCurrentProgram));
|
||||||
|
|
||||||
// 集合被整体替换后,对新集合重新编号
|
// 集合被整体替换后,对新集合重新编号
|
||||||
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
|
Application.Current?.Dispatcher.BeginInvoke(new Action(() =>
|
||||||
{
|
{
|
||||||
@@ -304,9 +469,9 @@ namespace TestingModule.ViewModels
|
|||||||
Program.PropertyChanged -= Program_PropertyChanged;
|
Program.PropertyChanged -= Program_PropertyChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 【核心修复】必须显式退订 Prism 全局事件(AlarmEvent)
|
// 3. 显式退订全局 Prism 事件
|
||||||
// 注意:因为订阅时使用的是匿名 Lambda,最安全稳妥的退订方式是把整个事件上的当前 VM 订阅者全部注销
|
|
||||||
_eventAggregator?.GetEvent<AlarmEvent>()?.Unsubscribe(null);
|
_eventAggregator?.GetEvent<AlarmEvent>()?.Unsubscribe(null);
|
||||||
|
_eventAggregator?.GetEvent<SubProgramNavigateEvent>()?.Unsubscribe(null);
|
||||||
|
|
||||||
// 4. 清空临时缓存集合与 UI 绑定列表,避免悬挂指针
|
// 4. 清空临时缓存集合与 UI 绑定列表,避免悬挂指针
|
||||||
tmpCopyList?.Clear();
|
tmpCopyList?.Clear();
|
||||||
|
|||||||
@@ -53,7 +53,8 @@
|
|||||||
<TreeView.Resources>
|
<TreeView.Resources>
|
||||||
<HierarchicalDataTemplate DataType="{x:Type model:InstructionNodeVM}"
|
<HierarchicalDataTemplate DataType="{x:Type model:InstructionNodeVM}"
|
||||||
ItemsSource="{Binding Children}">
|
ItemsSource="{Binding Children}">
|
||||||
<TextBlock Text="{Binding Name}" />
|
<TextBlock Text="{Binding Name}" ToolTip="{Binding Tooltip}"/>
|
||||||
|
|
||||||
</HierarchicalDataTemplate>
|
</HierarchicalDataTemplate>
|
||||||
</TreeView.Resources>
|
</TreeView.Resources>
|
||||||
<!-- 双击 -->
|
<!-- 双击 -->
|
||||||
|
|||||||
@@ -103,7 +103,7 @@
|
|||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid>
|
<Grid>
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="100" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="50" />
|
<ColumnDefinition Width="50" />
|
||||||
<ColumnDefinition />
|
<ColumnDefinition />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
@@ -240,7 +240,7 @@
|
|||||||
<DataTemplate>
|
<DataTemplate>
|
||||||
<Grid Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}, ConverterParameter=Item}">
|
<Grid Visibility="{Binding Category, Converter={StaticResource ParameterCategoryToVisibilityConverter}, ConverterParameter=Item}">
|
||||||
<Grid.ColumnDefinitions>
|
<Grid.ColumnDefinitions>
|
||||||
<ColumnDefinition Width="100" />
|
<ColumnDefinition Width="*" />
|
||||||
<ColumnDefinition Width="50" />
|
<ColumnDefinition Width="50" />
|
||||||
<ColumnDefinition />
|
<ColumnDefinition />
|
||||||
</Grid.ColumnDefinitions>
|
</Grid.ColumnDefinitions>
|
||||||
|
|||||||
@@ -8,9 +8,13 @@
|
|||||||
xmlns:behaviors="clr-namespace:UIShare.Behaviors;assembly=UIShare"
|
xmlns:behaviors="clr-namespace:UIShare.Behaviors;assembly=UIShare"
|
||||||
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
xmlns:materialDesign="http://materialdesigninxaml.net/winfx/xaml/themes"
|
||||||
xmlns:prism="http://prismlibrary.com/"
|
xmlns:prism="http://prismlibrary.com/"
|
||||||
|
xmlns:converters="clr-namespace:UIShare.Converters;assembly=UIShare"
|
||||||
d:DesignHeight="450"
|
d:DesignHeight="450"
|
||||||
d:DesignWidth="800"
|
d:DesignWidth="800"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter"/>
|
||||||
|
</UserControl.Resources>
|
||||||
<Grid>
|
<Grid>
|
||||||
<GroupBox Header="{Binding Title}">
|
<GroupBox Header="{Binding Title}">
|
||||||
<TabControl SelectedIndex="{Binding SelectedTabIndex, Mode=TwoWay}">
|
<TabControl SelectedIndex="{Binding SelectedTabIndex, Mode=TwoWay}">
|
||||||
@@ -19,6 +23,17 @@
|
|||||||
CommandParameter="{Binding SelectedTabHeader}" />
|
CommandParameter="{Binding SelectedTabHeader}" />
|
||||||
</i:Interaction.Behaviors>
|
</i:Interaction.Behaviors>
|
||||||
<TabItem Header="主程序">
|
<TabItem Header="主程序">
|
||||||
|
<DockPanel>
|
||||||
|
<!-- 主程序面包屑导航栏 -->
|
||||||
|
<Border DockPanel.Dock="Top" Background="#F0F0F0" Padding="6,3" Visibility="{Binding MainCanGoBack, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right" Content="↩ 返回上一级"
|
||||||
|
Command="{Binding GoBackCommand}"
|
||||||
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
|
FontSize="12" Padding="8,2" Margin="8,0,0,0"/>
|
||||||
|
<TextBlock Text="{Binding MainBreadcrumbPath}" VerticalAlignment="Center" FontSize="12" Foreground="#555"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
<DataGrid dd:DragDrop.IsDragSource="{Binding Admin}"
|
<DataGrid dd:DragDrop.IsDragSource="{Binding Admin}"
|
||||||
dd:DragDrop.IsDropTarget="{Binding Admin}"
|
dd:DragDrop.IsDropTarget="{Binding Admin}"
|
||||||
dd:DragDrop.UseDefaultDragAdorner="{Binding Admin}"
|
dd:DragDrop.UseDefaultDragAdorner="{Binding Admin}"
|
||||||
@@ -29,7 +44,7 @@
|
|||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
CanUserAddRows="False"
|
CanUserAddRows="False"
|
||||||
CanUserSortColumns="False"
|
CanUserSortColumns="False"
|
||||||
ItemsSource="{Binding Program.StepCollection}"
|
ItemsSource="{Binding MainDisplaySteps}"
|
||||||
SelectedItem="{Binding SelectedStep, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
SelectedItem="{Binding SelectedStep, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||||
SelectionMode="Extended"
|
SelectionMode="Extended"
|
||||||
SelectionUnit="FullRow">
|
SelectionUnit="FullRow">
|
||||||
@@ -38,6 +53,10 @@
|
|||||||
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
||||||
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||||
</i:EventTrigger>
|
</i:EventTrigger>
|
||||||
|
<i:EventTrigger EventName="ContextMenuOpening">
|
||||||
|
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
||||||
|
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||||
|
</i:EventTrigger>
|
||||||
</i:Interaction.Triggers>
|
</i:Interaction.Triggers>
|
||||||
<!-- 行样式 -->
|
<!-- 行样式 -->
|
||||||
<DataGrid.RowStyle>
|
<DataGrid.RowStyle>
|
||||||
@@ -80,6 +99,10 @@
|
|||||||
<DataGridCheckBoxColumn Width="58"
|
<DataGridCheckBoxColumn Width="58"
|
||||||
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
|
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Header="启用" />
|
Header="启用" />
|
||||||
|
<DataGridCheckBoxColumn Width="58"
|
||||||
|
Binding="{Binding IsTestItem}"
|
||||||
|
Header="测试项"
|
||||||
|
IsReadOnly="True" />
|
||||||
<DataGridTextColumn Binding="{Binding Index}"
|
<DataGridTextColumn Binding="{Binding Index}"
|
||||||
Header="序号"
|
Header="序号"
|
||||||
IsReadOnly="True" />
|
IsReadOnly="True" />
|
||||||
@@ -120,14 +143,31 @@
|
|||||||
<MenuItem Header="删除"
|
<MenuItem Header="删除"
|
||||||
Foreground="Red"
|
Foreground="Red"
|
||||||
Command="{Binding DeleteStepCommand}" />
|
Command="{Binding DeleteStepCommand}" />
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="打开子程序"
|
||||||
|
Command="{Binding OpenSubProgramCommand}" />
|
||||||
|
<MenuItem Header="标记/取消测试项"
|
||||||
|
Command="{Binding ToggleTestItemCommand}" />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</DataGrid.ContextMenu>
|
</DataGrid.ContextMenu>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
</DockPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
<TabItem Header="错误程序">
|
<TabItem Header="错误程序">
|
||||||
|
<DockPanel>
|
||||||
|
<!-- 错误程序面包屑导航栏 -->
|
||||||
|
<Border DockPanel.Dock="Top" Background="#F0F0F0" Padding="6,3" Visibility="{Binding ErrorCanGoBack, Converter={StaticResource BooleanToVisibilityConverter}}">
|
||||||
|
<DockPanel>
|
||||||
|
<Button DockPanel.Dock="Right" Content="↩ 返回上一级"
|
||||||
|
Command="{Binding GoBackCommand}"
|
||||||
|
Style="{StaticResource MaterialDesignFlatButton}"
|
||||||
|
FontSize="12" Padding="8,2" Margin="8,0,0,0"/>
|
||||||
|
<TextBlock Text="{Binding ErrorBreadcrumbPath}" VerticalAlignment="Center" FontSize="12" Foreground="#555"/>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
<DataGrid dd:DragDrop.IsDragSource="True"
|
<DataGrid dd:DragDrop.IsDragSource="True"
|
||||||
dd:DragDrop.IsDropTarget="True"
|
dd:DragDrop.IsDropTarget="True"
|
||||||
dd:DragDrop.UseDefaultDragAdorner="True"
|
dd:DragDrop.UseDefaultDragAdorner="True"
|
||||||
@@ -135,7 +175,7 @@
|
|||||||
Background="Transparent"
|
Background="Transparent"
|
||||||
CanUserAddRows="False"
|
CanUserAddRows="False"
|
||||||
CanUserSortColumns="False"
|
CanUserSortColumns="False"
|
||||||
ItemsSource="{Binding Program.ErrorStepCollection}"
|
ItemsSource="{Binding ErrorDisplaySteps}"
|
||||||
SelectedItem="{Binding SelectedStep, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
SelectedItem="{Binding SelectedStep, Mode=TwoWay,UpdateSourceTrigger=PropertyChanged}"
|
||||||
SelectionMode="Extended"
|
SelectionMode="Extended"
|
||||||
SelectionUnit="FullRow">
|
SelectionUnit="FullRow">
|
||||||
@@ -144,6 +184,10 @@
|
|||||||
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
||||||
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||||
</i:EventTrigger>
|
</i:EventTrigger>
|
||||||
|
<i:EventTrigger EventName="ContextMenuOpening">
|
||||||
|
<i:InvokeCommandAction Command="{Binding SelectionChangedCommand}"
|
||||||
|
CommandParameter="{Binding SelectedItems, RelativeSource={RelativeSource AncestorType=DataGrid}}" />
|
||||||
|
</i:EventTrigger>
|
||||||
</i:Interaction.Triggers>
|
</i:Interaction.Triggers>
|
||||||
<!-- 行样式 -->
|
<!-- 行样式 -->
|
||||||
<DataGrid.RowStyle>
|
<DataGrid.RowStyle>
|
||||||
@@ -186,6 +230,10 @@
|
|||||||
<DataGridCheckBoxColumn Width="58"
|
<DataGridCheckBoxColumn Width="58"
|
||||||
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
|
Binding="{Binding IsUsed, UpdateSourceTrigger=PropertyChanged}"
|
||||||
Header="启用" />
|
Header="启用" />
|
||||||
|
<DataGridCheckBoxColumn Width="58"
|
||||||
|
Binding="{Binding IsTestItem}"
|
||||||
|
Header="测试项"
|
||||||
|
IsReadOnly="True" />
|
||||||
<DataGridTextColumn Binding="{Binding Index}"
|
<DataGridTextColumn Binding="{Binding Index}"
|
||||||
Header="序号"
|
Header="序号"
|
||||||
IsReadOnly="True" />
|
IsReadOnly="True" />
|
||||||
@@ -226,6 +274,11 @@
|
|||||||
<MenuItem Header="删除"
|
<MenuItem Header="删除"
|
||||||
Foreground="Red"
|
Foreground="Red"
|
||||||
Command="{Binding DeleteStepCommand}" />
|
Command="{Binding DeleteStepCommand}" />
|
||||||
|
<Separator/>
|
||||||
|
<MenuItem Header="打开子程序"
|
||||||
|
Command="{Binding OpenSubProgramCommand}" />
|
||||||
|
<MenuItem Header="标记/取消测试项"
|
||||||
|
Command="{Binding ToggleTestItemCommand}" />
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
</DataGrid.ContextMenu>
|
</DataGrid.ContextMenu>
|
||||||
|
|
||||||
@@ -239,6 +292,7 @@
|
|||||||
</i:Interaction.Triggers>-->
|
</i:Interaction.Triggers>-->
|
||||||
|
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
|
</DockPanel>
|
||||||
</TabItem>
|
</TabItem>
|
||||||
</TabControl>
|
</TabControl>
|
||||||
</GroupBox>
|
</GroupBox>
|
||||||
|
|||||||
@@ -29,52 +29,7 @@ namespace UIShare.GlobalVariable
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
/// <summary>
|
|
||||||
/// 根据标题(格子标识)加载独立的配置文件
|
|
||||||
/// </summary>
|
|
||||||
public static SystemConfig Load(string title)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(title))
|
|
||||||
{
|
|
||||||
throw new ArgumentException("配置标题不能为空", nameof(title));
|
|
||||||
}
|
|
||||||
|
|
||||||
// 临时实例化一个对象以获取默认的 SystemPath
|
|
||||||
var dummy = new SystemConfig();
|
|
||||||
string configPath = Path.Combine(dummy.SystemPath, $"{title}.json");
|
|
||||||
|
|
||||||
if (!File.Exists(configPath))
|
|
||||||
{
|
|
||||||
// 如果不存在,创建一个带 Title 的默认配置并保存
|
|
||||||
var defaultConfig = new SystemConfig { Title = title };
|
|
||||||
EnsureDefaultCanDevice(defaultConfig);
|
|
||||||
Save(defaultConfig);
|
|
||||||
return defaultConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_fileLock)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string json = File.ReadAllText(configPath);
|
|
||||||
var config = JsonConvert.DeserializeObject<SystemConfig>(json, new JsonSerializerSettings
|
|
||||||
{
|
|
||||||
TypeNameHandling = TypeNameHandling.All
|
|
||||||
});
|
|
||||||
|
|
||||||
config ??= new SystemConfig { Title = title };
|
|
||||||
EnsureDefaultCanDevice(config);
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
LoggerHelper.ErrorWithNotify(title, $"格子 [{title}] 配置加载失败: {ex.Message}");
|
|
||||||
var fallback = new SystemConfig { Title = title };
|
|
||||||
EnsureDefaultCanDevice(fallback);
|
|
||||||
return fallback;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 保存指定的配置实例
|
/// 保存指定的配置实例
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
using DeviceCommand.Base;
|
||||||
|
using Logger;
|
||||||
|
using Model.Models;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Windows;
|
||||||
|
|
||||||
|
namespace UIShare.GlobalVariable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 设备健康监控器:独立于监控采样的心跳重连机制。
|
||||||
|
/// <para>
|
||||||
|
/// 设计原则——与检测值零冲突:
|
||||||
|
/// <list type="bullet">
|
||||||
|
/// <item>健康检查平时只读 <see cref="IBaseInterface.IsConnected"/>(纯本地属性,零网络开销,不碰通信锁)</item>
|
||||||
|
/// <item>仅在 IsConnected==false 时才获取 _commLock 执行重连,与监控采样天然串行化</item>
|
||||||
|
/// <item>ModbusTcp 的 ConnectAsync 是幂等的(已连接时直接返回),不会中断正在进行的监控</item>
|
||||||
|
/// <item>TCP 的 ConnectAsync 会重置连接,但监控的 catch 容忍单次失败,下次 tick 自动恢复</item>
|
||||||
|
/// </list>
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
public class DeviceHealthMonitor : IDisposable
|
||||||
|
{
|
||||||
|
private readonly IDictionary<string, IBaseInterface> _deviceMap;
|
||||||
|
private readonly SystemConfig _systemConfig;
|
||||||
|
private readonly string _scopeName;
|
||||||
|
|
||||||
|
/// <summary>健康检查定时器</summary>
|
||||||
|
private Timer? _healthCheckTimer;
|
||||||
|
|
||||||
|
/// <summary>每个设备的连续失败计数</summary>
|
||||||
|
private readonly ConcurrentDictionary<string, int> _failureCounts = new();
|
||||||
|
|
||||||
|
/// <summary>每个设备的重连尝试次数(用于计算退避阈值)</summary>
|
||||||
|
private readonly ConcurrentDictionary<string, int> _reconnectAttempts = new();
|
||||||
|
|
||||||
|
/// <summary>基础失败阈值(首次重连触发值)</summary>
|
||||||
|
private const int BaseFailureThreshold = 3;
|
||||||
|
|
||||||
|
/// <summary>退避上限(最大阈值)</summary>
|
||||||
|
private const int MaxBackoffThreshold = 15;
|
||||||
|
|
||||||
|
/// <summary>健康检查间隔(毫秒)</summary>
|
||||||
|
private readonly int _checkIntervalMs;
|
||||||
|
|
||||||
|
private bool _disposed;
|
||||||
|
private readonly object _startStopLock = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建健康监控器实例。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceMap">当前作用域的设备字典</param>
|
||||||
|
/// <param name="systemConfig">当前作用域的系统配置(用于更新 DeviceInfoVM.IsConnected)</param>
|
||||||
|
/// <param name="scopeName">作用域名称(日志标识)</param>
|
||||||
|
/// <param name="checkIntervalMs">健康检查间隔,默认 5000ms</param>
|
||||||
|
public DeviceHealthMonitor(
|
||||||
|
IDictionary<string, IBaseInterface> deviceMap,
|
||||||
|
SystemConfig systemConfig,
|
||||||
|
string scopeName,
|
||||||
|
int checkIntervalMs = 5000)
|
||||||
|
{
|
||||||
|
_deviceMap = deviceMap;
|
||||||
|
_systemConfig = systemConfig;
|
||||||
|
_scopeName = scopeName;
|
||||||
|
_checkIntervalMs = checkIntervalMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>启动健康监控(幂等:多次调用只启动一次)</summary>
|
||||||
|
public void Start()
|
||||||
|
{
|
||||||
|
lock (_startStopLock)
|
||||||
|
{
|
||||||
|
if (_disposed || _healthCheckTimer != null) return;
|
||||||
|
_healthCheckTimer = new Timer(
|
||||||
|
OnHealthCheckTick,
|
||||||
|
null,
|
||||||
|
TimeSpan.FromSeconds(10), // 首次检查延迟 10 秒,避免启动时设备尚未连接完成
|
||||||
|
TimeSpan.FromMilliseconds(_checkIntervalMs));
|
||||||
|
LoggerHelper.Info($"[{_scopeName}] 设备健康监控已启动,检查间隔={_checkIntervalMs}ms,基础重连阈值={BaseFailureThreshold}次(指数退避上限={MaxBackoffThreshold})");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>停止健康监控</summary>
|
||||||
|
public void Stop()
|
||||||
|
{
|
||||||
|
lock (_startStopLock)
|
||||||
|
{
|
||||||
|
_healthCheckTimer?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||||
|
_healthCheckTimer?.Dispose();
|
||||||
|
_healthCheckTimer = null;
|
||||||
|
_failureCounts.Clear();
|
||||||
|
_reconnectAttempts.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 健康检查核心逻辑:遍历设备,检查连接状态,失败计数超阈值则重连。
|
||||||
|
/// <para>TCP 设备额外进行主动探活(*IDN?),以检测死连接(对端崩溃但 TCP 未收到 FIN)。</para>
|
||||||
|
/// </summary>
|
||||||
|
private async void OnHealthCheckTick(object? state)
|
||||||
|
{
|
||||||
|
if (_disposed || _deviceMap.Count == 0) return;
|
||||||
|
|
||||||
|
// 快照避免枚举期间字典被修改
|
||||||
|
var snapshot = _deviceMap.ToArray();
|
||||||
|
|
||||||
|
foreach (var kvp in snapshot)
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
|
||||||
|
string deviceName = kvp.Key;
|
||||||
|
var device = kvp.Value;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
bool alive = device.IsConnected;
|
||||||
|
|
||||||
|
// TCP 设备主动探活:IsConnected 只反映上次操作状态,无法检测死连接
|
||||||
|
if (alive && device is Tcp tcpDevice)
|
||||||
|
{
|
||||||
|
alive = await ProbeTcpDeviceAsync(tcpDevice);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (alive)
|
||||||
|
{
|
||||||
|
// 连接正常:清零失败计数与退避
|
||||||
|
_failureCounts.TryRemove(deviceName, out _);
|
||||||
|
_reconnectAttempts.TryRemove(deviceName, out _);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连接断开:累加失败计数
|
||||||
|
int failures = _failureCounts.AddOrUpdate(deviceName, 1, (_, count) => count + 1);
|
||||||
|
|
||||||
|
// 计算当前退避阈值:基础值 + 重连尝试次数 × 2,上限为 MaxBackoffThreshold
|
||||||
|
int attempts = _reconnectAttempts.GetOrAdd(deviceName, 0);
|
||||||
|
int currentThreshold = Math.Min(BaseFailureThreshold + attempts * 2, MaxBackoffThreshold);
|
||||||
|
|
||||||
|
if (failures < currentThreshold)
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn(
|
||||||
|
$"[{_scopeName}] 设备 [{deviceName}] 连接断开,等待重连中 ({failures}/{currentThreshold})");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 达到阈值:执行重连
|
||||||
|
await ReconnectDeviceAsync(deviceName, device);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[{_scopeName}] 设备 [{deviceName}] 健康检查异常:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 对 TCP 设备执行轻量级主动探活(SCPI *IDN?),2 秒超时。
|
||||||
|
/// <para>通过 WriteReadAsync 内部获取 _commLock,与监控采样天然串行化。</para>
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>true: 探活成功(连接确实存活); false: 探活失败(死连接)</returns>
|
||||||
|
private async Task<bool> ProbeTcpDeviceAsync(Tcp tcpDevice)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var probeCts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
|
||||||
|
string resp = await tcpDevice.WriteReadAsync("*IDN?\n", "\n", probeCts.Token);
|
||||||
|
return !string.IsNullOrWhiteSpace(resp);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重连单个设备。
|
||||||
|
/// <para>
|
||||||
|
/// 冲突避免机制:ConnectAsync 内部获取设备的 _commLock,
|
||||||
|
/// 如果此时监控采样正在通信,重连会等待锁释放后再执行,
|
||||||
|
/// 保证同一时刻只有一个操作在使用通信链路。
|
||||||
|
/// </para>
|
||||||
|
/// </summary>
|
||||||
|
private async Task ReconnectDeviceAsync(string deviceName, IBaseInterface device)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
int attempts = _reconnectAttempts.AddOrUpdate(deviceName, 1, (_, c) => c + 1);
|
||||||
|
int nextThreshold = Math.Min(BaseFailureThreshold + attempts * 2, MaxBackoffThreshold);
|
||||||
|
LoggerHelper.Info($"[{_scopeName}] 设备 [{deviceName}] 连续失败触发重连(第 {attempts} 次重连,下次阈值={nextThreshold})...");
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(10));
|
||||||
|
bool ok = await device.ConnectAsync(cts.Token);
|
||||||
|
|
||||||
|
// 同步更新 DeviceInfoVM 的 UI 状态
|
||||||
|
UpdateDeviceInfoState(deviceName, ok);
|
||||||
|
|
||||||
|
if (ok)
|
||||||
|
{
|
||||||
|
_failureCounts.TryRemove(deviceName, out _);
|
||||||
|
_reconnectAttempts.TryRemove(deviceName, out _);
|
||||||
|
LoggerHelper.Info($"[{_scopeName}] 设备 [{deviceName}] 重连成功");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
LoggerHelper.Warn($"[{_scopeName}] 设备 [{deviceName}] 重连失败,将在下次检查时重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
UpdateDeviceInfoState(deviceName, false);
|
||||||
|
LoggerHelper.Warn($"[{_scopeName}] 设备 [{deviceName}] 重连超时(10s),将在下次检查时重试");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
UpdateDeviceInfoState(deviceName, false);
|
||||||
|
LoggerHelper.Error($"[{_scopeName}] 设备 [{deviceName}] 重连异常:{ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 通过 UI 线程更新 SystemConfig.DeviceList 中对应设备的 IsConnected 状态(驱动 UI 刷新)。
|
||||||
|
/// <para>Timer 回调运行在线程池线程上,必须切回 UI 线程才能触发 PropertyChanged。</para>
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateDeviceInfoState(string deviceName, bool isConnected)
|
||||||
|
{
|
||||||
|
var info = _systemConfig?.DeviceList?
|
||||||
|
.FirstOrDefault(d => d != null &&
|
||||||
|
string.Equals(d.DeviceName, deviceName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (info == null) return;
|
||||||
|
|
||||||
|
var dispatcher = Application.Current?.Dispatcher;
|
||||||
|
if (dispatcher == null || dispatcher.CheckAccess())
|
||||||
|
{
|
||||||
|
// 已在 UI 线程(或无 Dispatcher),直接赋值
|
||||||
|
info.IsConnected = isConnected;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// 切回 UI 线程赋值,避免跨线程 PropertyChanged 异常
|
||||||
|
dispatcher.BeginInvoke(() => info.IsConnected = isConnected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
if (_disposed) return;
|
||||||
|
_disposed = true;
|
||||||
|
Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,9 @@ namespace UIShare.GlobalVariable
|
|||||||
private readonly string _scopeName;
|
private readonly string _scopeName;
|
||||||
private readonly IEventAggregator _eventAggregator;
|
private readonly IEventAggregator _eventAggregator;
|
||||||
|
|
||||||
|
/// <summary>设备健康监控器:独立心跳检测 + 自动重连,与监控采样互不冲突</summary>
|
||||||
|
private DeviceHealthMonitor? _healthMonitor;
|
||||||
|
|
||||||
/// <summary>按 DeviceName 索引的设备字典,便于业务层按名取实例。</summary>
|
/// <summary>按 DeviceName 索引的设备字典,便于业务层按名取实例。</summary>
|
||||||
public IDictionary<string, IBaseInterface> DeviceMap { get; private set; }
|
public IDictionary<string, IBaseInterface> DeviceMap { get; private set; }
|
||||||
= new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
= new Dictionary<string, IBaseInterface>(StringComparer.OrdinalIgnoreCase);
|
||||||
@@ -252,6 +255,9 @@ namespace UIShare.GlobalVariable
|
|||||||
}
|
}
|
||||||
|
|
||||||
await Task.WhenAll(tasks);
|
await Task.WhenAll(tasks);
|
||||||
|
|
||||||
|
// 所有设备连接完成后,启动健康监控(心跳重连)
|
||||||
|
StartHealthMonitor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -320,6 +326,7 @@ namespace UIShare.GlobalVariable
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task CloseAllDevicesAsync()
|
public async Task CloseAllDevicesAsync()
|
||||||
{
|
{
|
||||||
|
StopHealthMonitor();
|
||||||
List<Task> tasks = new List<Task>();
|
List<Task> tasks = new List<Task>();
|
||||||
|
|
||||||
lock (_lockObj)
|
lock (_lockObj)
|
||||||
@@ -617,8 +624,25 @@ namespace UIShare.GlobalVariable
|
|||||||
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
return Activator.CreateInstance(type, cfg) as IBaseInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>启动设备健康监控(心跳检测 + 自动重连)</summary>
|
||||||
|
private void StartHealthMonitor()
|
||||||
|
{
|
||||||
|
StopHealthMonitor();
|
||||||
|
_healthMonitor = new DeviceHealthMonitor(DeviceMap, _systemConfig, _scopeName);
|
||||||
|
_healthMonitor.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>停止设备健康监控</summary>
|
||||||
|
private void StopHealthMonitor()
|
||||||
|
{
|
||||||
|
_healthMonitor?.Stop();
|
||||||
|
_healthMonitor?.Dispose();
|
||||||
|
_healthMonitor = null;
|
||||||
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
|
StopHealthMonitor();
|
||||||
if (string.IsNullOrEmpty(_scopeName)) return;
|
if (string.IsNullOrEmpty(_scopeName)) return;
|
||||||
|
|
||||||
// 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用
|
// 遍历当前作用域用到的所有指纹,逐一移除本作用域的引用
|
||||||
|
|||||||
@@ -12,10 +12,14 @@ namespace UIShare.GlobalVariable
|
|||||||
public class GlobalInfo:BindableBase
|
public class GlobalInfo:BindableBase
|
||||||
{
|
{
|
||||||
public event EventHandler? ScopeChanged;
|
public event EventHandler? ScopeChanged;
|
||||||
public Dictionary<string,ScopedContext> ContextDic { get; set; }
|
/// <summary>作用域名 → 作用域上下文(线程安全:可能被多个作用域并发创建/销毁)</summary>
|
||||||
public Dictionary<string,StepRunning> StepRunningDic { get; set; }
|
public ConcurrentDictionary<string, ScopedContext> ContextDic { get; set; }
|
||||||
public Dictionary<string, SystemConfig> ConfigDic { get; set; }
|
/// <summary>作用域名 → 测试运行器(线程安全:可能被多个作用域并发创建/销毁)</summary>
|
||||||
public Dictionary<string, IScopedProvider> ScopeDic { get; set; }
|
public ConcurrentDictionary<string, StepRunning> StepRunningDic { get; set; }
|
||||||
|
/// <summary>作用域名 → 系统配置(线程安全:可能被多个作用域并发创建/销毁)</summary>
|
||||||
|
public ConcurrentDictionary<string, SystemConfig> ConfigDic { get; set; }
|
||||||
|
/// <summary>作用域名 → 作用域容器(线程安全:可能被多个作用域并发创建/销毁)</summary>
|
||||||
|
public ConcurrentDictionary<string, IScopedProvider> ScopeDic { get; set; }
|
||||||
|
|
||||||
/// <summary>硬件指纹 → 设备实例的并发池,确保同一物理硬件全局只创建一个驱动实例。</summary>
|
/// <summary>硬件指纹 → 设备实例的并发池,确保同一物理硬件全局只创建一个驱动实例。</summary>
|
||||||
public ConcurrentDictionary<string, Lazy<IBaseInterface>> HardwarePool { get; set; }
|
public ConcurrentDictionary<string, Lazy<IBaseInterface>> HardwarePool { get; set; }
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
using Common.Attributes;
|
using Common.Attributes;
|
||||||
using DeviceCommand.Base;
|
using DeviceCommand.Base;
|
||||||
|
using Logger;
|
||||||
using Model.Models;
|
using Model.Models;
|
||||||
using Prism.Events;
|
using Prism.Events;
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
@@ -41,6 +43,18 @@ namespace UIShare.GlobalVariable
|
|||||||
private CancellationTokenSource? _cts;
|
private CancellationTokenSource? _cts;
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
|
|
||||||
|
/// <summary>是否已完成过至少一次 Discover(幂等标记,避免多作用域重复调用时 Clear + 重扫)</summary>
|
||||||
|
private bool _discovered;
|
||||||
|
|
||||||
|
/// <summary>连续失败次数达到此阈值后,暂停该通道采样并发布报警</summary>
|
||||||
|
private const int FailureThreshold = 5;
|
||||||
|
|
||||||
|
/// <summary>每通道连续失败计数(key = fingerprint + "|" + methodName)</summary>
|
||||||
|
private readonly ConcurrentDictionary<string, int> _failureCounts = new();
|
||||||
|
|
||||||
|
/// <summary>已因连续失败而被暂停的通道(key 同上)</summary>
|
||||||
|
private readonly ConcurrentDictionary<string, bool> _suspendedChannels = new();
|
||||||
|
|
||||||
/// <summary>采样间隔(默认 1000ms)</summary>
|
/// <summary>采样间隔(默认 1000ms)</summary>
|
||||||
public TimeSpan SampleInterval
|
public TimeSpan SampleInterval
|
||||||
{
|
{
|
||||||
@@ -89,9 +103,12 @@ namespace UIShare.GlobalVariable
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
|
/// 扫描 GlobalInfo.HardwarePool 中所有已创建的物理设备,
|
||||||
/// 反查可监测方法并编译为委托。每个指纹只注册一次,反射只执行一次。
|
/// 反查可监测方法并编译为委托。每个指纹只注册一次,反射只执行一次。
|
||||||
|
/// <para>幂等:首次调用后再次调用不会 Clear 重扫,避免多作用域重复 Discover 导致短暂采样中断。</para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Discover()
|
public void Discover()
|
||||||
{
|
{
|
||||||
|
if (_discovered) return;
|
||||||
|
_discovered = true;
|
||||||
_registeredMethods.Clear();
|
_registeredMethods.Clear();
|
||||||
|
|
||||||
foreach (var poolEntry in _globalInfo.HardwarePool)
|
foreach (var poolEntry in _globalInfo.HardwarePool)
|
||||||
@@ -157,6 +174,11 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
foreach (var entry in _registeredMethods)
|
foreach (var entry in _registeredMethods)
|
||||||
{
|
{
|
||||||
|
string channelKey = entry.Fingerprint + "|" + entry.MethodName;
|
||||||
|
|
||||||
|
// 已暂停的通道跳过采样
|
||||||
|
if (_suspendedChannels.ContainsKey(channelKey)) continue;
|
||||||
|
|
||||||
// fire-and-forget:每个通道独立采样,完成后自行广播
|
// fire-and-forget:每个通道独立采样,完成后自行广播
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
@@ -167,6 +189,9 @@ namespace UIShare.GlobalVariable
|
|||||||
|
|
||||||
if (!double.TryParse(raw, out double value)) return;
|
if (!double.TryParse(raw, out double value)) return;
|
||||||
|
|
||||||
|
// 采样成功,重置失败计数
|
||||||
|
_failureCounts.TryRemove(channelKey, out _);
|
||||||
|
|
||||||
// 向所有引用该物理设备的作用域分别广播
|
// 向所有引用该物理设备的作用域分别广播
|
||||||
var scopes = GetScopesForFingerprint(entry.Fingerprint);
|
var scopes = GetScopesForFingerprint(entry.Fingerprint);
|
||||||
foreach (var scope in scopes)
|
foreach (var scope in scopes)
|
||||||
@@ -188,9 +213,24 @@ namespace UIShare.GlobalVariable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
// 单个通道故障不干扰其他通道
|
int count = _failureCounts.AddOrUpdate(channelKey, 1, (_, c) => c + 1);
|
||||||
|
LoggerHelper.Warn($"采样通道 [{entry.Fingerprint}/{entry.MethodName}] 第 {count} 次失败: {ex.Message}");
|
||||||
|
|
||||||
|
if (count >= FailureThreshold)
|
||||||
|
{
|
||||||
|
_suspendedChannels.TryAdd(channelKey, true);
|
||||||
|
LoggerHelper.Error($"采样通道 [{entry.Fingerprint}/{entry.MethodName}] 连续失败 {count} 次,已暂停采样");
|
||||||
|
|
||||||
|
// 向所有引用该设备的作用域发布报警
|
||||||
|
var scopes = GetScopesForFingerprint(entry.Fingerprint);
|
||||||
|
foreach (var scope in scopes)
|
||||||
|
{
|
||||||
|
_eventAggregator.GetEvent<AlarmEvent>().Publish(
|
||||||
|
(scope, entry.Fingerprint, $"通道 {entry.MethodName} 连续失败 {count} 次,已暂停"));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, token);
|
}, token);
|
||||||
}
|
}
|
||||||
@@ -209,6 +249,8 @@ namespace UIShare.GlobalVariable
|
|||||||
_cts?.Dispose();
|
_cts?.Dispose();
|
||||||
_cts = null;
|
_cts = null;
|
||||||
_registeredMethods.Clear();
|
_registeredMethods.Clear();
|
||||||
|
_failureCounts.Clear();
|
||||||
|
_suspendedChannels.Clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
using Prism.Mvvm;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using UIShare.UIViewModel;
|
||||||
|
|
||||||
|
namespace UIShare.GlobalVariable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 单个 Tab(主程序 / 错误程序)的子程序导航状态。
|
||||||
|
/// 主程序和错误程序各自持有一个独立实例,互不干扰。
|
||||||
|
/// </summary>
|
||||||
|
public class ProgramNavigationState : BindableBase
|
||||||
|
{
|
||||||
|
/// <summary>是否为错误程序 Tab</summary>
|
||||||
|
public bool IsErrorTab { get; set; }
|
||||||
|
|
||||||
|
public Stack<ProgramVM> NavigationStack { get; } = new();
|
||||||
|
|
||||||
|
private ProgramVM _currentProgram;
|
||||||
|
public ProgramVM CurrentProgram
|
||||||
|
{
|
||||||
|
get => _currentProgram;
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (SetProperty(ref _currentProgram, value))
|
||||||
|
RaisePropertyChanged(nameof(DisplaySteps));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// DataGrid 实际绑定的步骤集合。
|
||||||
|
/// 根程序 + 错误 Tab → ErrorStepCollection;其他情况 → StepCollection。
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<StepVM> DisplaySteps
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (CurrentProgram == null)
|
||||||
|
return new ObservableCollection<StepVM>();
|
||||||
|
// 在根程序且是错误 Tab → 显示错误步骤集合
|
||||||
|
if (NavigationStack.Count == 0 && IsErrorTab)
|
||||||
|
return CurrentProgram.ErrorStepCollection;
|
||||||
|
// 其他情况(主 Tab 或已进入子程序)→ 显示正常步骤集合
|
||||||
|
return CurrentProgram.StepCollection;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private string _breadcrumbPath = "主程序";
|
||||||
|
public string BreadcrumbPath
|
||||||
|
{
|
||||||
|
get => _breadcrumbPath;
|
||||||
|
set => SetProperty(ref _breadcrumbPath, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool _canGoBack;
|
||||||
|
public bool CanGoBack
|
||||||
|
{
|
||||||
|
get => _canGoBack;
|
||||||
|
set => SetProperty(ref _canGoBack, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 用根程序初始化导航状态
|
||||||
|
/// </summary>
|
||||||
|
public void Initialize(ProgramVM rootProgram, string label)
|
||||||
|
{
|
||||||
|
NavigationStack.Clear();
|
||||||
|
CurrentProgram = rootProgram;
|
||||||
|
BreadcrumbPath = label;
|
||||||
|
CanGoBack = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 清空导航栈,回到根程序
|
||||||
|
/// </summary>
|
||||||
|
public void Reset(ProgramVM rootProgram, string label)
|
||||||
|
{
|
||||||
|
NavigationStack.Clear();
|
||||||
|
CurrentProgram = rootProgram;
|
||||||
|
BreadcrumbPath = label;
|
||||||
|
CanGoBack = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据栈状态刷新 CanGoBack
|
||||||
|
/// </summary>
|
||||||
|
public void UpdateCanGoBack() => CanGoBack = NavigationStack.Count > 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
using DeviceCommand.Base;
|
using DeviceCommand.Base;
|
||||||
using MaterialDesignThemes.Wpf;
|
using MaterialDesignThemes.Wpf;
|
||||||
|
using Prism.Mvvm;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -14,9 +15,35 @@ using UIShare.UIViewModel;
|
|||||||
|
|
||||||
namespace UIShare.GlobalVariable
|
namespace UIShare.GlobalVariable
|
||||||
{
|
{
|
||||||
public class ScopedContext
|
public class ScopedContext : BindableBase
|
||||||
{
|
{
|
||||||
private static readonly Random _randomSeed = new Random();
|
private static readonly Random _randomSeed = new Random();
|
||||||
|
|
||||||
|
#region 子程序导航(主程序 / 错误程序各自独立)
|
||||||
|
|
||||||
|
/// <summary>主程序 Tab 的导航状态</summary>
|
||||||
|
public ProgramNavigationState MainNav { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>错误程序 Tab 的导航状态</summary>
|
||||||
|
public ProgramNavigationState ErrorNav { get; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 根据当前选中的 Tab 返回对应的导航状态
|
||||||
|
/// </summary>
|
||||||
|
public ProgramNavigationState ActiveNav =>
|
||||||
|
SelectedStepList == "错误程序" ? ErrorNav : MainNav;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 重置两个 Tab 的导航栈,回到根程序
|
||||||
|
/// </summary>
|
||||||
|
public void ResetNavigation()
|
||||||
|
{
|
||||||
|
MainNav.Reset(Program, "主程序");
|
||||||
|
ErrorNav.Reset(Program, "错误程序");
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
public ProgramVM Program { get; set; } = new();
|
public ProgramVM Program { get; set; } = new();
|
||||||
public String SelectedStepList { get; set; } = "主程序";
|
public String SelectedStepList { get; set; } = "主程序";
|
||||||
public string CurrentFilePath { get; set; }
|
public string CurrentFilePath { get; set; }
|
||||||
@@ -45,6 +72,7 @@ namespace UIShare.GlobalVariable
|
|||||||
public int DebugRandomId { get; private set; }
|
public int DebugRandomId { get; private set; }
|
||||||
public ScopedContext()
|
public ScopedContext()
|
||||||
{
|
{
|
||||||
|
ErrorNav.IsErrorTab = true;
|
||||||
lock (_randomSeed)
|
lock (_randomSeed)
|
||||||
{
|
{
|
||||||
// 每次诞生一个新上下文,就在 10000 到 99999 之间随机摇一个数
|
// 每次诞生一个新上下文,就在 10000 到 99999 之间随机摇一个数
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ using System.Diagnostics;
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using UIShare.GlobalVariable;
|
using UIShare.GlobalVariable;
|
||||||
using static UIShare.UIViewModel.ParameterVM;
|
using static UIShare.UIViewModel.ParameterVM;
|
||||||
@@ -29,6 +30,7 @@ namespace UIShare
|
|||||||
private IContainerProvider containerProvider;
|
private IContainerProvider containerProvider;
|
||||||
private IEventAggregator _eventAggregator;
|
private IEventAggregator _eventAggregator;
|
||||||
private ITestReportService _testReportService;
|
private ITestReportService _testReportService;
|
||||||
|
private ITestCheckRecordService _testCheckRecordService;
|
||||||
|
|
||||||
private readonly Dictionary<Guid, ParameterVM> tmpParameters = [];
|
private readonly Dictionary<Guid, ParameterVM> tmpParameters = [];
|
||||||
|
|
||||||
@@ -38,6 +40,9 @@ namespace UIShare
|
|||||||
|
|
||||||
private readonly Stack<LoopContext> loopStack = new();
|
private readonly Stack<LoopContext> loopStack = new();
|
||||||
|
|
||||||
|
/// <summary>测试项上下文栈:进入 IsTestItem 子程序时压栈,栈非空期间每次 OKExpression 判断都归属栈顶测试项</summary>
|
||||||
|
private readonly Stack<TestItemContext> testItemStack = new();
|
||||||
|
|
||||||
public CancellationTokenSource stepCTS = new();
|
public CancellationTokenSource stepCTS = new();
|
||||||
public CancellationTokenSource errorStepCTS = new();
|
public CancellationTokenSource errorStepCTS = new();
|
||||||
private bool SubSingleStep = false;
|
private bool SubSingleStep = false;
|
||||||
@@ -46,13 +51,14 @@ namespace UIShare
|
|||||||
private volatile bool _disposed = false;
|
private volatile bool _disposed = false;
|
||||||
|
|
||||||
public Guid TestRoundID;
|
public Guid TestRoundID;
|
||||||
public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService)
|
public StepRunning(ScopedContext ScopedContext, SystemConfig systemConfig,IEventAggregator eventAggregator, DeviceManager deviceManager, ITestReportService testReportService, ITestCheckRecordService testCheckRecordService)
|
||||||
{
|
{
|
||||||
_scopedContext = ScopedContext;
|
_scopedContext = ScopedContext;
|
||||||
_systemConfig = systemConfig;
|
_systemConfig = systemConfig;
|
||||||
_eventAggregator = eventAggregator;
|
_eventAggregator = eventAggregator;
|
||||||
_deviceManager= deviceManager;
|
_deviceManager= deviceManager;
|
||||||
_testReportService = testReportService;
|
_testReportService = testReportService;
|
||||||
|
_testCheckRecordService = testCheckRecordService;
|
||||||
//_devices = containerProvider.Resolve<Devices>();
|
//_devices = containerProvider.Resolve<Devices>();
|
||||||
}
|
}
|
||||||
public async Task<bool> ExecuteErrorSteps(ProgramVM program, int depth = 0, CancellationToken cancellationToken = default)
|
public async Task<bool> ExecuteErrorSteps(ProgramVM program, int depth = 0, CancellationToken cancellationToken = default)
|
||||||
@@ -181,7 +187,23 @@ namespace UIShare
|
|||||||
_scopedContext.SingleStep = false;
|
_scopedContext.SingleStep = false;
|
||||||
}
|
}
|
||||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
||||||
|
// 发布进入子程序导航事件
|
||||||
|
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||||
|
{
|
||||||
|
Scope = _systemConfig.Title,
|
||||||
|
Action = NavigateAction.Enter,
|
||||||
|
SubProgram = step.SubProgram,
|
||||||
|
StepName = step.Name,
|
||||||
|
IsErrorProgram = true
|
||||||
|
});
|
||||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||||
|
// 发布退出子程序导航事件
|
||||||
|
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||||
|
{
|
||||||
|
Scope = _systemConfig.Title,
|
||||||
|
Action = NavigateAction.Exit,
|
||||||
|
IsErrorProgram = true
|
||||||
|
});
|
||||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||||
if (SubSingleStep)
|
if (SubSingleStep)
|
||||||
{
|
{
|
||||||
@@ -237,8 +259,10 @@ namespace UIShare
|
|||||||
loopStopwatchStack.Clear();
|
loopStopwatchStack.Clear();
|
||||||
ResetAllStepStatus(program.StepCollection);
|
ResetAllStepStatus(program.StepCollection);
|
||||||
tmpParameters.Clear();
|
tmpParameters.Clear();
|
||||||
|
testItemStack.Clear();
|
||||||
TestRoundID = Guid.NewGuid();
|
TestRoundID = Guid.NewGuid();
|
||||||
}
|
}
|
||||||
|
int initialLoopStackCount = loopStack.Count;
|
||||||
foreach (var item in program.Parameters)
|
foreach (var item in program.Parameters)
|
||||||
{
|
{
|
||||||
tmpParameters.TryAdd(item.ID, item);
|
tmpParameters.TryAdd(item.ID, item);
|
||||||
@@ -356,8 +380,32 @@ namespace UIShare
|
|||||||
_scopedContext.SingleStep = false;
|
_scopedContext.SingleStep = false;
|
||||||
}
|
}
|
||||||
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
LoggerHelper.InfoWithNotify(_systemConfig.Title, $"开始执行子程序 [ {step.Index} ] [ {step.Name} ] ", depth);
|
||||||
|
// 发布进入子程序导航事件
|
||||||
|
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||||
|
{
|
||||||
|
Scope = _systemConfig.Title,
|
||||||
|
Action = NavigateAction.Enter,
|
||||||
|
SubProgram = step.SubProgram,
|
||||||
|
StepName = step.Name
|
||||||
|
});
|
||||||
|
bool isTestItemStep = step.IsTestItem;
|
||||||
|
if (isTestItemStep)
|
||||||
|
{
|
||||||
|
testItemStack.Push(new TestItemContext { Name = step.Name ?? "未命名测试项" });
|
||||||
|
}
|
||||||
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
stepSuccess = await ExecuteSteps(step.SubProgram, depth + 1, cancellationToken);
|
||||||
|
// 先评估本步骤自身(含自身 OKExpression 判断,归属本测试项),再写测试项汇总并弹栈
|
||||||
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
UpdateCurrentStepResult(step, true, stepSuccess, depth);
|
||||||
|
if (isTestItemStep)
|
||||||
|
{
|
||||||
|
await FinalizeTestItemAsync(depth);
|
||||||
|
}
|
||||||
|
// 发布退出子程序导航事件
|
||||||
|
_eventAggregator.GetEvent<SubProgramNavigateEvent>().Publish(new SubProgramNavigatePayload
|
||||||
|
{
|
||||||
|
Scope = _systemConfig.Title,
|
||||||
|
Action = NavigateAction.Exit
|
||||||
|
});
|
||||||
if (SubSingleStep)
|
if (SubSingleStep)
|
||||||
{
|
{
|
||||||
SubSingleStep = false;
|
SubSingleStep = false;
|
||||||
@@ -405,7 +453,12 @@ namespace UIShare
|
|||||||
await SaveStepRecordAsync(step, depth, false);
|
await SaveStepRecordAsync(step, depth, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bool finalResult = loopStack.Count == initialLoopStackCount && stepSuccess;
|
||||||
|
|
||||||
|
if (depth > 0) // 子程序
|
||||||
|
{
|
||||||
|
return finalResult;
|
||||||
|
}
|
||||||
return loopStack.Count == 0 && stepSuccess;
|
return loopStack.Count == 0 && stepSuccess;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -776,12 +829,46 @@ namespace UIShare
|
|||||||
paraDic.TryAdd(item.Name, item.Value!);
|
paraDic.TryAdd(item.Name, item.Value!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
bool re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic);
|
bool re;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
re = ExpressionEvaluator.EvaluateExpression(step.OKExpression, paraDic);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// 表达式执行错误也视为 NG,并记录到测试项判断明细
|
||||||
|
LoggerHelper.ErrorWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] OKExpression 执行异常: {ex.Message}", depth: depth);
|
||||||
|
re = false;
|
||||||
|
}
|
||||||
step.Result = re ? 1 : 2;
|
step.Result = re ? 1 : 2;
|
||||||
if (step.Result == 2)
|
if (step.Result == 2)
|
||||||
{
|
{
|
||||||
LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] NG:条件表达式验证失败", depth: depth);
|
LoggerHelper.WarnWithNotify(_systemConfig.Title, $"指令 [ {step.Index} ] NG:条件表达式验证失败", depth: depth);
|
||||||
}
|
}
|
||||||
|
// 测试项范围内:记录本次判断(重复判断逐次记录)
|
||||||
|
if (testItemStack.Count > 0)
|
||||||
|
{
|
||||||
|
var ctx = testItemStack.Peek();
|
||||||
|
bool isSelfCheck = ctx.Name == (step.Name ?? "未命名测试项") && step.SubProgram != null;
|
||||||
|
if (!re) ctx.HasFailure = true;
|
||||||
|
if (!isSelfCheck)
|
||||||
|
{
|
||||||
|
SaveCheckRecordAsync(new TestCheckRecordEntity
|
||||||
|
{
|
||||||
|
TestRoundId = TestRoundID,
|
||||||
|
Scope = _systemConfig.Title,
|
||||||
|
FileName = _systemConfig.CurrentADPFile ?? "",
|
||||||
|
TestItemName = ctx.Name,
|
||||||
|
StepName = step.Name ?? "",
|
||||||
|
Depth = depth,
|
||||||
|
OKExpression = step.OKExpression,
|
||||||
|
Pass = re,
|
||||||
|
Values = ExtractExpressionValues(step.OKExpression, paraDic),
|
||||||
|
IsSummary = false,
|
||||||
|
CreateTime = DateTime.Now
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -792,6 +879,90 @@ namespace UIShare
|
|||||||
}
|
}
|
||||||
step.Result = 2;
|
step.Result = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 测试项范围内的步骤执行错误/表达式 NG 均计入当前测试项汇总(自身步骤的错误已在压栈期间计入)
|
||||||
|
if (step.Result == 2 && testItemStack.Count > 0)
|
||||||
|
{
|
||||||
|
testItemStack.Peek().HasFailure = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 测试项执行结束:写入汇总记录(PASS/NG)并弹栈。
|
||||||
|
/// 测试项内无任何判断时,结果由范围内步骤执行成败决定。
|
||||||
|
/// </summary>
|
||||||
|
private async Task FinalizeTestItemAsync(int depth)
|
||||||
|
{
|
||||||
|
if (testItemStack.Count == 0) return;
|
||||||
|
var ctx = testItemStack.Pop();
|
||||||
|
SaveCheckRecordAsync(new TestCheckRecordEntity
|
||||||
|
{
|
||||||
|
TestRoundId = TestRoundID,
|
||||||
|
Scope = _systemConfig.Title,
|
||||||
|
FileName = _systemConfig.CurrentADPFile ?? "",
|
||||||
|
TestItemName = ctx.Name,
|
||||||
|
StepName = ctx.Name,
|
||||||
|
Depth = depth,
|
||||||
|
OKExpression = null,
|
||||||
|
Pass = !ctx.HasFailure,
|
||||||
|
Values = null,
|
||||||
|
IsSummary = true,
|
||||||
|
CreateTime = DateTime.Now
|
||||||
|
});
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 保存测试项判断记录(不阻塞执行主流程)
|
||||||
|
/// </summary>
|
||||||
|
private void SaveCheckRecordAsync(TestCheckRecordEntity entity)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _testCheckRecordService.InsertAsync(entity);
|
||||||
|
if (!result.IsSuccess)
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"保存测试项判断记录失败 [{entity.TestItemName}]: {result.Msg}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"保存测试项判断记录失败 [{entity.TestItemName}]: {ex.Message}");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 从表达式中提取实际出现的变量并取其当前值,拼接为 "变量=值; " 格式(长名优先,避免子串误匹配)
|
||||||
|
/// </summary>
|
||||||
|
private static string? ExtractExpressionValues(string expression, Dictionary<string, object> paraDic)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
var matchedSpans = new List<(int Start, int End)>();
|
||||||
|
foreach (var name in paraDic.Keys.OrderByDescending(k => k.Length))
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(name)) continue;
|
||||||
|
foreach (Match m in Regex.Matches(expression, $@"\b{Regex.Escape(name)}\b"))
|
||||||
|
{
|
||||||
|
bool overlaps = matchedSpans.Any(s => m.Index < s.End && m.Index + m.Length > s.Start);
|
||||||
|
if (!overlaps)
|
||||||
|
{
|
||||||
|
matchedSpans.Add((m.Index, m.Index + m.Length));
|
||||||
|
sb.Append($"{name}={paraDic[name]}; ");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.Length > 0 ? sb.ToString() : null;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
@@ -825,6 +996,7 @@ namespace UIShare
|
|||||||
tmpParameters.Clear();
|
tmpParameters.Clear();
|
||||||
loopStack.Clear();
|
loopStack.Clear();
|
||||||
loopStopwatchStack.Clear();
|
loopStopwatchStack.Clear();
|
||||||
|
testItemStack.Clear();
|
||||||
stepStopwatch.Stop();
|
stepStopwatch.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -840,6 +1012,13 @@ namespace UIShare
|
|||||||
public StepVM? LoopStartStep { get; set; }
|
public StepVM? LoopStartStep { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>测试项执行上下文:记录测试项名称与范围内是否出现过失败</summary>
|
||||||
|
private class TestItemContext
|
||||||
|
{
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public bool HasFailure { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,28 +51,20 @@ namespace UIShare.GlobalVariable
|
|||||||
Name = "台架序号",
|
Name = "台架序号",
|
||||||
Value = 1,
|
Value = 1,
|
||||||
IsEditable=false
|
IsEditable=false
|
||||||
},
|
|
||||||
new ParameterVM
|
|
||||||
{
|
|
||||||
Category = ParameterCategory.Input,
|
|
||||||
Type = typeof(int),
|
|
||||||
Name = "直流负载通道",
|
|
||||||
Value = 1,
|
|
||||||
IsEditable=false
|
|
||||||
},
|
|
||||||
new ParameterVM
|
|
||||||
{
|
|
||||||
Category = ParameterCategory.Input,
|
|
||||||
Type = typeof(int),
|
|
||||||
Name = "交流电源通道",
|
|
||||||
Value = 1,
|
|
||||||
IsEditable=false
|
|
||||||
},
|
},
|
||||||
new ParameterVM
|
new ParameterVM
|
||||||
{
|
{
|
||||||
Category = ParameterCategory.Input,
|
Category = ParameterCategory.Input,
|
||||||
Type = typeof(int),
|
Type = typeof(int),
|
||||||
Name = "CAN通道",
|
Name = "CAN通道1",
|
||||||
|
Value = 0,
|
||||||
|
IsEditable=false
|
||||||
|
},
|
||||||
|
new ParameterVM
|
||||||
|
{
|
||||||
|
Category = ParameterCategory.Input,
|
||||||
|
Type = typeof(int),
|
||||||
|
Name = "CAN通道2",
|
||||||
Value = 0,
|
Value = 0,
|
||||||
IsEditable=false
|
IsEditable=false
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,4 +9,22 @@ namespace UIShare.PubEvent
|
|||||||
public class OverlayEvent : PubSubEvent<bool>
|
public class OverlayEvent : PubSubEvent<bool>
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 作用域感知的灰度遮罩事件:仅台架名称与 Scope 匹配的台架视图显示/隐藏加载遮罩,
|
||||||
|
/// 避免全局 OverlayEvent 导致所有台架及主窗口同时变灰。
|
||||||
|
/// </summary>
|
||||||
|
public class ScopeOverlayEvent : PubSubEvent<ScopeOverlayArgs>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>作用域遮罩事件参数。</summary>
|
||||||
|
public class ScopeOverlayArgs
|
||||||
|
{
|
||||||
|
/// <summary>台架名称(与 SystemConfig.Title / TestStatus 一致)。</summary>
|
||||||
|
public string Scope { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>true 显示遮罩,false 隐藏遮罩。</summary>
|
||||||
|
public bool Show { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
using Prism.Events;
|
||||||
|
using UIShare.UIViewModel;
|
||||||
|
|
||||||
|
namespace UIShare.PubEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 子程序导航事件:运行时进入/退出子程序时发布,
|
||||||
|
/// StepsManagerViewModel 订阅后自动切换显示。
|
||||||
|
/// </summary>
|
||||||
|
public class SubProgramNavigateEvent : PubSubEvent<SubProgramNavigatePayload>
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SubProgramNavigatePayload
|
||||||
|
{
|
||||||
|
/// <summary>目标台架的作用域标识</summary>
|
||||||
|
public string Scope { get; set; } = "";
|
||||||
|
|
||||||
|
/// <summary>Enter = 进入子程序,Exit = 退出回到上一级</summary>
|
||||||
|
public NavigateAction Action { get; set; }
|
||||||
|
|
||||||
|
/// <summary>进入时:子程序的 ProgramVM;退出时可为 null</summary>
|
||||||
|
public ProgramVM? SubProgram { get; set; }
|
||||||
|
|
||||||
|
/// <summary>子程序步骤的名称(用于面包屑显示)</summary>
|
||||||
|
public string? StepName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>是否为错误程序 Tab 的导航(默认 false = 主程序 Tab)</summary>
|
||||||
|
public bool IsErrorProgram { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public enum NavigateAction
|
||||||
|
{
|
||||||
|
Enter,
|
||||||
|
Exit
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
private int _报文ID;
|
private int _报文ID;
|
||||||
private double _时间戳;
|
private double _时间戳;
|
||||||
private byte _长度;
|
private byte _长度;
|
||||||
|
private string _数据;
|
||||||
|
|
||||||
|
|
||||||
// 通道属性
|
// 通道属性
|
||||||
@@ -37,6 +38,13 @@
|
|||||||
set { SetProperty(ref _长度, value); }
|
set { SetProperty(ref _长度, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 数据属性(原始报文字节十六进制字符串,如 [0x00, 0x01, ...])
|
||||||
|
public string 数据
|
||||||
|
{
|
||||||
|
get { return _数据; }
|
||||||
|
set { SetProperty(ref _数据, value); }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,5 +32,12 @@ namespace UIShare.UIViewModel
|
|||||||
get => _tag;
|
get => _tag;
|
||||||
set => SetProperty(ref _tag, value);
|
set => SetProperty(ref _tag, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string? _tooltip;
|
||||||
|
public string? Tooltip
|
||||||
|
{
|
||||||
|
get => _tooltip;
|
||||||
|
set => SetProperty(ref _tooltip, value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -26,6 +26,7 @@ namespace UIShare.UIViewModel
|
|||||||
NGGotoStepID = source.NGGotoStepID;
|
NGGotoStepID = source.NGGotoStepID;
|
||||||
Description = source.Description;
|
Description = source.Description;
|
||||||
IsUsed = source.IsUsed;
|
IsUsed = source.IsUsed;
|
||||||
|
IsTestItem = source.IsTestItem;
|
||||||
|
|
||||||
if (source.Method != null)
|
if (source.Method != null)
|
||||||
{
|
{
|
||||||
@@ -55,6 +56,15 @@ namespace UIShare.UIViewModel
|
|||||||
set => SetProperty(ref _isUsed, value);
|
set => SetProperty(ref _isUsed, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool _isTestItem = false;
|
||||||
|
|
||||||
|
/// <summary>是否测试项(仅子程序步骤可标记,运行时记录其范围内所有 OKExpression 判断)</summary>
|
||||||
|
public bool IsTestItem
|
||||||
|
{
|
||||||
|
get => _isTestItem;
|
||||||
|
set => SetProperty(ref _isTestItem, value);
|
||||||
|
}
|
||||||
|
|
||||||
private int _index;
|
private int _index;
|
||||||
|
|
||||||
public int Index
|
public int Index
|
||||||
|
|||||||
+212
-11
@@ -11,6 +11,11 @@ using System.Threading.Tasks;
|
|||||||
|
|
||||||
namespace ZLGUSBCANFD
|
namespace ZLGUSBCANFD
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 周立功 USBCANFD 系列 CAN 卡控制类(如 USBCANFD-400U),封装设备连接、通道初始化、
|
||||||
|
/// DBC 矩阵加载与解码、报文发送(单次/循环)、信号物理值设置等功能。
|
||||||
|
/// 每个通道拥有独立的 DBC 引擎句柄与细粒度锁,支持多台架并行互不干扰。
|
||||||
|
/// </summary>
|
||||||
[ADPCommand]
|
[ADPCommand]
|
||||||
public class ZLGCANFD : IDisposable
|
public class ZLGCANFD : IDisposable
|
||||||
{
|
{
|
||||||
@@ -35,6 +40,10 @@ namespace ZLGUSBCANFD
|
|||||||
// 每次 设置报文 会累积写入此表,发送时以 DBC 初始值为底再叠加此表覆盖值
|
// 每次 设置报文 会累积写入此表,发送时以 DBC 初始值为底再叠加此表覆盖值
|
||||||
private readonly ConcurrentDictionary<(uint 通道号, uint 帧ID), Dictionary<string, double>> _signalOverrides = new ConcurrentDictionary<(uint, uint), Dictionary<string, double>>();
|
private readonly ConcurrentDictionary<(uint 通道号, uint 帧ID), Dictionary<string, double>> _signalOverrides = new ConcurrentDictionary<(uint, uint), Dictionary<string, double>>();
|
||||||
|
|
||||||
|
// 最新解码报文缓存:Key = (通道号, 帧ID),Value = 最近一次 DBC 解码成功的报文快照(含各信号原始值)
|
||||||
|
// 接收线程解码成功时写入,供 获取报文信号值 主动查询最新信号物理值
|
||||||
|
private readonly ConcurrentDictionary<(uint 通道号, uint 帧ID), ZDBC.DBCMessage> _latestDecodedMessages = new ConcurrentDictionary<(uint, uint), ZDBC.DBCMessage>();
|
||||||
|
|
||||||
// 动态硬件参数
|
// 动态硬件参数
|
||||||
private readonly uint _deviceType; // 76: USBCANFD-400U
|
private readonly uint _deviceType; // 76: USBCANFD-400U
|
||||||
private readonly uint _deviceIndex; // 设备索引
|
private readonly uint _deviceIndex; // 设备索引
|
||||||
@@ -56,6 +65,22 @@ namespace ZLGUSBCANFD
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public event Action<uint, ZDBC.DBCMessage>? OnDbcMessageDecoded;
|
public event Action<uint, ZDBC.DBCMessage>? OnDbcMessageDecoded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 事件:当接收到任意 CAN/CANFD 原始报文时触发(不依赖 DBC,自定义报文也能收到)
|
||||||
|
/// 参数:通道号, 帧ID, 数据, 时间戳(微秒), 是否FD, 数据长度
|
||||||
|
/// </summary>
|
||||||
|
public event Action<uint, uint, byte[], ulong, bool, byte>? OnRawMessageReceived;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 构造函数,配置设备类型、索引、通道数及所有通道共用的波特率与终端电阻参数,
|
||||||
|
/// 并初始化通道状态数组与 DBC 解析器。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="deviceType">设备类型,76 为 USBCANFD-400U</param>
|
||||||
|
/// <param name="deviceIndex">设备索引,多台同型号设备时区分,默认 0</param>
|
||||||
|
/// <param name="maxChannels">通道数量,默认 4</param>
|
||||||
|
/// <param name="abitBaud">仲裁域波特率,默认 "500000"</param>
|
||||||
|
/// <param name="dbitBaud">数据域波特率(仅 CANFD 生效),默认 "2000000"</param>
|
||||||
|
/// <param name="enableTerminalResistance">是否启用通道内部终端电阻,默认 true</param>
|
||||||
public ZLGCANFD(uint deviceType = 76, uint deviceIndex = 0, int maxChannels = 4,
|
public ZLGCANFD(uint deviceType = 76, uint deviceIndex = 0, int maxChannels = 4,
|
||||||
string abitBaud = "500000", string dbitBaud = "2000000", bool enableTerminalResistance = true)
|
string abitBaud = "500000", string dbitBaud = "2000000", bool enableTerminalResistance = true)
|
||||||
{
|
{
|
||||||
@@ -85,8 +110,9 @@ namespace ZLGUSBCANFD
|
|||||||
#region 1. 硬件连接与通道初始化
|
#region 1. 硬件连接与通道初始化
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 仅仅打开设备,不做具体的通道波特率配置(留给具体的台架去分别配置)
|
/// 仅仅打开设备,不做具体的通道波特率配置(留给具体的台架去分别配置)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <returns>是否打开成功;设备已打开时重复调用直接返回 true</returns>
|
||||||
public virtual bool 打开设备()
|
public virtual bool 打开设备()
|
||||||
{
|
{
|
||||||
if (_deviceHandle != IntPtr.Zero) return true;
|
if (_deviceHandle != IntPtr.Zero) return true;
|
||||||
@@ -95,8 +121,12 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 针对特定通道进行参数初始化并启动(使用构造时传入的波特率与终端电阻配置)
|
/// 针对特定通道进行参数初始化并启动(使用构造时传入的波特率与终端电阻配置),
|
||||||
|
/// 同时为该通道启动专属的后台高性能接收轮询线程。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="通道号">通道索引 (0 起始)</param>
|
||||||
|
/// <returns>是否初始化并启动成功;通道已启动时重复调用直接返回 true</returns>
|
||||||
|
/// <exception cref="InvalidOperationException">未先调用“打开设备()”时抛出</exception>
|
||||||
public virtual bool 初始化并启动通道(uint 通道号)
|
public virtual bool 初始化并启动通道(uint 通道号)
|
||||||
{
|
{
|
||||||
_isClosing = false; // 重置关闭标志,允许循环发送
|
_isClosing = false; // 重置关闭标志,允许循环发送
|
||||||
@@ -151,6 +181,10 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 关闭 CAN 卡设备:先停止所有循环发送与接收轮询线程,再复位全部通道、
|
||||||
|
/// 释放各通道 DBC 资源,最后关闭设备主句柄。
|
||||||
|
/// </summary>
|
||||||
public virtual void 关闭CAN卡设备()
|
public virtual void 关闭CAN卡设备()
|
||||||
{
|
{
|
||||||
_isClosing = true; // 通知循环发送任务尽快退出
|
_isClosing = true; // 通知循环发送任务尽快退出
|
||||||
@@ -212,8 +246,11 @@ namespace ZLGUSBCANFD
|
|||||||
#region 2. DBC 矩阵文件操作
|
#region 2. DBC 矩阵文件操作
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 指定通道加载特定的 DBC 文件(支持不同通道加载不同的DBC矩阵)
|
/// 指定通道加载特定的 DBC 文件(支持不同通道加载不同的DBC矩阵)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="通道号">通道索引 (0 起始)</param>
|
||||||
|
/// <param name="dbcFilePath">DBC 矩阵文件的完整路径</param>
|
||||||
|
/// <returns>是否加载成功;该通道已加载过则直接返回 true</returns>
|
||||||
public virtual bool 加载通道DBC文件(uint 通道号, string dbcFilePath)
|
public virtual bool 加载通道DBC文件(uint 通道号, string dbcFilePath)
|
||||||
{
|
{
|
||||||
if (通道号 >= _maxChannels) return false;
|
if (通道号 >= _maxChannels) return false;
|
||||||
@@ -251,6 +288,10 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 释放指定通道已加载的 DBC 资源,并清空该通道的报文数据库。
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="通道号">通道索引 (0 起始)</param>
|
||||||
public virtual void 释放通道DBC(uint 通道号)
|
public virtual void 释放通道DBC(uint 通道号)
|
||||||
{
|
{
|
||||||
if (通道号 >= _maxChannels) return;
|
if (通道号 >= _maxChannels) return;
|
||||||
@@ -263,6 +304,13 @@ namespace ZLGUSBCANFD
|
|||||||
_dbcHandles[通道号] = 0;
|
_dbcHandles[通道号] = 0;
|
||||||
_isDbcLoadedArray[通道号] = false;
|
_isDbcLoadedArray[通道号] = false;
|
||||||
DBCParser.MsgDatabase[(int)通道号].Clear();
|
DBCParser.MsgDatabase[(int)通道号].Clear();
|
||||||
|
|
||||||
|
// 同步清理该通道的最新解码报文缓存,避免残留旧信号值
|
||||||
|
foreach (var key in _latestDecodedMessages.Keys)
|
||||||
|
{
|
||||||
|
if (key.通道号 == 通道号)
|
||||||
|
_latestDecodedMessages.TryRemove(key, out _);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -297,12 +345,18 @@ namespace ZLGUSBCANFD
|
|||||||
{
|
{
|
||||||
IntPtr framePtr = IntPtr.Add(ptrCanBuffer, i * canStructSize);
|
IntPtr framePtr = IntPtr.Add(ptrCanBuffer, i * canStructSize);
|
||||||
|
|
||||||
|
// 触发原始报文事件(不依赖 DBC,自定义报文也能收到)
|
||||||
|
var rawData = (ZLGCAN.ZCAN_Receive_Data)Marshal.PtrToStructure(framePtr, typeof(ZLGCAN.ZCAN_Receive_Data))!;
|
||||||
|
OnRawMessageReceived?.Invoke(channelIndex, rawData.frame.can_id, rawData.frame.data, rawData.timestamp, false, rawData.frame.can_dlc);
|
||||||
|
|
||||||
// 只锁定当前通道的DBC句柄进行解码,高并发完全不卡顿
|
// 只锁定当前通道的DBC句柄进行解码,高并发完全不卡顿
|
||||||
lock (_channelLocks[channelIndex])
|
lock (_channelLocks[channelIndex])
|
||||||
{
|
{
|
||||||
if (_isDbcLoadedArray[channelIndex] && ZDBC.ZDBC_Decode(_dbcHandles[channelIndex], ptrDbcMsg, framePtr, 1, 0))
|
if (_isDbcLoadedArray[channelIndex] && ZDBC.ZDBC_Decode(_dbcHandles[channelIndex], ptrDbcMsg, framePtr, 1, 0))
|
||||||
{
|
{
|
||||||
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
|
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
|
||||||
|
// 缓存最新解码报文快照,供 获取报文信号值 查询
|
||||||
|
_latestDecodedMessages[(channelIndex, msg.nID)] = msg;
|
||||||
OnDbcMessageDecoded?.Invoke(channelIndex, msg);
|
OnDbcMessageDecoded?.Invoke(channelIndex, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -319,11 +373,17 @@ namespace ZLGUSBCANFD
|
|||||||
{
|
{
|
||||||
IntPtr framePtr = IntPtr.Add(ptrCanFDBuffer, i * canfdStructSize);
|
IntPtr framePtr = IntPtr.Add(ptrCanFDBuffer, i * canfdStructSize);
|
||||||
|
|
||||||
|
// 触发原始报文事件(不依赖 DBC,自定义报文也能收到)
|
||||||
|
var rawFdData = (ZLGCAN.ZCAN_ReceiveFD_Data)Marshal.PtrToStructure(framePtr, typeof(ZLGCAN.ZCAN_ReceiveFD_Data))!;
|
||||||
|
OnRawMessageReceived?.Invoke(channelIndex, rawFdData.frame.can_id, rawFdData.frame.data, rawFdData.timestamp, true, rawFdData.frame.len);
|
||||||
|
|
||||||
lock (_channelLocks[channelIndex])
|
lock (_channelLocks[channelIndex])
|
||||||
{
|
{
|
||||||
if (_isDbcLoadedArray[channelIndex] && ZDBC.ZDBC_Decode(_dbcHandles[channelIndex], ptrDbcMsg, framePtr, 1, 1))
|
if (_isDbcLoadedArray[channelIndex] && ZDBC.ZDBC_Decode(_dbcHandles[channelIndex], ptrDbcMsg, framePtr, 1, 1))
|
||||||
{
|
{
|
||||||
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
|
var msg = (ZDBC.DBCMessage)Marshal.PtrToStructure(ptrDbcMsg, typeof(ZDBC.DBCMessage));
|
||||||
|
// 缓存最新解码报文快照,供 获取报文信号值 查询
|
||||||
|
_latestDecodedMessages[(channelIndex, msg.nID)] = msg;
|
||||||
OnDbcMessageDecoded?.Invoke(channelIndex, msg);
|
OnDbcMessageDecoded?.Invoke(channelIndex, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -358,6 +418,8 @@ namespace ZLGUSBCANFD
|
|||||||
/// <param name="循环间隔毫秒">0 = 只发送一次;>0 = 按指定间隔循环发送(毫秒)</param>
|
/// <param name="循环间隔毫秒">0 = 只发送一次;>0 = 按指定间隔循环发送(毫秒)</param>
|
||||||
/// <param name="是否使用CANFD">1 = CANFD,0 = CAN</param>
|
/// <param name="是否使用CANFD">1 = CANFD,0 = CAN</param>
|
||||||
/// <returns>是否成功启动/发送</returns>
|
/// <returns>是否成功启动/发送</returns>
|
||||||
|
|
||||||
|
[Browsable(false)]
|
||||||
public virtual bool 发送DBC定义报文(
|
public virtual bool 发送DBC定义报文(
|
||||||
uint 通道号,
|
uint 通道号,
|
||||||
uint 帧ID,
|
uint 帧ID,
|
||||||
@@ -426,6 +488,8 @@ namespace ZLGUSBCANFD
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 停止指定通道、指定帧 ID 的循环发送。
|
/// 停止指定通道、指定帧 ID 的循环发送。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="通道号">通道索引 (0 起始)</param>
|
||||||
|
/// <param name="帧ID">要停止循环发送的报文帧 ID</param>
|
||||||
public virtual void 停止循环发送(uint 通道号, uint 帧ID)
|
public virtual void 停止循环发送(uint 通道号, uint 帧ID)
|
||||||
{
|
{
|
||||||
if (_cyclicSenders.TryRemove((通道号, 帧ID), out var entry))
|
if (_cyclicSenders.TryRemove((通道号, 帧ID), out var entry))
|
||||||
@@ -436,7 +500,7 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 停止所有 DBC 循环发送任务。
|
/// 停止所有 DBC 循环发送任务,并等待全部发送任务退出(最多 3 秒)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public virtual void 停止所有循环发送()
|
public virtual void 停止所有循环发送()
|
||||||
{
|
{
|
||||||
@@ -470,6 +534,8 @@ namespace ZLGUSBCANFD
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清除指定通道、指定帧 ID 的信号覆盖值,恢复为 DBC 初始值。
|
/// 清除指定通道、指定帧 ID 的信号覆盖值,恢复为 DBC 初始值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="通道号">通道索引 (0 起始)</param>
|
||||||
|
/// <param name="帧ID">要清除覆盖值的报文帧 ID</param>
|
||||||
public virtual void 清除信号覆盖(uint 通道号, uint 帧ID)
|
public virtual void 清除信号覆盖(uint 通道号, uint 帧ID)
|
||||||
{
|
{
|
||||||
if (_signalOverrides.TryRemove((通道号, 帧ID), out var dict))
|
if (_signalOverrides.TryRemove((通道号, 帧ID), out var dict))
|
||||||
@@ -479,8 +545,9 @@ namespace ZLGUSBCANFD
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 清除指定通道所有帧的信号覆盖值。
|
/// 清除指定通道所有帧的信号覆盖值,全部恢复为 DBC 初始值。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="通道号">通道索引 (0 起始)</param>
|
||||||
public virtual void 清除通道信号覆盖(uint 通道号)
|
public virtual void 清除通道信号覆盖(uint 通道号)
|
||||||
{
|
{
|
||||||
foreach (var key in _signalOverrides.Keys)
|
foreach (var key in _signalOverrides.Keys)
|
||||||
@@ -497,6 +564,7 @@ namespace ZLGUSBCANFD
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 单次发送 DBC 报文(调用方已持有通道锁)。
|
/// 单次发送 DBC 报文(调用方已持有通道锁)。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
private bool 发送DBC定义报文单次(uint 通道号, uint 帧ID, Dictionary<string, double> 信号物理值字典, int 是否使用CANFD)
|
private bool 发送DBC定义报文单次(uint 通道号, uint 帧ID, Dictionary<string, double> 信号物理值字典, int 是否使用CANFD)
|
||||||
{
|
{
|
||||||
uint chDbcHandle = _dbcHandles[通道号];
|
uint chDbcHandle = _dbcHandles[通道号];
|
||||||
@@ -618,6 +686,7 @@ namespace ZLGUSBCANFD
|
|||||||
/// <param name="信号名称">DBC 中定义的英文信号名称(不区分大小写)</param>
|
/// <param name="信号名称">DBC 中定义的英文信号名称(不区分大小写)</param>
|
||||||
/// <param name="物理值">要写入的实际物理数值</param>
|
/// <param name="物理值">要写入的实际物理数值</param>
|
||||||
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param>
|
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(单位毫秒)</param>
|
||||||
|
/// <param name="直接发送">true = 设置后立即发送;false = 仅记录到持久化覆盖表,等后续 发送报文 时生效</param>
|
||||||
/// <returns>操作是否成功</returns>
|
/// <returns>操作是否成功</returns>
|
||||||
public virtual bool 设置报文(uint 通道号, string 帧IDStr, string 信号名称, double 物理值, int 循环发送间隔毫秒 = 0, bool 直接发送 = false)
|
public virtual bool 设置报文(uint 通道号, string 帧IDStr, string 信号名称, double 物理值, int 循环发送间隔毫秒 = 0, bool 直接发送 = false)
|
||||||
{
|
{
|
||||||
@@ -653,6 +722,7 @@ namespace ZLGUSBCANFD
|
|||||||
/// <param name="帧IDStr">DBC 中定义的帧 ID(可以是 "26"、"0x1A"、"1A")</param>
|
/// <param name="帧IDStr">DBC 中定义的帧 ID(可以是 "26"、"0x1A"、"1A")</param>
|
||||||
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(毫秒)</param>
|
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(毫秒)</param>
|
||||||
/// <param name="是否使用CANFD">1 = 以 CANFD 格式发送;0 = 经典 CAN 格式</param>
|
/// <param name="是否使用CANFD">1 = 以 CANFD 格式发送;0 = 经典 CAN 格式</param>
|
||||||
|
/// <returns>是否成功启动/发送</returns>
|
||||||
public virtual bool 发送报文(uint 通道号, string 帧IDStr, int 循环发送间隔毫秒 = 0, int 是否使用CANFD = 1)
|
public virtual bool 发送报文(uint 通道号, string 帧IDStr, int 循环发送间隔毫秒 = 0, int 是否使用CANFD = 1)
|
||||||
{
|
{
|
||||||
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
||||||
@@ -671,6 +741,7 @@ namespace ZLGUSBCANFD
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 发送自定义报文(原始字节,帧 ID 支持 string 兼容模式)。
|
/// 发送自定义报文(原始字节,帧 ID 支持 string 兼容模式)。
|
||||||
|
/// 循环发送间隔毫秒 = 0 时只发送一次;>0 时按指定间隔循环发送,可调用 停止循环发送 停止。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="通道号">CAN/CANFD 通道索引</param>
|
/// <param name="通道号">CAN/CANFD 通道索引</param>
|
||||||
/// <param name="帧IDStr">帧 ID(可以是 "26"、"0x1A"、"1A");扩展帧会自动置位 0x80000000</param>
|
/// <param name="帧IDStr">帧 ID(可以是 "26"、"0x1A"、"1A");扩展帧会自动置位 0x80000000</param>
|
||||||
@@ -679,6 +750,8 @@ namespace ZLGUSBCANFD
|
|||||||
/// <param name="是否是扩展帧">是否为 29 位扩展帧</param>
|
/// <param name="是否是扩展帧">是否为 29 位扩展帧</param>
|
||||||
/// <param name="是否是CANFD">true = CANFD;false = 经典 CAN</param>
|
/// <param name="是否是CANFD">true = CANFD;false = 经典 CAN</param>
|
||||||
/// <param name="开启波特率加速BRS">CANFD 是否开启波特率加速</param>
|
/// <param name="开启波特率加速BRS">CANFD 是否开启波特率加速</param>
|
||||||
|
/// <param name="循环发送间隔毫秒">0 = 单次发送;>0 = 周期循环发送(毫秒)</param>
|
||||||
|
/// <returns>是否成功启动/发送</returns>
|
||||||
public virtual bool 发送自定义报文(
|
public virtual bool 发送自定义报文(
|
||||||
uint 通道号,
|
uint 通道号,
|
||||||
string 帧IDStr,
|
string 帧IDStr,
|
||||||
@@ -686,7 +759,8 @@ namespace ZLGUSBCANFD
|
|||||||
byte dlcLength,
|
byte dlcLength,
|
||||||
bool 是否是扩展帧 = false,
|
bool 是否是扩展帧 = false,
|
||||||
bool 是否是CANFD = true,
|
bool 是否是CANFD = true,
|
||||||
bool 开启波特率加速BRS = true)
|
bool 开启波特率加速BRS = true,
|
||||||
|
int 循环发送间隔毫秒 = 0)
|
||||||
{
|
{
|
||||||
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
||||||
{
|
{
|
||||||
@@ -696,23 +770,88 @@ namespace ZLGUSBCANFD
|
|||||||
|
|
||||||
if (通道号 >= _maxChannels || _channelHandles[通道号] == IntPtr.Zero) return false;
|
if (通道号 >= _maxChannels || _channelHandles[通道号] == IntPtr.Zero) return false;
|
||||||
if (原始数据 == null) return false;
|
if (原始数据 == null) return false;
|
||||||
|
if (循环发送间隔毫秒 < 0) throw new ArgumentOutOfRangeException(nameof(循环发送间隔毫秒), "循环发送间隔必须大于或等于 0。");
|
||||||
|
|
||||||
// 扩展帧需要把最高位标志位置起来
|
// 扩展帧需要把最高位标志位置起来
|
||||||
uint canId = 帧ID & 0x7FFFFFFF;
|
uint canId = 帧ID & 0x7FFFFFFF;
|
||||||
if (是否是扩展帧) canId |= 0x80000000;
|
if (是否是扩展帧) canId |= 0x80000000;
|
||||||
|
|
||||||
|
// 单次发送
|
||||||
|
if (循环发送间隔毫秒 == 0)
|
||||||
|
{
|
||||||
|
return 发送自定义报文单次(通道号, canId, 原始数据, dlcLength, 是否是扩展帧, 是否是CANFD, 开启波特率加速BRS);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 循环发送:先停止同通道同帧 ID 的旧循环,再启动新循环
|
||||||
|
停止循环发送(通道号, 帧ID);
|
||||||
|
|
||||||
|
// 拷贝原始数据,避免外部修改影响循环发送
|
||||||
|
byte[] dataCopy = new byte[原始数据.Length];
|
||||||
|
Array.Copy(原始数据, dataCopy, 原始数据.Length);
|
||||||
|
|
||||||
|
var cts = new CancellationTokenSource();
|
||||||
|
var sendTask = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
while (!cts.Token.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
if (_isClosing) break;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
lock (_channelLocks[通道号])
|
||||||
|
{
|
||||||
|
if (_channelHandles[通道号] == IntPtr.Zero || _isClosing) break;
|
||||||
|
发送自定义报文单次(通道号, canId, dataCopy, dlcLength, 是否是扩展帧, 是否是CANFD, 开启波特率加速BRS);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Delay(循环发送间隔毫秒, cts.Token);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LoggerHelper.Error($"[ZLGCANFD] 循环发送自定义报文失败: {ex.Message}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_cyclicSenders.TryGetValue((通道号, 帧ID), out var current) && ReferenceEquals(current.Cts, cts))
|
||||||
|
{
|
||||||
|
_cyclicSenders.TryRemove((通道号, 帧ID), out _);
|
||||||
|
}
|
||||||
|
cts.Dispose();
|
||||||
|
}, cts.Token);
|
||||||
|
|
||||||
|
_cyclicSenders[(通道号, 帧ID)] = (cts, sendTask);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 发送自定义报文的单次执行(内部方法,无循环逻辑)。
|
||||||
|
/// </summary>
|
||||||
|
[Browsable(false)]
|
||||||
|
private bool 发送自定义报文单次(
|
||||||
|
uint 通道号,
|
||||||
|
uint canId,
|
||||||
|
byte[] 原始数据,
|
||||||
|
byte dlcLength,
|
||||||
|
bool 是否是扩展帧,
|
||||||
|
bool 是否是CANFD,
|
||||||
|
bool 开启波特率加速BRS)
|
||||||
|
{
|
||||||
lock (_channelLocks[通道号])
|
lock (_channelLocks[通道号])
|
||||||
{
|
{
|
||||||
if (是否是CANFD)
|
if (是否是CANFD)
|
||||||
{
|
{
|
||||||
// CANFD 有效 DLC 校验
|
|
||||||
byte validFdDlc = 校验CANFD的Dlc(dlcLength);
|
byte validFdDlc = 校验CANFD的Dlc(dlcLength);
|
||||||
|
|
||||||
ZLGCAN.canfd_frame fdFrame = new ZLGCAN.canfd_frame
|
ZLGCAN.canfd_frame fdFrame = new ZLGCAN.canfd_frame
|
||||||
{
|
{
|
||||||
can_id = canId,
|
can_id = canId,
|
||||||
len = validFdDlc,
|
len = validFdDlc,
|
||||||
flags = (byte)((是否是扩展帧 ? 0x01 : 0x00) | (开启波特率加速BRS ? 0x02 : 0x00) | 0x20), // 0x20 本地回显
|
flags = (byte)((是否是扩展帧 ? 0x01 : 0x00) | (开启波特率加速BRS ? 0x02 : 0x00) | 0x20),
|
||||||
data = new byte[64]
|
data = new byte[64]
|
||||||
};
|
};
|
||||||
Array.Copy(原始数据, 0, fdFrame.data, 0, Math.Min(原始数据.Length, 64));
|
Array.Copy(原始数据, 0, fdFrame.data, 0, Math.Min(原始数据.Length, 64));
|
||||||
@@ -732,7 +871,7 @@ namespace ZLGUSBCANFD
|
|||||||
{
|
{
|
||||||
can_id = canId,
|
can_id = canId,
|
||||||
can_dlc = dlcLength > 8 ? (byte)8 : dlcLength,
|
can_dlc = dlcLength > 8 ? (byte)8 : dlcLength,
|
||||||
__pad = 0x20, // 发送回显
|
__pad = 0x20,
|
||||||
data = new byte[8]
|
data = new byte[8]
|
||||||
};
|
};
|
||||||
Array.Copy(原始数据, 0, standardFrame.data, 0, Math.Min(原始数据.Length, 8));
|
Array.Copy(原始数据, 0, standardFrame.data, 0, Math.Min(原始数据.Length, 8));
|
||||||
@@ -899,11 +1038,73 @@ namespace ZLGUSBCANFD
|
|||||||
|
|
||||||
#endregion
|
#endregion
|
||||||
|
|
||||||
#region 5. 辅助工具子系统
|
#region 4.2 报文信号查询
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 计算物理值也需要传入对应通道,使用通道专属的DBC句柄进行计算
|
/// 获取指定通道最新一帧 DBC 报文中某个信号的物理值(帧 ID 支持 string 兼容模式)。
|
||||||
|
/// <para>
|
||||||
|
/// 返回值为最近一次接收并解码成功的报文快照:物理值 = 原始值 × 因子 + 偏移,
|
||||||
|
/// 与监控界面广播的数值口径一致。若报文尚未收到、DBC 未加载、报文或信号名称不存在,
|
||||||
|
/// 将记录错误日志并返回 0。
|
||||||
|
/// </para>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
/// <param name="通道号">CAN/CANFD 通道索引</param>
|
||||||
|
/// <param name="帧IDStr">对应报文的帧 ID(可以是 "26"、"0x1A"、"1A")</param>
|
||||||
|
/// <param name="信号名称">DBC 中定义的英文信号名称(不区分大小写)</param>
|
||||||
|
/// <returns>信号物理值;失败时返回 0</returns>
|
||||||
|
public virtual double 获取报文信号值(uint 通道号, string 帧IDStr, string 信号名称)
|
||||||
|
{
|
||||||
|
if (!TryParseFrameId(帧IDStr, out uint 帧ID))
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[ZLGCANFD] 获取报文信号值失败: 无法解析的帧 ID '{帧IDStr}'");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (通道号 >= _maxChannels)
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[ZLGCANFD] 获取报文信号值失败: 通道号 {通道号} 越界(有效范围 0~{_maxChannels - 1})");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_isDbcLoadedArray[通道号])
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[ZLGCANFD] 获取报文信号值失败: 通道 {通道号} 尚未加载 DBC 文件");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(信号名称))
|
||||||
|
{
|
||||||
|
LoggerHelper.Error("[ZLGCANFD] 获取报文信号值失败: 信号名称为空");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 查找该通道该帧 ID 最近一次解码成功的报文快照
|
||||||
|
if (!_latestDecodedMessages.TryGetValue((通道号, 帧ID), out var msg))
|
||||||
|
{
|
||||||
|
LoggerHelper.Error($"[ZLGCANFD] 获取报文信号值失败: 通道 {通道号} 未收到帧 ID 0x{帧ID:X} 的报文(尚未接收或 DBC 中无此报文)");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 在报文信号集合中查找目标信号(不区分大小写,与 设置报文 的信号匹配口径一致)
|
||||||
|
for (int i = 0; i < msg.nSignalCount; i++)
|
||||||
|
{
|
||||||
|
var signal = msg.vSignals[i];
|
||||||
|
string name = Encoding.Default.GetString(signal.strName).TrimEnd('\0');
|
||||||
|
if (string.Equals(name, 信号名称, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// 物理值 = 原始值 × 因子 + 偏移(与 CANSignalBroadcaster 广播公式一致)
|
||||||
|
return signal.nRawvalue * signal.nFactor + signal.nOffset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerHelper.Error($"[ZLGCANFD] 获取报文信号值失败: 帧 ID 0x{帧ID:X} 的报文中不存在信号 '{信号名称}'");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
|
||||||
|
#region 5. 辅助工具子系统
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 智能解析帧 ID 字符串,兼容 10 进制、16 进制(如 "0x1A", "1A", "26")
|
/// 智能解析帧 ID 字符串,兼容 10 进制、16 进制(如 "0x1A", "1A", "26")
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
Reference in New Issue
Block a user